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