3 File read/write functions using mjsonrpc calls
5 Created by Zaher Salman on April 25th, 2023
10 Open file selection dialogs to load and save files. The accessible
11 files are restricted to folders within the
13 experiment_directory/userfiles/
15 To use the file picker, you have to include this file in your custom page via
17 <scropt src="filesrw.js"></script>
19 Then you can use the file picker like:
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);
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
32 Featrures of the file_picker:
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)
39 Additional helper functions:
41 Save ascii file: file_save_ascii(filename, text, alert)
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
47 Save ascii file: file_save_ascii_overwrite(filename, text, alert)
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).
54 Load ascii file: file_load_ascii(filename, callback)
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
59 Load binary file: file_load_bin(filename, callback)
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
69option:nth-of-type(even) {
70 background-color: #DDDDDD;
73option:nth-of-type(odd) {
74 background-color: #EEEEEE;
78 display: inline-block;
106 border-collapse: collapse;
120 text-overflow: ellipsis;
128 background-color: #C0C0C0;
130 vertical-align: middle;
141 /* border-right: 1px dotted black; */
142 border-right: 1px solid #A9A9A9;
143 border-collapse: collapse;
146.filesTable th:last-child {
150.filesTable tr:hover {
151 /*background-color: #004CBD;*/
155.filesTable tr.selFile td {
156 background-color: #004CBD;
162 background-color: yellow;
168const fstyle = document.createElement('style');
169fstyle.textContent = filesrw_css;
170document.head.appendChild(fstyle);
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
182 let depthDir = sessionStorage.getItem("depthDir") || 0;
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";
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);
203 // Focus on fileSelect so user can scroll by typing
204 const sel = document.getElementById("fileSelect");
206 // Empty selection - different
209 const folders = pathName.split("/");
210 const folderTree = document.getElementById("folderTree");
211 folderTree.innerHTML = "";
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
221 file_browser(tmpPath, ext, funcCall, saveFlag, param);
222 sessionStorage.setItem("depthDir", depthDir)
228 addFolder.innerHTML += '<img style="height:16px;vertical-align: middle;margin-bottom: 2px;" src="icons/slash-square.svg" title="Root folder"> ';
230 addFolder.innerText = folders[i] + "/";
232 folderTree.appendChild(addFolder);
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);
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];
247 // fill lines with file names etc.
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>
255 o.addEventListener("click", function () {
256 if (saveFlag) dlgFilename.value = this.value;
258 // add calback function if provided
259 o.addEventListener("dblclick", function () {
260 if (saveFlag) dlgFilename.value = this.value;
262 funcCall(tmpPath + this.value, param);
264 sessionStorage.removeItem("depthDir");
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)
282 if (options.length == 0) {
283 // empty list of files
284 let o = document.createElement("option");
285 o.innerHTML = "No files found!";
289 sel.append(...options);
290 sel.size = count + 2;
292 // sort the files by modified time
294 }).catch(function (error) {
295 console.error(error);
298 dlgButton.addEventListener("click", function () {
299 const selFile = sel.value;
300 if (saveFlag) dlgFilename.value = selFile;
302 funcCall(pathName + "/" + selFile, param);
304 sessionStorage.removeItem("depthDir");
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
322 let depthDir = sessionStorage.getItem("depthDir") || 0;
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";
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
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(".");
349 dlgFilename.value = dlgFilename.value.substring(0,index+1) + tmp_ext;
352 dlgFilename.value = dlgFilename.value + "." + tmp_ext;
355 dlgFilename.setSelectionRange(0,index);
356 dlgFilename.addEventListener("keypress", function(event) {
357 if (event.key === "Enter") {
358 event.preventDefault();
364 // Show tool bar to create folders if requested
366 document.getElementById("pickerTools").style.display = "block";
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);
378 folderTree.innerHTML = "";
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
388 file_picker(tmpPath, ext, funcCall, saveFlag, param, crtFldr);
389 sessionStorage.setItem("depthDir", depthDir);
395 addFolder.innerHTML += '<img style="height:16px;vertical-align: middle;margin-bottom: 2px;" src="icons/slash-square.svg" title="Root folder"> ';
397 addFolder.innerText = folders[i] + "/";
399 if (i == folders.length -1)
400 addFolder.id = "lastFolder"; // mark last folder
401 folderTree.appendChild(addFolder);
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,"");
411 const fList = get_list_files(rpc.result.files, rpc.result.subdirs);
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];
421 // fill lines with file names etc.
423 tr.innerHTML = `<td>${filename}</td>
426 tr.addEventListener("click", function () {
427 if (saveFlag) dlgFilename.value = this.firstChild.innerText;
429 const selFiles = Array.from(document.getElementsByClassName('selFile'));
430 selFiles.forEach(selFile => {
431 selFile.classList.remove('selFile');
433 this.classList.add('selFile');
435 // add calback function if provided
436 tr.addEventListener("dblclick", function () {
438 dlgFilename.value = this.firstChild.innerText;
439 // Save the name to use later
440 sessionStorage.setItem("fileName", dlgFilename.value);
443 tmpPath = tmpPath.replace(/\/\//g, "/");
444 funcCall(tmpPath + this.firstChild.innerText, param);
446 sessionStorage.removeItem("depthDir");
447 document.removeEventListener("keydown", tableClickHandler);
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)
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>";
472 sel.append(...options);
473 // add tabindex and focus on Modified column
474 const firstRow = sel.rows[0];
476 const secondCell = firstRow.cells[1];
478 secondCell.setAttribute("tabindex", "0");
479 if (typeof dlgFilename === 'undefined') secondCell.focus();
480 // sort the files by modified time
484 }).catch(function (error) {
485 console.error(error);
488 dlgButton.addEventListener("click", function () {
489 const selFiles = Array.from(document.getElementsByClassName('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.");
494 selFile = (typeof dlgFilename === 'undefined') ? selFiles[0].firstChild.innerText : dlgFilename.value;
495 // Save the name to use later
496 sessionStorage.setItem("fileName", selFile);
498 funcCall(pathName + "/" + selFile, param);
500 sessionStorage.removeItem("depthDir");
501 document.removeEventListener("keydown", tableClickHandler);
507 // allow resizing table columns
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
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";
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;
536 const dlgTitle = document.createElement("div");
537 dlgTitle.className = "dlgTitlebar";
538 dlgTitle.id = "dlgMessageTitle";
539 dlgTitle.innerText = "Select file dialog";
540 d.appendChild(dlgTitle);
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);
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);
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);'>
560 <img id='nameArrow' style='float:right;visibility:hidden;' src='icons/chevron-up.svg'>
562 <span id='timeSort' class='colTitle' onclick='check_sorting(this);'>
564 <img id='timeArrow' style='float:right;' src='icons/chevron-down.svg'>
566 <span id='sizeSort' class='colTitle' onclick='check_sorting(this);'>
568 <img id='sizeArrow' style='float:right;' src='icons/chevron-down.svg'>
570 filesTable.appendChild(colTitles);
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);
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);
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 () {
604 btnContainer.appendChild(dlgCancelBtn);
606 document.body.appendChild(d);
609 if (x !== undefined && y !== undefined)
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";
616 resizeObs.observe(d);
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
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";
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;
644 const dlgTitle = document.createElement("div");
645 dlgTitle.className = "dlgTitlebar";
646 dlgTitle.id = "dlgMessageTitle";
647 dlgTitle.innerText = "Select file dialog";
648 d.appendChild(dlgTitle);
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);
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>
665 dlgFolder.style.textAlign = "left";
666 dlgPanel.appendChild(dlgFolder);
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%";
678 const colTitles = fileSelect.createTHead();
679 colTitles.innerHTML = `
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'>
684 <th id='timeSort' style='width: 40%;' onclick='check_sorting(this);'>Modified
685 <img id='timeArrow' style='float:right;' src='icons/chevron-down.svg'>
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'>
691 fileSelect.appendChild(document.createElement("tbody"));
692 filesTable.appendChild(fileSelect);
693 dlgPanel.appendChild(filesTable);
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);
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);
720 btnContainer.appendChild(dlgCancelBtn);
722 document.body.appendChild(d);
725 if (x !== undefined && y !== undefined)
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";
732 resizeObs.observe(d);
734 // Add key navigation
735 document.addEventListener("keydown", tableClickHandler);
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
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);
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');
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")
783 const fileSelect = document.getElementById("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") {
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);
808 clearTimeout(sequenceTimer);
809 sequenceOfLetters = !sequenceOfLetters ? key : sequenceOfLetters + key;
810 sequenceTimer = setTimeout(() => {
811 sequenceOfLetters = '';
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
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
831 selectedRowIndex = rowIndex;
836 // Apply new highlights based on sequenceOfLetters
837 if (sequenceOfLetters) {
838 applyHighlights(rows[selectedRowIndex].childNodes[0], sequenceOfLetters);
842 // de-select all files
843 const selFiles = document.querySelectorAll('.selFile');
844 selFiles.forEach(selFile => {
845 selFile.classList.remove('selFile');
847 rows[selectedRowIndex].scrollIntoView({behavior: 'smooth',block: 'center'});
848 rows[selectedRowIndex].classList.add('selFile');
852// highlight characters in file selection table
853function applyHighlights(node, letters) {
854 const text = node.textContent;
855 let highlighted = '';
856 let currentIndex = 0;
858 for (let i = 0; i < text.length; i++) {
859 const originalChar = text[i];
860 const lowerCaseChar = originalChar.toLowerCase();
862 if (lowerCaseChar === letters[currentIndex]) {
863 highlighted += `<span class="hlkeys">${originalChar}</span>`;
866 highlighted += originalChar;
869 if (currentIndex === letters.length) {
870 highlighted += text.substring(i + 1);
875 node.innerHTML = highlighted;
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);
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
898 fList.push(...subdirs.map((item) => `[${item}]`));
902 fList.push(...files);
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();
914 if (xhr.status === 200) callback(xhr.responseText);
916 xhr.open('GET', `${file}?${Date.now()}`, true);
920// load an ascii file from server using mjsonrpc call
921function file_load_ascii(filename,callback){
923 filename - the name of the file to save
924 callback - the call back function recieving the contect
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.");
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)
939 // Check if file exists before even trying
940 mjsonrpc_send_request(mjsonrpc_make_request("ext_list_files", {
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);
953 callback(rpc2.result.content);
954 return rpc2.result.content;
956 }).catch(function (error) {
957 console.error(error);
960 }).catch(function (error) {
961 console.error(error);
965// Save file to server
966function file_save_ascii(filename, text, alert) {
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
973 // Possible checks here for illegal names
974 if (filename === "") {
975 dlgAlert("Illegal empty file name! Please provide a file name.");
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)
983 // ToDo: check if the mimetype is in ODB before accepting an extension
985 // Check if file already exists
986 mjsonrpc_send_request(mjsonrpc_make_request("ext_list_files", {
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,"");
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) => {
1001 file_save_ascii_overwrite(filename, text, alert);
1005 file_save_ascii_overwrite(filename, text, alert);
1006 }).catch(function (error) {
1007 console.error(error);
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);
1019 // show dialog box if alert is a string, otherwise call if it's a funciton
1020 if (typeof alert === "string" && alert !== "")
1022 else if (typeof alert === "function")
1026 }).catch(function (error) {
1027 console.error(error);
1031function sort_files_select(sort) {
1032 const select = document.getElementById("fileSelect");
1033 let fsType = select.nodeName;
1035 if (fsType == "SELECT") {
1036 options = Array.from(select.options);
1038 options = Array.from(select.rows);
1039 // Keep first row (labels).
1040 select.innerHTML = "";
1041 select.appendChild(options[0]);
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
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];
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);
1074 // Sort increasing modification time (default)
1075 return (bModtime - aModtime);
1079 options.forEach((option) => {
1080 if (fsType == "SELECT") {
1083 select.appendChild(option);
1088function check_sorting(e) {
1090 // Get the bounding rectangle of the <th> element and exclude clicks near the resize
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) {
1103 const nameArrow = document.getElementById("nameArrow");
1104 const timeArrow = document.getElementById("timeArrow");
1105 const sizeArrow = document.getElementById("sizeArrow");
1109 e = timeArrow.parentElement;
1112 if (e.id === "nameSort") {
1114 nameArrow.style.visibility = "visible";
1115 timeArrow.style.visibility = "hidden";
1116 sizeArrow.style.visibility = "hidden";
1117 } else if (e.id === "sizeSort") {
1119 nameArrow.style.visibility = "hidden";
1120 timeArrow.style.visibility = "hidden";
1121 sizeArrow.style.visibility = "visible";
1124 nameArrow.style.visibility = "hidden";
1125 timeArrow.style.visibility = "visible";
1126 sizeArrow.style.visibility = "hidden";
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"}`;
1133 sort_files_select(sort);
1136function resize_th() {
1137 var pressed = false;
1138 var start = undefined;
1139 var startX, startWidth;
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) {
1147 var rect = this.getBoundingClientRect();
1148 var edgeWidth = 5; // Adjust this value to change the clickable edge width
1150 if (e.pageX >= rect.right - edgeWidth) {
1154 startWidth = this.offsetWidth;
1155 start.classList.add("resizing");
1156 start.classList.add("noSelect");
1161 th.addEventListener("click", function(e) {
1163 e.stopPropagation();
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
1175 if (e.pageX >= rect.right - edgeWidth) {
1176 th.style.cursor = "ew-resize";
1178 th.style.cursor = "default";
1184 document.addEventListener("mousemove", function(e) {
1186 start.style.width = startWidth + (e.pageX - startX) + "px";
1190 document.addEventListener("mouseup", function() {
1192 start.classList.remove("resizing");
1193 start.classList.remove("noSelect");
1201// load a binary file from server using mjsonrpc call
1202function file_load_bin(filename,callback){
1204 filename - the name of the file to save
1205 callback - the call back function receiving the content
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.");
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)
1220 // Check if file exists before even trying
1221 mjsonrpc_send_request(mjsonrpc_make_request("ext_list_files", {
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)
1235 throw new Error("Cannot read file or file is empty.");
1237 }).catch(function (error) {
1238 console.error(error);
1241 }).catch(function (error) {
1242 console.error(error);
1250 * js-indent-level: 3
1251 * indent-tabs-mode: nil