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;
111 text-overflow: ellipsis;
119 background-color: #C0C0C0;
121 vertical-align: middle;
132 /* border-right: 1px dotted black; */
133 border-right: 1px solid #A9A9A9;
134 border-collapse: collapse;
137.filesTable th:last-child {
141.filesTable tr:hover {
142 /*background-color: #004CBD;*/
146.filesTable tr.selFile td {
147 background-color: #004CBD;
153 background-color: yellow;
159const fstyle = document.createElement('style');
160fstyle.textContent = filesrw_css;
161document.head.appendChild(fstyle);
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
173 let depthDir = sessionStorage.getItem("depthDir") || 0;
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";
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);
194 // Focus on fileSelect so user can scroll by typing
195 const sel = document.getElementById("fileSelect");
197 // Empty selection - different
200 const folders = pathName.split("/");
201 const folderTree = document.getElementById("folderTree");
202 folderTree.innerHTML = "";
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
212 file_browser(tmpPath, ext, funcCall, saveFlag, param);
213 sessionStorage.setItem("depthDir", depthDir)
219 addFolder.innerHTML += '<img style="height:16px;vertical-align: middle;margin-bottom: 2px;" src="icons/slash-square.svg" title="Root folder"> ';
221 addFolder.innerText = folders[i] + "/";
223 folderTree.appendChild(addFolder);
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);
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];
238 // fill lines with file names etc.
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>
246 o.addEventListener("click", function () {
247 if (saveFlag) dlgFilename.value = this.value;
249 // add calback function if provided
250 o.addEventListener("dblclick", function () {
251 if (saveFlag) dlgFilename.value = this.value;
253 funcCall(tmpPath + this.value, param);
255 sessionStorage.removeItem("depthDir");
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)
273 if (options.length == 0) {
274 // empty list of files
275 let o = document.createElement("option");
276 o.innerHTML = "No files found!";
280 sel.append(...options);
281 sel.size = count + 2;
283 // sort the files by modified time
285 }).catch(function (error) {
286 console.error(error);
289 dlgButton.addEventListener("click", function () {
290 const selFile = sel.value;
291 if (saveFlag) dlgFilename.value = selFile;
293 funcCall(pathName + "/" + selFile, param);
295 sessionStorage.removeItem("depthDir");
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
313 let depthDir = sessionStorage.getItem("depthDir") || 0;
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";
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
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(".");
340 dlgFilename.value = dlgFilename.value.substring(0,index+1) + tmp_ext;
343 dlgFilename.value = dlgFilename.value + "." + tmp_ext;
346 dlgFilename.setSelectionRange(0,index);
347 dlgFilename.addEventListener("keypress", function(event) {
348 if (event.key === "Enter") {
349 event.preventDefault();
355 // Show tool bar to create folders if requested
357 document.getElementById("pickerTools").style.display = "block";
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);
369 folderTree.innerHTML = "";
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
379 file_picker(tmpPath, ext, funcCall, saveFlag, param, crtFldr);
380 sessionStorage.setItem("depthDir", depthDir);
386 addFolder.innerHTML += '<img style="height:16px;vertical-align: middle;margin-bottom: 2px;" src="icons/slash-square.svg" title="Root folder"> ';
388 addFolder.innerText = folders[i] + "/";
390 if (i == folders.length -1)
391 addFolder.id = "lastFolder"; // mark last folder
392 folderTree.appendChild(addFolder);
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,"");
402 const fList = get_list_files(rpc.result.files, rpc.result.subdirs);
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];
412 // fill lines with file names etc.
414 tr.innerHTML = `<td>${filename}</td>
417 tr.addEventListener("click", function () {
418 if (saveFlag) dlgFilename.value = this.firstChild.innerText;
420 const selFiles = Array.from(document.getElementsByClassName('selFile'));
421 selFiles.forEach(selFile => {
422 selFile.classList.remove('selFile');
424 this.classList.add('selFile');
426 // add calback function if provided
427 tr.addEventListener("dblclick", function () {
429 dlgFilename.value = this.firstChild.innerText;
430 // Save the name to use later
431 sessionStorage.setItem("fileName", dlgFilename.value);
434 tmpPath = tmpPath.replace(/\/\//g, "/");
435 funcCall(tmpPath + this.firstChild.innerText, param);
437 sessionStorage.removeItem("depthDir");
438 document.removeEventListener("keydown", tableClickHandler);
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)
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>";
463 sel.append(...options);
464 // add tabindex and focus on Modified column
465 const firstRow = sel.rows[0];
467 const secondCell = firstRow.cells[1];
469 secondCell.setAttribute("tabindex", "0");
470 if (typeof dlgFilename === 'undefined') secondCell.focus();
471 // sort the files by modified time
475 }).catch(function (error) {
476 console.error(error);
479 dlgButton.addEventListener("click", function () {
480 const selFiles = Array.from(document.getElementsByClassName('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.");
485 selFile = (typeof dlgFilename === 'undefined') ? selFiles[0].firstChild.innerText : dlgFilename.value;
486 // Save the name to use later
487 sessionStorage.setItem("fileName", selFile);
489 funcCall(pathName + "/" + selFile, param);
491 sessionStorage.removeItem("depthDir");
492 document.removeEventListener("keydown", tableClickHandler);
498 // allow resizing table columns
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
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";
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;
527 const dlgTitle = document.createElement("div");
528 dlgTitle.className = "dlgTitlebar";
529 dlgTitle.id = "dlgMessageTitle";
530 dlgTitle.innerText = "Select file dialog";
531 d.appendChild(dlgTitle);
533 const dlgPanel = document.createElement("div");
534 dlgPanel.className = "dlgPanel";
535 dlgPanel.id = "dlgPanel";
536 d.appendChild(dlgPanel);
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);
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);'>
549 <img id='nameArrow' style='float:right;visibility:hidden;' src='icons/chevron-up.svg'>
551 <span id='timeSort' class='colTitle' onclick='check_sorting(this);'>
553 <img id='timeArrow' style='float:right;' src='icons/chevron-down.svg'>
555 <span id='sizeSort' class='colTitle' onclick='check_sorting(this);'>
557 <img id='sizeArrow' style='float:right;' src='icons/chevron-down.svg'>
559 filesTable.appendChild(colTitles);
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);
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);
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 () {
593 btnContainer.appendChild(dlgCancelBtn);
595 document.body.appendChild(d);
598 if (x !== undefined && y !== undefined)
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";
605 resizeObs.observe(d);
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
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";
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;
633 const dlgTitle = document.createElement("div");
634 dlgTitle.className = "dlgTitlebar";
635 dlgTitle.id = "dlgMessageTitle";
636 dlgTitle.innerText = "Select file dialog";
637 d.appendChild(dlgTitle);
639 const dlgPanel = document.createElement("div");
640 dlgPanel.className = "dlgPanel";
641 dlgPanel.id = "dlgPanel";
642 dlgPanel.onclick = unselect_files;
643 d.appendChild(dlgPanel);
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>
650 dlgFolder.style.textAlign = "left";
651 dlgPanel.appendChild(dlgFolder);
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();
663 const colTitles = fileSelect.createTHead();
664 colTitles.innerHTML = `
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'>
669 <th id='timeSort' style='width: 40%;' onclick='check_sorting(this);'>Modified
670 <img id='timeArrow' style='float:right;' src='icons/chevron-down.svg'>
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'>
676 fileSelect.appendChild(document.createElement("tbody"));
677 filesTable.appendChild(fileSelect);
678 dlgPanel.appendChild(filesTable);
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);
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);
705 btnContainer.appendChild(dlgCancelBtn);
707 document.body.appendChild(d);
710 if (x !== undefined && y !== undefined)
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";
717 resizeObs.observe(d);
719 // Add key navigation
720 document.addEventListener("keydown", tableClickHandler);
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
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);
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');
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")
768 const fileSelect = document.getElementById("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") {
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);
793 clearTimeout(sequenceTimer);
794 sequenceOfLetters = !sequenceOfLetters ? key : sequenceOfLetters + key;
795 sequenceTimer = setTimeout(() => {
796 sequenceOfLetters = '';
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
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
816 selectedRowIndex = rowIndex;
821 // Apply new highlights based on sequenceOfLetters
822 if (sequenceOfLetters) {
823 applyHighlights(rows[selectedRowIndex].childNodes[0], sequenceOfLetters);
827 // de-select all files
828 const selFiles = document.querySelectorAll('.selFile');
829 selFiles.forEach(selFile => {
830 selFile.classList.remove('selFile');
832 rows[selectedRowIndex].scrollIntoView({behavior: 'smooth',block: 'center'});
833 rows[selectedRowIndex].classList.add('selFile');
837// highlight characters in file selection table
838function applyHighlights(node, letters) {
839 const text = node.textContent;
840 let highlighted = '';
841 let currentIndex = 0;
843 for (let i = 0; i < text.length; i++) {
844 const originalChar = text[i];
845 const lowerCaseChar = originalChar.toLowerCase();
847 if (lowerCaseChar === letters[currentIndex]) {
848 highlighted += `<span class="hlkeys">${originalChar}</span>`;
851 highlighted += originalChar;
854 if (currentIndex === letters.length) {
855 highlighted += text.substring(i + 1);
860 node.innerHTML = highlighted;
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);
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
883 fList.push(...subdirs.map((item) => `[${item}]`));
887 fList.push(...files);
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();
899 if (xhr.status === 200) callback(xhr.responseText);
901 xhr.open('GET', `${file}?${Date.now()}`, true);
905// load an ascii file from server using mjsonrpc call
906function file_load_ascii(filename,callback){
908 filename - the name of the file to save
909 callback - the call back function recieving the contect
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.");
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)
924 // Check if file exists before even trying
925 mjsonrpc_send_request(mjsonrpc_make_request("ext_list_files", {
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);
938 callback(rpc2.result.content);
939 return rpc2.result.content;
941 }).catch(function (error) {
942 console.error(error);
945 }).catch(function (error) {
946 console.error(error);
950// Save file to server
951function file_save_ascii(filename, text, alert) {
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
958 // Possible checks here for illegal names
959 if (filename === "") {
960 dlgAlert("Illegal empty file name! Please provide a file name.");
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)
968 // ToDo: check if the mimetype is in ODB before accepting an extension
970 // Check if file already exists
971 mjsonrpc_send_request(mjsonrpc_make_request("ext_list_files", {
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,"");
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) => {
986 file_save_ascii_overwrite(filename, text, alert);
990 file_save_ascii_overwrite(filename, text, alert);
991 }).catch(function (error) {
992 console.error(error);
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);
1004 // show dialog box if alert is a string, otherwise call if it's a funciton
1005 if (typeof alert === "string" && alert !== "")
1007 else if (typeof alert === "function")
1011 }).catch(function (error) {
1012 console.error(error);
1016function sort_files_select(sort) {
1017 const select = document.getElementById("fileSelect");
1018 let fsType = select.nodeName;
1020 if (fsType == "SELECT") {
1021 options = Array.from(select.options);
1023 options = Array.from(select.rows);
1024 // Keep first row (labels).
1025 select.innerHTML = "";
1026 select.appendChild(options[0]);
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
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];
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);
1059 // Sort increasing modification time (default)
1060 return (bModtime - aModtime);
1064 options.forEach((option) => {
1065 if (fsType == "SELECT") {
1068 select.appendChild(option);
1073function check_sorting(e) {
1075 // Get the bounding rectangle of the <th> element and exclude clicks near the resize
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) {
1088 const nameArrow = document.getElementById("nameArrow");
1089 const timeArrow = document.getElementById("timeArrow");
1090 const sizeArrow = document.getElementById("sizeArrow");
1094 e = timeArrow.parentElement;
1097 if (e.id === "nameSort") {
1099 nameArrow.style.visibility = "visible";
1100 timeArrow.style.visibility = "hidden";
1101 sizeArrow.style.visibility = "hidden";
1102 } else if (e.id === "sizeSort") {
1104 nameArrow.style.visibility = "hidden";
1105 timeArrow.style.visibility = "hidden";
1106 sizeArrow.style.visibility = "visible";
1109 nameArrow.style.visibility = "hidden";
1110 timeArrow.style.visibility = "visible";
1111 sizeArrow.style.visibility = "hidden";
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"}`;
1118 sort_files_select(sort);
1121function resize_th() {
1122 var pressed = false;
1123 var start = undefined;
1124 var startX, startWidth;
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) {
1132 var rect = this.getBoundingClientRect();
1133 var edgeWidth = 5; // Adjust this value to change the clickable edge width
1135 if (e.pageX >= rect.right - edgeWidth) {
1139 startWidth = this.offsetWidth;
1140 start.classList.add("resizing");
1141 start.classList.add("noSelect");
1146 th.addEventListener("click", function(e) {
1148 e.stopPropagation();
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
1160 if (e.pageX >= rect.right - edgeWidth) {
1161 th.style.cursor = "ew-resize";
1163 th.style.cursor = "default";
1169 document.addEventListener("mousemove", function(e) {
1171 start.style.width = startWidth + (e.pageX - startX) + "px";
1175 document.addEventListener("mouseup", function() {
1177 start.classList.remove("resizing");
1178 start.classList.remove("noSelect");
1186// load a binary file from server using mjsonrpc call
1187function file_load_bin(filename,callback){
1189 filename - the name of the file to save
1190 callback - the call back function receiving the content
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.");
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)
1205 // Check if file exists before even trying
1206 mjsonrpc_send_request(mjsonrpc_make_request("ext_list_files", {
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)
1220 throw new Error("Cannot read file or file is empty.");
1222 }).catch(function (error) {
1223 console.error(error);
1226 }).catch(function (error) {
1227 console.error(error);
1235 * js-indent-level: 3
1236 * indent-tabs-mode: nil