MIDAS
Loading...
Searching...
No Matches
filesrw.js
Go to the documentation of this file.
1/*
2
3 File read/write functions using mjsonrpc calls
4
5 Created by Zaher Salman on April 25th, 2023
6
7 Usage
8 =====
9
10 Open file selection dialogs to load and save files. The accessible
11 files are restricted to folders within the
12
13 experiment_directory/userfiles/
14
15 To use the file picker, you have to include this file in your custom page via
16
17 <scropt src="filesrw.js"></script>
18
19 Then you can use the file picker like:
20
21 To load a file: file_picker(subfolder, extension, callback, false, param, mkdir);
22 To save a file: file_picker(subfolder, extension, callback, true, param, mkdir);
23
24 - subfolder - is the subfolder of experiment_directory/userfiles/ to browse
25 - extension - show only files with this extension
26 - callback - (optional) function to call when save/load is pressed (or file doubleclicked)
27 the callback function will receive the selected file name as an argument
28 - saveflag - (optional) false for load dialog and true for save dialog
29 - param - (optional) extra parameter to pass to the callback function
30 - mkdir - (optional) true to allow creating subfolders
31
32 Featrures of the file_picker:
33
34 - Navigate using the keyboard to look for file names starting with the pressed key sequence
35 - Click on the colum headers (Name, Modified and Size) to sort the files accordingly.
36 - Click on the column header edge to change the width of the columns.
37 - Click on the folder with plus sign to create a new folder (if enabled)
38
39 Additional helper functions:
40
41 Save ascii file: file_save_ascii(filename, text, alert)
42
43 - filename - name of the file to save (relative to experiment_directory/userfiles/)
44 - text - ascii content of the file
45 - alert - (opt) string to be shown in dlgAlert when file is saved
46
47 Save ascii file: file_save_ascii_overwrite(filename, text, alert)
48
49 - filename - name of the file to save (relative to experiment_directory/userfiles/)
50 - text - ascii content of the file
51 - alert - (opt) string to be shown in dlgAlert when file is saved. Unlike
52 file_save_ascii, alert can be a function that will be called alert(filename).
53
54 Load ascii file: file_load_ascii(filename, callback)
55
56 - filename - name of the file to load (relative to experiment_directory/userfiles/)
57 - callback - call back function recieving the ascii content of the file
58
59 Load binary file: file_load_bin(filename, callback)
60
61 - filename - name of the file to load (relative to experiment_directory/userfiles/)
62 - callback - call back function recieving the binary content of the file
63
64*/
65
66
67
68var filesrw_css = `
69option:nth-of-type(even) {
70 background-color: #DDDDDD;
71}
72
73option:nth-of-type(odd) {
74 background-color: #EEEEEE;
75}
76
77.colTitle {
78 display: inline-block;
79 font-weight: bold;
80 height: 1.2em;
81 text-align: left;
82 padding-top: 5px;
83 padding-bottom: 0px;
84 padding-left: 0px;
85 padding-right: 0px;
86 overflow: hidden;
87}
88
89a {
90 color: #0000FF;
91 cursor: pointer;
92}
93
94.filesTable {
95 white-space: nowrap;
96 overflow: auto;
97 /*height: 100px;*/
98 vertical-align: top;
99 padding-top: 0px;
100 padding: 0px;
101 border-spacing: 0px;
102 border: 0;
103 table-layout:fixed;
104 margin-top: 0;
105}
106
107.filesTable td {
108 overflow: hidden;
109 text-align: left;
110 height: 1.2em;
111 text-overflow: ellipsis;
112}
113
114.filesTable th {
115 position: sticky;
116 top: 0;
117 z-index: 31;
118 font-weight: bold;
119 background-color: #C0C0C0;
120 text-align: left;
121 vertical-align: middle;
122 padding-top: 6px;
123 padding-bottom: 0px;
124 padding-right: 2px;
125 padding-left: 2px;
126 user-select: none;
127 overflow: hidden;
128 height: 1.2em;
129 border-top: 0;
130 border-bottom: 0;
131 border-left: 0;
132 /* border-right: 1px dotted black; */
133 border-right: 1px solid #A9A9A9;
134 border-collapse: collapse;
135}
136
137.filesTable th:last-child {
138 border-right: 0;
139}
140
141.filesTable tr:hover {
142 /*background-color: #004CBD;*/
143 cursor: pointer;
144}
145
146.filesTable tr.selFile td {
147 background-color: #004CBD;
148 color: white;
149 user-select: none;
150}
151
152.hlkeys{
153 background-color: yellow;
154 color: black;
155 font-weight: bold;
156}
157`;
158
159const fstyle = document.createElement('style');
160fstyle.textContent = filesrw_css;
161document.head.appendChild(fstyle);
162
163// Load or save file from server, possible only restricted path
164function file_browser(pathName, ext, funcCall, saveFlag, param = {}) {
165 /* Function to browse files to load/save
166 pathName - relative subdirectory of files to load
167 ext - extension of files to load
168 funcCall - (opt) call back function when/if file is selected
169 flag - (opt) true makes it a save dialog
170 param - (opt) extra parameter to pass to funcCall
171 */
172
173 let depthDir = sessionStorage.getItem("depthDir") || 0;
174
175 const d = select_file_dialog("dlgPrompt");
176 const fContainer = document.getElementById("fContainer");
177 const dlgButton = document.getElementById("dlgButton");
178 dlgButton.textContent = saveFlag ? "Save" : "Load";
179 if (saveFlag) {
180 const lblFilename = document.createElement("label");
181 lblFilename.innerHTML = "Save as: ";
182 lblFilename.style.textAlign = "left";
183 lblFilename.style.width = "100px";
184 lblFilename.style.paddingBottom = "5px";
185 lblFilename.style.verticalAlign = "middle";
186 const dlgFilename = document.createElement("input");
187 dlgFilename.id = "dlgFilename";
188 dlgFilename.type = "text";
189 dlgFilename.style.width = "calc(100% - 100px)";
190 fContainer.appendChild(lblFilename);
191 fContainer.appendChild(dlgFilename);
192 }
193
194 // Focus on fileSelect so user can scroll by typing
195 const sel = document.getElementById("fileSelect");
196 sel.focus();
197 // Empty selection - different
198 sel.innerHTML = "";
199
200 const folders = pathName.split("/");
201 const folderTree = document.getElementById("folderTree");
202 folderTree.innerHTML = "";
203 let tmpPath = ""
204 for (let i = 0; i < folders.length; i++) {
205 tmpPath += folders[i];
206 let addFolder = document.createElement("a");
207 // create closure to capture the current value of `i` and `tmpPath`
208 (function (i, tmpPath) {
209 addFolder.addEventListener("click", function () {
210 // Start all over with clicked folder
211 depthDir = i;
212 file_browser(tmpPath, ext, funcCall, saveFlag, param);
213 sessionStorage.setItem("depthDir", depthDir)
214 });
215 })(i, tmpPath);
216
217 tmpPath += "/";
218 if (i == 0) {
219 addFolder.innerHTML += '<img style="height:16px;vertical-align: middle;margin-bottom: 2px;" src="icons/slash-square.svg" title="Root folder"> ';
220 } else {
221 addFolder.innerText = folders[i] + "/";
222 }
223 folderTree.appendChild(addFolder);
224 }
225
226 const req = mjsonrpc_make_request("ext_list_files", {"subdir": pathName, "fileext": ext});
227 mjsonrpc_send_request(req).then(function (rpc) {
228 const fList = get_list_files(rpc.result.files, rpc.result.subdirs);
229 let count = 0;
230 let options = fList.map(function (item) {
231 let o = document.createElement("option");
232 let filename = item.filename;
233 let modtime = item.modtime;
234 let fsize = item.size;
235 // convert modtime from integer to date, here you can change format
236 let moddate = new Date(modtime * 1000).toString().split(" GMT")[0];
237
238 // fill lines with file names etc.
239 if (filename) {
240 o.innerHTML = `<span style="width:40%">${filename}</span>
241 <span style="width:60%;float:right;">
242 <span style="width:80%">${moddate}</span>
243 <span style="float:right;width:20%">${fsize}</span>
244 </span>`;
245 o.value = filename;
246 o.addEventListener("click", function () {
247 if (saveFlag) dlgFilename.value = this.value;
248 });
249 // add calback function if provided
250 o.addEventListener("dblclick", function () {
251 if (saveFlag) dlgFilename.value = this.value;
252 if (funcCall) {
253 funcCall(tmpPath + this.value, param);
254 }
255 sessionStorage.removeItem("depthDir");
256 d.remove();
257 });
258 } else {
259 let folderName = item.replace(/[\[\]]/g, "");
260 o.innerHTML = `<a>${folderName}</a>`;
261 const newPath = pathName + "/" + folderName;
262 // No dblclick to be mobile friendly
263 o.addEventListener("click", function () {
264 // Start all over with newPath
265 file_browser(newPath, ext, funcCall, saveFlag, param);
266 sessionStorage.setItem("depthDir", depthDir + 1)
267 });
268 }
269 count++;
270 return o;
271 });
272
273 if (options.length == 0) {
274 // empty list of files
275 let o = document.createElement("option");
276 o.innerHTML = "No files found!";
277 options = [o];
278 }
279
280 sel.append(...options);
281 sel.size = count + 2;
282
283 // sort the files by modified time
284 check_sorting();
285 }).catch(function (error) {
286 console.error(error);
287 });
288
289 dlgButton.addEventListener("click", function () {
290 const selFile = sel.value;
291 if (saveFlag) dlgFilename.value = selFile;
292 if (funcCall) {
293 funcCall(pathName + "/" + selFile, param);
294 }
295 sessionStorage.removeItem("depthDir");
296 d.remove();
297 });
298
299 return d;
300}
301
302// Load or save file from server, possible only restricted path
303function file_picker(pathName, ext, funcCall, saveFlag = false, param = {}, crtFldr = false) {
304 /* Function to browse files to load/save
305 pathName - relative subdirectory of files to load
306 ext - extension of files to load
307 funcCall - (opt) call back function when/if file is selected
308 flag - (opt) true makes it a save dialog
309 param - (opt) extra parameter to pass to funcCall
310 crtFldr - (opt) enable create new folder tool
311 */
312
313 let depthDir = sessionStorage.getItem("depthDir") || 0;
314
315 const d = table_file_dialog("dlgPrompt");
316 const fContainer = document.getElementById("fContainer");
317 const dlgButton = document.getElementById("dlgButton");
318 dlgButton.textContent = saveFlag ? "Save" : "Load";
319 if (saveFlag) {
320 const lblFilename = document.createElement("label");
321 lblFilename.innerHTML = "Save as: ";
322 lblFilename.style.textAlign = "left";
323 lblFilename.style.width = "100px";
324 lblFilename.style.paddingBottom = "5px";
325 lblFilename.style.verticalAlign = "middle";
326 const dlgFilename = document.createElement("input");
327 dlgFilename.id = "dlgFilename";
328 dlgFilename.type = "text";
329 dlgFilename.style.width = "calc(100% - 100px)";
330 fContainer.appendChild(lblFilename);
331 fContainer.appendChild(dlgFilename);
332 // Focus on filename and fill with previously used value or default
333 dlgFilename.focus();
334 let tmp_ext = ext != "*.*" ? ext.substring(ext.indexOf(".")+1) : "";
335 dlgFilename.value = sessionStorage.getItem("fileName") || "fname." + tmp_ext;
336 //dlgFilename.select();
337 // replace extension with the correct one if different
338 let index = dlgFilename.value.indexOf(".");
339 if (index) {
340 dlgFilename.value = dlgFilename.value.substring(0,index+1) + tmp_ext;
341 } else {
342 if (tmp_ext != "") {
343 dlgFilename.value = dlgFilename.value + "." + tmp_ext;
344 }
345 }
346 dlgFilename.setSelectionRange(0,index);
347 dlgFilename.addEventListener("keypress", function(event) {
348 if (event.key === "Enter") {
349 event.preventDefault();
350 dlgButton.click();
351 }
352 });
353 }
354
355 // Show tool bar to create folders if requested
356 if (crtFldr)
357 document.getElementById("pickerTools").style.display = "block";
358
359 const sel = document.getElementById("fileSelect");
360 const folders = pathName.split("/");
361 const folderTree = document.getElementById("folderTree");
362 // Handle create subdir clicks
363 const createSubdir = document.getElementById("createSubdir");
364 let subdirValue = "New Folder";
365 createSubdir.addEventListener("click", function () {
366 dlgQuery("New folder: ",subdirValue,create_subdir,pathName);
367 });
368
369 folderTree.innerHTML = "";
370 let tmpPath = ""
371 for (let i = 0; i < folders.length; i++) {
372 tmpPath += folders[i];
373 let addFolder = document.createElement("a");
374 // create closure to capture the current value of `i` and `tmpPath`
375 (function (i, tmpPath) {
376 addFolder.addEventListener("click", function () {
377 // Start all over with clicked folder
378 depthDir = i;
379 file_picker(tmpPath, ext, funcCall, saveFlag, param, crtFldr);
380 sessionStorage.setItem("depthDir", depthDir);
381 });
382 })(i, tmpPath);
383
384 tmpPath += "/";
385 if (i == 0) {
386 addFolder.innerHTML += '<img style="height:16px;vertical-align: middle;margin-bottom: 2px;" src="icons/slash-square.svg" title="Root folder"> ';
387 } else {
388 addFolder.innerText = folders[i] + "/";
389 }
390 if (i == folders.length -1)
391 addFolder.id = "lastFolder"; // mark last folder
392 folderTree.appendChild(addFolder);
393 }
394
395 const req = mjsonrpc_make_request("ext_list_files", {"subdir": pathName, "fileext": ext});
396 mjsonrpc_send_request(req).then(function (rpc) {
397 if (rpc.result.status !== 1) {
398 //throw new Error(pathName + " does not exist, create it first.");
399 // Or just create directories and call again
400 create_subdir(pathName,"");
401 }
402 const fList = get_list_files(rpc.result.files, rpc.result.subdirs);
403 let countFolder = 0;
404 let options = fList.map(function (item) {
405 let tr = document.createElement("tr");
406 let filename = item.filename;
407 let modtime = item.modtime;
408 let fsize = item.size;
409 // convert modtime from integer to date, here you can change format
410 let moddate = new Date(modtime * 1000).toString().split(" GMT")[0];
411
412 // fill lines with file names etc.
413 if (filename) {
414 tr.innerHTML = `<td>${filename}</td>
415 <td>${moddate}</td>
416 <td>${fsize}</td>`;
417 tr.addEventListener("click", function () {
418 if (saveFlag) dlgFilename.value = this.firstChild.innerText;
419 // deselect all
420 const selFiles = Array.from(document.getElementsByClassName('selFile'));
421 selFiles.forEach(selFile => {
422 selFile.classList.remove('selFile');
423 });
424 this.classList.add('selFile');
425 });
426 // add calback function if provided
427 tr.addEventListener("dblclick", function () {
428 if (saveFlag) {
429 dlgFilename.value = this.firstChild.innerText;
430 // Save the name to use later
431 sessionStorage.setItem("fileName", dlgFilename.value);
432 }
433 if (funcCall) {
434 tmpPath = tmpPath.replace(/\/\//g, "/");
435 funcCall(tmpPath + this.firstChild.innerText, param);
436 }
437 sessionStorage.removeItem("depthDir");
438 document.removeEventListener("keydown", tableClickHandler);
439 d.remove();
440 });
441 } else {
442 countFolder++;
443 let folderName = item.replace(/[\[\]]/g, "");
444 tr.innerHTML = `<td colspan="3"><a>${folderName}</a></td>`;
445 const newPath = pathName + "/" + folderName;
446 // No dblclick to be mobile friendly
447 tr.addEventListener("click", function () {
448 // Start all over with newPath
449 file_picker(newPath, ext, funcCall, saveFlag, param, crtFldr);
450 sessionStorage.setItem("depthDir", depthDir + 1)
451 });
452 }
453 return tr;
454 });
455
456 if (options.length == 0) {
457 // empty list of files
458 let tr = document.createElement("tr");
459 tr.innerHTML = "<td colspan='3'>No files found!</td>";
460 options = [tr];
461 }
462
463 sel.append(...options);
464 // add tabindex and focus on Modified column
465 const firstRow = sel.rows[0];
466 if (firstRow) {
467 const secondCell = firstRow.cells[1];
468 if (secondCell) {
469 secondCell.setAttribute("tabindex", "0");
470 if (typeof dlgFilename === 'undefined') secondCell.focus();
471 // sort the files by modified time
472 secondCell.click();
473 }
474 }
475 }).catch(function (error) {
476 console.error(error);
477 });
478
479 dlgButton.addEventListener("click", function () {
480 const selFiles = Array.from(document.getElementsByClassName('selFile'));
481 let selFile;
482 if ((!saveFlag && selFiles.length == 0) || (saveFlag && dlgFilename.value == "")) {
483 dlgAlert(saveFlag ? "Please select a file or provide a name." : "Please select a file.");
484 } else {
485 selFile = (typeof dlgFilename === 'undefined') ? selFiles[0].firstChild.innerText : dlgFilename.value;
486 // Save the name to use later
487 sessionStorage.setItem("fileName", selFile);
488 if (funcCall) {
489 funcCall(pathName + "/" + selFile, param);
490 }
491 sessionStorage.removeItem("depthDir");
492 document.removeEventListener("keydown", tableClickHandler);
493 d.remove();
494 }
495 });
496
497
498 // allow resizing table columns
499 resize_th();
500
501 return d;
502}
503
504// Populates a modal with a file load/save options in select
505function select_file_dialog(iddiv = "dlgFile", width = 600, height = 350, x, y) {
506 /* Load/save file dialog, optional parameters
507 iddiv - give the dialog a name
508 width/height - minimal width/height of dialog
509 x/y - initial position of dialog
510 */
511
512 // First make sure you removed exisitng iddiv
513 if (document.getElementById(iddiv)) document.getElementById(iddiv).remove();
514 const d = document.createElement("div");
515 d.className = "dlgFrame";
516 d.id = iddiv;
517 d.style.zIndex = "30";
518 // allow resizing modal
519 d.style.overflow = "hidden";
520 d.style.resize = "both";
521 d.style.minWidth = width + "px";
522 d.style.minHeight = height + "px";
523 d.style.width = width + "px";
524 d.style.height = height + "px";
525 d.shouldDestroy = true;
526
527 const dlgTitle = document.createElement("div");
528 dlgTitle.className = "dlgTitlebar";
529 dlgTitle.id = "dlgMessageTitle";
530 dlgTitle.innerText = "Select file dialog";
531 d.appendChild(dlgTitle);
532
533 const dlgPanel = document.createElement("div");
534 dlgPanel.className = "dlgPanel";
535 dlgPanel.id = "dlgPanel";
536 d.appendChild(dlgPanel);
537
538 const dlgFolder = document.createElement("div");
539 dlgFolder.innerHTML = '<span id="folderTree"><img style="height:16px;vertical-align: middle;margin-bottom: 2px;" src="icons/slash-square.svg" title="Current folder"> </span>';
540 dlgFolder.style.textAlign = "left";
541 dlgPanel.appendChild(dlgFolder);
542
543 const filesTable = document.createElement("div");
544 const colTitles = document.createElement("div");
545 colTitles.style.backgroundColor = "#C0C0C0";
546 colTitles.innerHTML = `
547 <span id='nameSort' class='colTitle' onclick='check_sorting(this);'>
548 &nbsp;Name
549 <img id='nameArrow' style='float:right;visibility:hidden;' src='icons/chevron-up.svg'>
550 </span>
551 <span id='timeSort' class='colTitle' onclick='check_sorting(this);'>
552 &nbsp;Modified
553 <img id='timeArrow' style='float:right;' src='icons/chevron-down.svg'>
554 </span>
555 <span id='sizeSort' class='colTitle' onclick='check_sorting(this);'>
556 &nbsp;Size
557 <img id='sizeArrow' style='float:right;' src='icons/chevron-down.svg'>
558 </span>`;
559 filesTable.appendChild(colTitles);
560
561 const fileSelect = document.createElement("select");
562 fileSelect.id = "fileSelect";
563 fileSelect.style.overflow = "auto";
564 fileSelect.style.height = "100%";
565 fileSelect.style.width = "100%";
566 filesTable.appendChild(fileSelect);
567 dlgPanel.appendChild(filesTable);
568
569 const fContainer = document.createElement("div");
570 fContainer.id = "fContainer";
571 fContainer.style.width = "100%";
572 fContainer.style.paddingTop = "5px";
573 fContainer.style.paddingBottom = "3px";
574 dlgPanel.appendChild(fContainer);
575 const btnContainer = document.createElement("div");
576 btnContainer.id = "btnContainer";
577 btnContainer.style.paddingBottom = "3px";
578 dlgPanel.appendChild(btnContainer);
579
580 const dlgButton = document.createElement("button");
581 dlgButton.className = "dlgButtonDefault";
582 dlgButton.id = "dlgButton";
583 dlgButton.type = "button";
584 btnContainer.appendChild(dlgButton);
585 const dlgCancelBtn = document.createElement("button");
586 dlgCancelBtn.className = "dlgButton";
587 dlgCancelBtn.id = "dlgCancelBtn";
588 dlgCancelBtn.type = "button";
589 dlgCancelBtn.textContent = "Cancel";
590 dlgCancelBtn.addEventListener("click", function () {
591 d.remove();
592 });
593 btnContainer.appendChild(dlgCancelBtn);
594
595 document.body.appendChild(d);
596 dlgShow(d);
597
598 if (x !== undefined && y !== undefined)
599 dlgMove(d, x, y);
600
601 // adjust select size when resizing modal
602 const resizeObs = new ResizeObserver(() => {
603 fileSelect.style.height = (d.offsetHeight - dlgTitle.offsetHeight - dlgFolder.offsetHeight - fContainer.offsetHeight - btnContainer.offsetHeight - colTitles.offsetHeight -5) + "px";
604 });
605 resizeObs.observe(d);
606
607 return d;
608}
609
610// Populates a modal with a file load/save options in select
611function table_file_dialog(iddiv = "dlgFile", width = 600, height = 350, x, y) {
612 /* Load/save file dialog, optional parameters
613 iddiv - give the dialog a name
614 width/height - minimal width/height of dialog
615 x/y - initial position of dialog
616 */
617
618 // First make sure you removed exisitng iddiv
619 if (document.getElementById(iddiv)) document.getElementById(iddiv).remove();
620 const d = document.createElement("div");
621 d.className = "dlgFrame";
622 d.id = iddiv;
623 d.style.zIndex = "30";
624 // allow resizing modal
625 d.style.overflow = "hidden";
626 d.style.resize = "both";
627 d.style.minWidth = width + "px";
628 d.style.minHeight = height + "px";
629 d.style.width = width + "px";
630 d.style.height = height + "px";
631 d.shouldDestroy = true;
632
633 const dlgTitle = document.createElement("div");
634 dlgTitle.className = "dlgTitlebar";
635 dlgTitle.id = "dlgMessageTitle";
636 dlgTitle.innerText = "Select file dialog";
637 d.appendChild(dlgTitle);
638
639 const dlgPanel = document.createElement("div");
640 dlgPanel.className = "dlgPanel";
641 dlgPanel.id = "dlgPanel";
642 dlgPanel.onclick = unselect_files;
643 d.appendChild(dlgPanel);
644
645 const dlgFolder = document.createElement("div");
646 dlgFolder.innerHTML = `
647 <div id="folderTree"><img style="height:16px;vertical-align: middle;margin-bottom: 2px;" src="icons/slash-square.svg" title="Root folder"> </div>
648 <div style="background-color: #DDDDDD;display: none;" id="pickerTools"><img style="padding: 5px;height:22px;vertical-align: middle;margin-bottom: 2px;" src="icons/folder-plus.svg" title="Create subdirectory" id="createSubdir"> </div>
649 `;
650 dlgFolder.style.textAlign = "left";
651 dlgPanel.appendChild(dlgFolder);
652
653 const filesTable = document.createElement("div");
654 filesTable.style.overflow = "auto";
655 const fileSelect = document.createElement("table");
656 fileSelect.className = "mtable dialogTable filesTable";
657 fileSelect.id = "fileSelect";
658 fileSelect.style.border = "none";
659 fileSelect.style.width = "100%";
660 //fileSelect.setAttribute("tabindex","0");
661 //fileSelect.focus();
662
663 const colTitles = fileSelect.createTHead();
664 colTitles.innerHTML = `
665 <tr>
666 <th id='nameSort' style='width: 45%;' onclick='check_sorting(this);'>Name
667 <img id='nameArrow' style='float:right;visibility:hidden;' src='icons/chevron-up.svg'>
668 </th>
669 <th id='timeSort' style='width: 40%;' onclick='check_sorting(this);'>Modified
670 <img id='timeArrow' style='float:right;' src='icons/chevron-down.svg'>
671 </th>
672 <th id='sizeSort' style='width: auto;' onclick='check_sorting(this);'>Size
673 <img id='sizeArrow' style='float:right;visibility:hidden;' src='icons/chevron-down.svg'>
674 </th>
675 </tr>`;
676 fileSelect.appendChild(document.createElement("tbody"));
677 filesTable.appendChild(fileSelect);
678 dlgPanel.appendChild(filesTable);
679
680 const fContainer = document.createElement("div");
681 fContainer.id = "fContainer";
682 fContainer.style.width = "100%";
683 fContainer.style.paddingTop = "5px";
684 fContainer.style.paddingBottom = "3px";
685 dlgPanel.appendChild(fContainer);
686 const btnContainer = document.createElement("div");
687 btnContainer.id = "btnContainer";
688 btnContainer.style.paddingBottom = "3px";
689 dlgPanel.appendChild(btnContainer);
690
691 const dlgButton = document.createElement("button");
692 dlgButton.className = "dlgButtonDefault";
693 dlgButton.id = "dlgButton";
694 dlgButton.type = "button";
695 btnContainer.appendChild(dlgButton);
696 const dlgCancelBtn = document.createElement("button");
697 dlgCancelBtn.className = "dlgButton";
698 dlgCancelBtn.id = "dlgCancelBtn";
699 dlgCancelBtn.type = "button";
700 dlgCancelBtn.textContent = "Cancel";
701 dlgCancelBtn.addEventListener("click", function () {
702 document.removeEventListener("keydown", tableClickHandler);
703 d.remove();
704 });
705 btnContainer.appendChild(dlgCancelBtn);
706
707 document.body.appendChild(d);
708 dlgShow(d);
709
710 if (x !== undefined && y !== undefined)
711 dlgMove(d, x, y);
712
713 // adjust select size when resizing modal
714 const resizeObs = new ResizeObserver(() => {
715 filesTable.style.height = (d.offsetHeight - dlgTitle.offsetHeight - dlgFolder.offsetHeight - fContainer.offsetHeight - btnContainer.offsetHeight - 5 ) + "px";
716 });
717 resizeObs.observe(d);
718
719 // Add key navigation
720 document.addEventListener("keydown", tableClickHandler);
721
722 return d;
723}
724
725// Handle create new folder
726function create_subdir(subDir,pathName = "") {
727 if (subDir == false) return;
728 if (!pathName.endsWith("/")) pathName += "/";
729 const fullPath = pathName + subDir;
730 // clean up the fullPath and split into folders
731 const folders = fullPath.replaceAll("..","").replaceAll("//","/").split("/");
732 // create folders one by one
733 let tmpPath = "";
734 for (let i = 0; i < folders.length; i++) {
735 if (folders[i] != "") {
736 tmpPath += "/" + folders[i];
737 const req = mjsonrpc_make_request("make_subdir", {"subdir": tmpPath});
738 mjsonrpc_send_request(req).then(function (rpc) {
739 if (document.getElementById("lastFolder"))
740 document.getElementById("lastFolder").click();
741 }).catch(function (error) {
742 console.error(error);
743 });
744 }
745 }
746}
747
748// Handle mouse click to unselect files
749function unselect_files() {
750 if (event.target.tagName === "DIV") {
751 const selFiles = document.querySelectorAll('.selFile');
752 selFiles.forEach(selFile => {
753 selFile.classList.remove('selFile');
754 });
755 }
756}
757
758// Handle key events in the files table created file_picker
759let sequenceOfLetters = '';
760let sequenceTimer = null;
761function tableClickHandler() {
762 // Do not take key events from editable elements
763 const target = event.target;
764 const tagName = target.tagName;
765 if (event.target.isContentEditable || tagName === "INPUT")
766 return;
767
768 const fileSelect = document.getElementById("fileSelect");
769 if (fileSelect) {
770 let selectedRow = document.querySelector('.selFile');
771 let selectedRowIndex = (selectedRow) ? selectedRow.rowIndex : 0;
772 const rows = fileSelect.rows;
773 const nRows = rows.length;
774 const key = event.key.toLowerCase();
775 // Ignore special keys (except arrow up/down and enter) and non-characters
776 if (key.length !== 1 && key !== "arrowup" && key !== "arrowdown" && key !== "enter") {
777 return;
778 }
779
780 if (key === "arrowup") {
781 // scroll up and loop
782 selectedRowIndex = (selectedRowIndex >= 2) ? selectedRowIndex - 1 : nRows - 1;
783 event.preventDefault();
784 } else if (key === "arrowdown") {
785 // scroll down and loop
786 selectedRowIndex = (selectedRowIndex < nRows - 1) ? selectedRowIndex + 1 : 1;
787 event.preventDefault();
788 } else if (key === "enter") {
789 const selFiles = document.querySelectorAll('.selFile');
790 var doubleClickEvent = new Event('dblclick');
791 selFiles[0].dispatchEvent(doubleClickEvent);
792 } else {
793 clearTimeout(sequenceTimer);
794 sequenceOfLetters = !sequenceOfLetters ? key : sequenceOfLetters + key;
795 sequenceTimer = setTimeout(() => {
796 sequenceOfLetters = '';
797 clearHighlights();
798 },1000);
799
800 for (let i = selectedRowIndex + 1; i < selectedRowIndex + nRows; i++) {
801 const rowIndex = i % nRows; // Calculate the current row index
802 const firstCellText = rows[rowIndex].childNodes[0].textContent;
803 let currentIndex = 0;
804 let matched = true; // Assume a match by default
805
806 for (let j = 0; j < sequenceOfLetters.length; j++) {
807 const letter = sequenceOfLetters[j];
808 if (currentIndex >= firstCellText.length || firstCellText[currentIndex].toLowerCase() !== letter) {
809 matched = false; // No match found
810 break;
811 }
812 currentIndex++;
813 }
814
815 if (matched) {
816 selectedRowIndex = rowIndex;
817 break;
818 }
819 }
820 clearHighlights();
821 // Apply new highlights based on sequenceOfLetters
822 if (sequenceOfLetters) {
823 applyHighlights(rows[selectedRowIndex].childNodes[0], sequenceOfLetters);
824 }
825 }
826
827 // de-select all files
828 const selFiles = document.querySelectorAll('.selFile');
829 selFiles.forEach(selFile => {
830 selFile.classList.remove('selFile');
831 });
832 rows[selectedRowIndex].scrollIntoView({behavior: 'smooth',block: 'center'});
833 rows[selectedRowIndex].classList.add('selFile');
834 }
835}
836
837// highlight characters in file selection table
838function applyHighlights(node, letters) {
839 const text = node.textContent;
840 let highlighted = '';
841 let currentIndex = 0;
842
843 for (let i = 0; i < text.length; i++) {
844 const originalChar = text[i];
845 const lowerCaseChar = originalChar.toLowerCase();
846
847 if (lowerCaseChar === letters[currentIndex]) {
848 highlighted += `<span class="hlkeys">${originalChar}</span>`;
849 currentIndex++;
850 } else {
851 highlighted += originalChar;
852 }
853
854 if (currentIndex === letters.length) {
855 highlighted += text.substring(i + 1);
856 break;
857 }
858 }
859
860 node.innerHTML = highlighted;
861}
862
863// clear highlighetd characters in file selction table
864function clearHighlights() {
865 const highlightedElements = document.querySelectorAll('.hlkeys');
866 highlightedElements.forEach(element => {
867 const parent = element.parentNode;
868 parent.replaceChild(document.createTextNode(element.textContent), element);
869 });
870}
871
872// get list of files and folders (in [])
873function get_list_files(files, subdirs) {
874 /* Sort array of file objects
875 files - array of file objects
876 subdirs - (optional) subdirst to be included in array
877 */
878
879 const fList = [];
880
881 // Consider subdirs
882 if (subdirs) {
883 fList.push(...subdirs.map((item) => `[${item}]`));
884 }
885
886 if (files) {
887 fList.push(...files);
888 }
889
890 return fList;
891}
892
893// load an ascii file from server using http request
894function file_load_ascii_http(file, callback){
895 // Both file and callback must be provided
896 if (!file || !callback) return;
897 const xhr = new XMLHttpRequest();
898 xhr.onload = () => {
899 if (xhr.status === 200) callback(xhr.responseText);
900 };
901 xhr.open('GET', `${file}?${Date.now()}`, true);
902 xhr.send();
903}
904
905// load an ascii file from server using mjsonrpc call
906function file_load_ascii(filename,callback){
907 /*
908 filename - the name of the file to save
909 callback - the call back function recieving the contect
910 */
911 // Both file and callback must be provided
912 if (!filename || !callback) return;
913 // Possible checks here for illegal names
914 if (filename == "") {
915 dlgAlert("Illegal empty file name! Please provide a file name.");
916 return;
917 }
918
919 const ext = "*" + filename.substr( filename.lastIndexOf("."));
920 const pathName = filename.substr(0,filename.lastIndexOf("/") + 1);
921 // File name without path
922 let fname = filename.substr(filename.lastIndexOf("/") + 1)
923
924 // Check if file exists before even trying
925 mjsonrpc_send_request(mjsonrpc_make_request("ext_list_files", {
926 "subdir": pathName,
927 "fileext": ext
928 })).then(function (rpc) {
929 // Get list of files, ignore subdirectories
930 const fList = get_list_files(rpc.result.files);
931 if (fList.map(({ filename }) => filename).indexOf(fname) > -1) {
932 // Prepare read request
933 const reqread = mjsonrpc_make_request("ext_read_file", { "filename": filename });
934 mjsonrpc_send_request(reqread).then(function (rpc2) {
935 if (rpc2.result.status != 1) {
936 throw new Error("Cannot read file, error: " + rpc2.result.error);
937 } else {
938 callback(rpc2.result.content);
939 return rpc2.result.content;
940 }
941 }).catch(function (error) {
942 console.error(error);
943 });
944 }
945 }).catch(function (error) {
946 console.error(error);
947 });
948}
949
950// Save file to server
951function file_save_ascii(filename, text, alert) {
952 /*
953 filename - the name of the file to save
954 text - the content of the file
955 alert - string to be shown in dlgAlert when file is saved
956 */
957
958 // Possible checks here for illegal names
959 if (filename === "") {
960 dlgAlert("Illegal empty file name! Please provide a file name.");
961 return;
962 }
963 const ext = "*" + filename.substring(filename.lastIndexOf("."));
964 const pathName = filename.substring(0,filename.lastIndexOf("/") + 1);
965 // File name without path
966 let fname = filename.substring(filename.lastIndexOf("/") + 1)
967
968 // ToDo: check if the mimetype is in ODB before accepting an extension
969
970 // Check if file already exists
971 mjsonrpc_send_request(mjsonrpc_make_request("ext_list_files", {
972 "subdir": pathName,
973 "fileext": ext
974 })).then(function (rpc) {
975 if (rpc.result.status !== 1) {
976 // throw new Error(rpc.result.error);
977 // Or just create directories and call again
978 create_subdir(pathName,"");
979 }
980 // Get list of files, ignore subdirectories
981 const fList = get_list_files(rpc.result.files);
982 if (fList.map(({filename}) => filename).indexOf(fname) > -1) {
983 // File exists, overwrite?
984 dlgConfirm("File " + filename + " exists. Overwrite it?", (flag) => {
985 if (flag === true)
986 file_save_ascii_overwrite(filename, text, alert);
987 });
988 return;
989 }
990 file_save_ascii_overwrite(filename, text, alert);
991 }).catch(function (error) {
992 console.error(error);
993 });
994}
995
996function file_save_ascii_overwrite(filename, text, alert) {
997 // Prepare save request
998 const reqsave = mjsonrpc_make_request("ext_save_file", {"filename": filename, "script": text});
999 mjsonrpc_send_request(reqsave).then(function (rpc) {
1000 if (rpc.result.status !== 1) {
1001 throw new Error("Cannot save file, error: " + rpc.result.error);
1002 } else {
1003
1004 // show dialog box if alert is a string, otherwise call if it's a funciton
1005 if (typeof alert === "string" && alert !== "")
1006 dlgAlert(alert);
1007 else if (typeof alert === "function")
1008 alert(filename);
1009 return filename;
1010 }
1011 }).catch(function (error) {
1012 console.error(error);
1013 });
1014}
1015
1016function sort_files_select(sort) {
1017 const select = document.getElementById("fileSelect");
1018 let fsType = select.nodeName;
1019 let options = "";
1020 if (fsType == "SELECT") {
1021 options = Array.from(select.options);
1022 } else {
1023 options = Array.from(select.rows);
1024 // Keep first row (labels).
1025 select.innerHTML = "";
1026 select.appendChild(options[0]);
1027 options.shift();
1028 }
1029
1030 options.sort((a, b) => {
1031 const aFields = a.innerText.split(/\n|\t/).map(field => field.trim());
1032 const bFields = b.innerText.split(/\n|\t/).map(field => field.trim());
1033 if (aFields.length === 1 || bFields.length === 1) {
1034 return 0; // Skip sorting if either option has only one field
1035 }
1036
1037 const aFilename = aFields[0];
1038 const bFilename = bFields[0];
1039 const aModtime = new Date(aFields[1]).getTime();
1040 const bModtime = new Date(bFields[1]).getTime();
1041 const aSize = aFields[2];
1042 const bSize = bFields[2];
1043 if (sort == "ni") {
1044 // Sort increasing alphabetically on filenames
1045 return aFilename.localeCompare(bFilename);
1046 } else if (sort == "nd") {
1047 // Sort increasing alphabetically on filenames
1048 return bFilename.localeCompare(aFilename);
1049 } else if (sort == "si") {
1050 // Sort increasing file size
1051 return (aSize - bSize);
1052 } else if (sort == "sd") {
1053 // Sort increasing file size
1054 return (bSize - aSize);
1055 } else if (sort == "ti") {
1056 // Sort increasing modification time
1057 return (aModtime - bModtime);
1058 } else {
1059 // Sort increasing modification time (default)
1060 return (bModtime - aModtime);
1061 }
1062 });
1063
1064 options.forEach((option) => {
1065 if (fsType == "SELECT") {
1066 select.add(option);
1067 } else {
1068 select.appendChild(option);
1069 }
1070 });
1071}
1072
1073function check_sorting(e) {
1074 if (e) {
1075 // Get the bounding rectangle of the <th> element and exclude clicks near the resize
1076 // boundaries.
1077 const thRect = e.getBoundingClientRect();
1078 const xFromLEdge = Math.abs(event.clientX - thRect.left);
1079 const xFromREdge = Math.abs(event.clientX - thRect.right);
1080 // exclude 10px from edges (5px if we want a tight fit).
1081 const excludedRange = 10;
1082 if (xFromLEdge < excludedRange || xFromREdge < excludedRange) {
1083 // Igone click
1084 return;
1085 }
1086 }
1087
1088 const nameArrow = document.getElementById("nameArrow");
1089 const timeArrow = document.getElementById("timeArrow");
1090 const sizeArrow = document.getElementById("sizeArrow");
1091
1092 let sort = "";
1093 if (!e) {
1094 e = timeArrow.parentElement;
1095 }
1096
1097 if (e.id === "nameSort") {
1098 sort = "n";
1099 nameArrow.style.visibility = "visible";
1100 timeArrow.style.visibility = "hidden";
1101 sizeArrow.style.visibility = "hidden";
1102 } else if (e.id === "sizeSort") {
1103 sort = "s";
1104 nameArrow.style.visibility = "hidden";
1105 timeArrow.style.visibility = "hidden";
1106 sizeArrow.style.visibility = "visible";
1107 } else {
1108 sort = "t";
1109 nameArrow.style.visibility = "hidden";
1110 timeArrow.style.visibility = "visible";
1111 sizeArrow.style.visibility = "hidden";
1112 }
1113
1114 sort += (e.childNodes[1].src === "" || e.childNodes[1].src.includes("chevron-down")) ? "d" : "i";
1115 e.childNodes[1].src = `icons/chevron-${sort.endsWith("d") ? "up" : "down"}.svg`;
1116 e.childNodes[1].alt = `${sort.startsWith("t") ? "Modified time" : "Name"} ${sort.endsWith("d") ? "decreasing" : "increasing"}`;
1117 // run the sorting
1118 sort_files_select(sort);
1119}
1120
1121function resize_th() {
1122 var pressed = false;
1123 var start = undefined;
1124 var startX, startWidth;
1125
1126 var tables = document.querySelectorAll(".filesTable");
1127 tables.forEach(function(table) {
1128 var thElements = table.querySelectorAll("th");
1129 thElements.forEach(function(th) {
1130 th.addEventListener("mousedown", function(e) {
1131 if (!pressed) {
1132 var rect = this.getBoundingClientRect();
1133 var edgeWidth = 5; // Adjust this value to change the clickable edge width
1134
1135 if (e.pageX >= rect.right - edgeWidth) {
1136 start = this;
1137 pressed = true;
1138 startX = e.pageX;
1139 startWidth = this.offsetWidth;
1140 start.classList.add("resizing");
1141 start.classList.add("noSelect");
1142 }
1143 }
1144 });
1145
1146 th.addEventListener("click", function(e) {
1147 if (pressed) {
1148 e.stopPropagation();
1149 e.preventDefault();
1150 }
1151 });
1152 });
1153
1154 table.addEventListener("mousemove", function(e) {
1155 var thElements = table.querySelectorAll("th");
1156 thElements.forEach(function(th) {
1157 var rect = th.getBoundingClientRect();
1158 var edgeWidth = 5; // Adjust this value to match the clickable edge width
1159
1160 if (e.pageX >= rect.right - edgeWidth) {
1161 th.style.cursor = "ew-resize";
1162 } else {
1163 th.style.cursor = "default";
1164 }
1165 });
1166 });
1167 });
1168
1169 document.addEventListener("mousemove", function(e) {
1170 if (pressed) {
1171 start.style.width = startWidth + (e.pageX - startX) + "px";
1172 }
1173 });
1174
1175 document.addEventListener("mouseup", function() {
1176 if (pressed) {
1177 start.classList.remove("resizing");
1178 start.classList.remove("noSelect");
1179 pressed = false;
1180 }
1181 });
1182
1183}
1184
1185
1186// load a binary file from server using mjsonrpc call
1187function file_load_bin(filename,callback){
1188 /*
1189 filename - the name of the file to save
1190 callback - the call back function receiving the content
1191 */
1192 // Both file and callback must be provided
1193 if (!filename || !callback) return;
1194 // Possible checks here for illegal names
1195 if (filename === "") {
1196 dlgAlert("Illegal empty file name! Please provide a file name.");
1197 return;
1198 }
1199
1200 const ext = "*" + filename.substring( filename.lastIndexOf("."));
1201 const pathName = filename.substring(0,filename.lastIndexOf("/") + 1);
1202 // File name without path
1203 let fname = filename.substring(filename.lastIndexOf("/") + 1)
1204
1205 // Check if file exists before even trying
1206 mjsonrpc_send_request(mjsonrpc_make_request("ext_list_files", {
1207 "subdir": pathName,
1208 "fileext": ext
1209 })).then(function (rpc) {
1210 // Get list of files, ignore subdirectories
1211 const fList = get_list_files(rpc.result.files);
1212 if (fList.map(({ filename }) => filename).indexOf(fname) > -1) {
1213 // Prepare read request
1214 mjsonrpc_call("read_binary_file", { "filename": filename },"arraybuffer").then(function (result) {
1215 let array = new Uint8Array(result)
1216 if (array) {
1217 callback(array);
1218 return array;
1219 } else {
1220 throw new Error("Cannot read file or file is empty.");
1221 }
1222 }).catch(function (error) {
1223 console.error(error);
1224 });
1225 }
1226 }).catch(function (error) {
1227 console.error(error);
1228 });
1229}
1230
1231/* emacs
1232 * Local Variables:
1233 * tab-width: 8
1234 * c-basic-offset: 3
1235 * js-indent-level: 3
1236 * indent-tabs-mode: nil
1237 * End:
1238 */