3 Sequencer syntax highlighting and operation functions
5 Created by Zaher Salman on September 16th, 2023
10 Some of these functions, although specific to the midas sequencer,
11 can be used for general syntax highlighting and editing.
13 All files created and read from subdirectories within
14 experiment_directory/userfiles/sequencer. The path defined in ODB
15 /Sequencer/State/Path is added to this folder.
17 Generate highlighted script text:
18 syntax_msl(seqLines,keywordGroups)
19 - seqLines - is the text of the script (or array of lines).
20 - keywordGroups - (optional) object defining the command groups for
21 highlighting. If not provided, a msl default will
24 Highlight the current line:
26 - lineNum - the current line number.
27 - color - (optional) the bg color to use
30 - start -> load, switch to main tab and confirm
31 - progress to embed in infocolumn
34// Using Solarized color scheme - https://ethanschoonover.com/solarized/
36// Default msl definitions, keywords, indentation and file name
40 keywords: [/(["'])(.*?)\1/g],
43 mslclass: "msl_string",
46 keywords: [/(\$[\w]+|^\s*[\w]+(?=\s*=))/gm],
49 mslclass: "msl_variable",
52 keywords: ["GOTO", "CALL", "SCRIPT", "SUBROUTINE", "ENDSUBROUTINE", "TRANSITION", "INCLUDE", "EXIT"],
53 color: "#268bd2", // blue
55 mslclass: "msl_control_flow",
58 keywords: ["ODBSET", "ODBGET", "ODBCREATE", "ODBDELETE", "ODBINC", "ODBLOAD", "ODBSAVE", "ODBSUBDIR", "PARAM", "SET", "CAT"],
59 color: "#2aa198", // cyan
61 mslclass: "msl_data_management",
64 keywords: ["RUNDESCRIPTION", "LIBRARY", "MESSAGE", "MSG"],
65 color: "#6c71c4", // violet
70 keywords: ["IF", "ELSE", "ENDIF", "WAIT"],
71 color: "#c577f6", // pink (not solarized)
76 keywords: ["BREAK", "LOOP", "ENDLOOP"],
77 color: "#d33682", // magenta
79 mslclass: "msl_loops",
82 keywords: ["UNIT8", "INT8", "UNIT16", "INT16", "UNIT32", "INT32", "BOOL", "FLOAT", "DOUBLE", "STRING"],
83 color: "#859900", // green
85 mslclass: "msl_data_types",
88 keywords: ["SECONDS", "EVENTS", "ODBVALUE"],
89 color: "#cb4b16", // orange
91 mslclass: "msl_units",
94 keywords: ["start", "stop", "pause", "resume"],
95 color: "#dc322f", // red
97 mslclass: "msl_actions",
100 keywords: ["true","false"],
103 mslclass: "msl_bool",
106 keywords: [/\b(?<![0-9a-fA-F#])\d+(\.\d+)?([eE][-+]?\d+)?\b/g],
107 color: "#b58900", // yellow
109 mslclass: "msl_number",
112 keywords: ["#","COMMENT"],
113 color: "#839496", // base0
114 fontWeight: "italic",
115 mslclass: "msl_comment",
119 indentplus: ["IF", "LOOP", "ELSE", "SUBROUTINE"],
120 indentminos: ["ENDIF", "ENDLOOP", "ELSE", "ENDSUBROUTINE"],
128// Iindentation keywords
129const defIndent = mslDefs.defIndent;
130// Default values for MSL
131const fnExt = mslDefs.filename.ext;
132const fnPath = mslDefs.filename.path;
150 background-color: #f0f0f0;
157 box-sizing: border-box;
159 display: inline-block;
161 font-family: monospace;
163 -moz-user-select: text;
164 pointer-events: none;
177 background-color:white;
180 box-sizing: border-box;
181 display: inline-block;
183 border-top: 1px solid gray;
184 font-family: monospace;
186 -moz-user-select: text;
190 display: inline-block;
193 display: inline-block;
197 background-color: #FFFF00;
201 background-color: #FF0000;
205 background-color: #FF8800;
209 background-color: #11FF11;
213 background-color: #FFFF00;
229 border: 1px solid black;
234 line-height: 30px; /* since modbvalue forces resize */
239.infotable tr:first-child th:first-child,
240.infotable tr:first-child td:first-child {
241 border-top-left-radius: 5px;
243.infotable tr:first-child th:last-child,
244.infotable tr:first-child td:last-child {
245 border-top-right-radius: 5px;
247.infotable tr:last-child td:first-child {
248 border-bottom-left-radius: 5px;
250.infotable tr:last-child td:last-child {
251 border-bottom-right-radius: 5px;
258 display: inline-block;
262 width: calc(100% - 2px);
267/* Dropdown button styles*/
269 background-color: Transparent;
271 font-family: verdana,tahoma,sans-serif;
280 background-color: #C0D0D0;
283/* Style the dropdown content (hidden by default) */
286 display: inline-block;
291 background-color: #f9f9f9;
293 box-shadow: 0 8px 16px rgba(0,0,0,0.2);
304.dropdown-content div{
306 justify-content: space-between;
310.dropdown-content a:hover {
311 background-color: #C0D0D0;
313.dropdown-content div:hover {
314 background-color: #C0D0D0;
316.dropdown:hover .dropdown-content {
329 -webkit-overflow-scrolling: touch;
331 /* background-color: #475057;*/
334 background-color: #D0D0D0;
336 margin: 4px 2px 0px 2px;
337 border-top-left-radius: 10px;
338 border-top-right-radius: 10px;
340 border-top: 1px solid Transparent;
341 border-right: 1px solid Transparent;
342 border-left: 1px solid Transparent;
344 padding: 3px 5px 3px 5px;
347 background-color: #FFFFFF;
348 border-bottom: 5px solid #FFFFFF;
351.etab button.edt_active:hover {
353 background-color: #FFFFFF;
354 border-bottom: 5px solid #FFFFFF;
356.etab button.edt_active {
357 background-color: white;/*Transparent;*/
358 border-top: 1px solid gray;
359 border-right: 1px solid gray;
360 border-left: 1px solid gray;
361 border-bottom: 5px solid white;
366 /*padding: 6px 12px;*/
368 border: 1px solid #ccc;
375 background-color: #f0f0f0;
377 font-family: verdana,tahoma,sans-serif;
380 border: 1px solid black;
383 padding: 0px 2px 0px 2px;
387 background-color: #C0D0D0;
390 font-family: verdana,tahoma,sans-serif;
398 background-color: #FD5E59;
406 border: 1px solid black;
407 box-shadow: 6px 6px 10px 4px rgba(0,0,0,0.2);
413 border: 2px dashed #6bb28c;
414 background-color: white;
417 background-color: gray;
422 text-overflow: ellipsis;
423 max-width: calc(10em - 30px);
425#nextFNContainer img {
428 vertical-align: center;
432// Implement colors and styles from KeywordsGroups in CSS
433for (const group in mslDefs.groups) {
434 const { mslclass, color, fontWeight } = mslDefs.groups[group];
437 seq_css += `.${mslclass} { color: ${color}; font-weight: ${fontWeight}; }\n`;
441const seqStyle = document.createElement('style');
442seqStyle.textContent = seq_css;
443document.head.appendChild(seqStyle);
445// line connector string
447// revisions array, maximum nRevisions
448const previousRevisions = {};
449const nRevisions = 20;
450var revisionIndex = {};
451var saveRevision = {};
452// Meta combo keydown flag
453var MetaCombo = false;
455// -- Sequencer specific functions --
456// Setup the correct sequencer state visually
457function seqState(funcCall) {
459 funcCall - (optional) a function to be called when the state is set (with the state text)
461 let stateText = "Stopped";
462 // Check sequence state
463 mjsonrpc_db_get_values(["/Sequencer/State/Running","/Sequencer/State/Paused","/Sequencer/State/Finished","/Sequencer/State/Debug"]).then(function(rpc) {
464 if (rpc.result.data[0] && !rpc.result.data[1] && !rpc.result.data[2]) {
465 stateText = "Running";
466 } else if (rpc.result.data[1] && rpc.result.data[0] && !rpc.result.data[2]) {
467 stateText = "Paused";
469 stateText = "Stopped";
475 }).catch (function (error) {
476 console.error(error);
481// Ask user to edit current sequence
482function askToEdit(flag,event) {
484 openETab(document.getElementById("etab1-btn"));
485 const [lineNumbers,editor,btnLabel,label] = editorElements();
486 // make editable and add event listeners
487 //editor.contentEditable = true;
488 addETab(document.getElementById("addETab"));
489 seqOpen(label.title.split("\n")[0]);
490 event.stopPropagation();
493 const message = "To edit the sequence it must be opened in an editor tab.<br>Would you like to proceed?";
494 dlgConfirm(message,function(resp) {
496 const label = editorElements()[3];
497 addETab(document.getElementById("addETab"));
498 seqOpen(label.title.split("\n")[0]);
503// Enable editing of sequence
504function editorEventListeners() {
505 let [lineNumbers,editor] = editorElements();
506 editor.contentEditable = true;
507 // Attached syntax highlight event editor
508 editor.addEventListener("keydown",checkSyntaxEventDown);
509 editor.addEventListener("keyup",checkSyntaxEventUp);
510 editor.addEventListener("paste", checkSyntaxEventPaste);
512 editor.addEventListener("input", function() {
515 document.addEventListener("selectionchange", function(event) {
516 if (event.target.activeElement === editor) markCurrLineNum();
518 // Synchronize the scroll position of lineNumbers with editor
519 editor.addEventListener("scroll", function() {
520 lineNumbers.scrollTop = editor.scrollTop;
524// apply changes of filename in the ODB (triggers reload)
525function seqChange(filename) {
527 filename - full file name with path to change
529 if (!filename) return;
530 const lastIndex = filename.lastIndexOf('/');
531 const path = filename.substring(0, lastIndex).replace(new RegExp('^' + fnPath),"").replace(/\/+/g, '/').replace(/^\/+|\/+$/g, '');
532 const file = filename.substring(lastIndex + 1);
533 // set path and filename, wait for completion and return
534 mjsonrpc_db_paste(["/Sequencer/State/Path","/Sequencer/State/Filename"],[path,file]).then(function (rpc1) {
535 sessionStorage.removeItem("depthDir");
536 if (rpc1.result.status[0] === 1 && rpc1.result.status[1] === 1) {
537 mjsonrpc_db_paste(["/Sequencer/Command/Load new file"],[true]).then(function (rpc2) {
538 if (rpc2.result.status[0] === 1) {
541 }).catch(function (error) {console.error(error);});
543 dlgAlert("Something went wrong, I could not set the filename!");
545 }).catch(function (error) {console.error(error);});
548// Save sequence text in filename.
549function seqSave(filename) {
551 filename (opt) - save to provided filename with path. If undefined save to original
552 filename and if empty trigger file_picker.
554 let [lineNumbers,editor,label] = editorElements();
555 let text = editor.innerText;
556 if (editor.id !== "editorTab1") {
557 if (filename === undefined) {
558 // take name from button title
559 filename = label.title;
560 if (filename.endsWith(".msl")) {
561 file_save_ascii_overwrite(filename,text);
566 } else if (filename === "") {
567 filename = label.title;
568 let file = filename.split("/").pop();
569 let path = filename.substring(0, filename.lastIndexOf("/") + 1).replace(/\/+/g, '/').replace(/^\/+|\/+$/g, '');
570 // If file/path are empty start with default value
572 path = sessionStorage.getItem("pathName") ? sessionStorage.getItem("pathName") : fnPath + "/";
574 file = sessionStorage.getItem("fileName") ? sessionStorage.getItem("fileName") : "filename.msl";
575 file_picker(path,fnExt,seqSave,true,{},true);
577 file_save_ascii_overwrite(filename,text);
578 let file = filename.split("/").pop();
579 let path = filename.substring(0, filename.lastIndexOf("/") + 1).replace(/\/+/g, '/').replace(/^\/+|\/+$/g, '');
580 label.title = filename;
581 label.innerText = file;
582 sessionStorage.setItem("fileName",file);
583 sessionStorage.setItem("pathName",path);
586 // Check if filename is currently in editorTab1 and reload
587 let currFilename = document.getElementById("etab1-btn").title;
588 if (filename == currFilename) {
589 modbset("/Sequencer/Command/Load new file",true);
596function seqOpen(filename) {
598 filename - file name to open (empty trigger file_picker)
600 // if a full filename is provided, open and return
601 if (filename && filename !== "") {
602 // Identify active tab
603 let [lineNumbers,editor,label] = editorElements();
604 // Check the option to open in new tab
605 if (document.getElementById("inNewTab").checked && editor.id !== "editorTab1") {
606 if (label.title !== "") { // if the tab is empty open new file there
607 addETab(document.getElementById("addETab"));
608 [lineNumbers,editor,label] = editorElements();
611 let file = filename.split("/").pop();
612 let path = filename.substring(0, filename.lastIndexOf("/") + 1).replace(/\/+/g, '/').replace(/^\/+|\/+$/g, '');
613 label.title = filename.replaceAll(/\/+/g, '/');
614 label.innerText = file;
615 sessionStorage.setItem("fileName",file);
616 sessionStorage.setItem("pathName",path);
617 if (editor.id === "editorTab1") {
620 file_load_ascii(filename, function(text) {
621 editor.innerHTML = syntax_msl(text).join(lc).slice(0,-1);
622 updateLineNumbers(lineNumbers,editor);
627 // empty or undefined file name
628 mjsonrpc_db_get_values(["/Sequencer/State/Path"]).then(function(rpc) {
629 let path = fnPath + "/" + rpc.result.data[0].replace(/\/+/g, '/').replace(/^\/+|\/+$/g, '');
630 sessionStorage.setItem("pathName",path);
631 file_picker(path,fnExt,seqOpen,false);
632 }).catch(function (error) {
633 mjsonrpc_error_alert(error);
638// Show/hide buttons according to sequencer state
639function updateBtns(state) {
641 state - the state of the sequencer
645 color: "var(--mgreen)",
648 color: "var(--mred)",
651 color: "var(--mred)",
654 color: "var(--myellow)",
657 const color = seqState[state].color;
658 const seqStateSpans = document.querySelectorAll('.seqstate');
659 seqStateSpans.forEach(e => {e.style.backgroundColor = color;});
661 const hideBtns = document.querySelectorAll('.seqbtn');
662 hideBtns.forEach(button => {
663 button.style.display = "none";
665 // then show only those belonging to the current state
666 const showBtns = document.querySelectorAll('.seqbtn.' + state);
667 showBtns.forEach(button => {
668 if (button.tagName === "IMG") {
669 button.style.display = "inline-block";
671 button.style.display = "flex";
674 // Hide progress modal when stopped
675 const hideProgress = document.getElementById("Progress");
676 if (state === "Stopped") hideProgress.style.display = "none";
679// Show sequencer messages if present
680function mslMessage(message) {
681 // Empty message, return
682 if (!message) return;
683 // Check message and message wait
684 mjsonrpc_db_get_values(["/Sequencer/State/Message","/Sequencer/State/Message Wait"]).then(function(rpc) {
685 const message = rpc.result.data[0];
686 const hold = rpc.result.data[1];
688 dlgMessage("Message", message, true, false,clrMessage);
692 }).catch (function (error) {
693 console.error(error);
697// Clear sequencer messages
698function clrMessage() {
699 mjsonrpc_db_paste(["Sequencer/State/Message"], [""]).then(function (rpc) {
701 }).catch(function (error) {
702 console.error(error);
706// Adjust size of sequencer editor according to browser window size
707function windowResize() {
708 const [lineNumbers,editor] = editorElements();
709 const seqTable = document.getElementById("seqTable");
710 editor.style.height = document.documentElement.clientHeight - editor.getBoundingClientRect().top - 20 + "px";
711 editor.style.maxWidth = document.documentElement.clientWidth - editor.getBoundingClientRect().left - 20 + "px";
712 seqTable.style.width = (window.innerWidth - seqTable.getBoundingClientRect().left - 10) + "px";
713 seqTable.style.maxWidth = (window.innerWidth - seqTable.getBoundingClientRect().left - 10) + "px";
714 seqTable.style.height = (window.innerHeight - seqTable.getBoundingClientRect().top - 10) + "px";
715 seqTable.style.maxHeight = (window.innerHeight - seqTable.getBoundingClientRect().top - 10) + "px";
718// Load the current sequence from ODB (only on main tab)
720 const editor = document.getElementById("editorTab1");
721 const btn = document.getElementById("etab1-btn");
722 mjsonrpc_db_get_values(["/Sequencer/Script/Lines","/Sequencer/State/Running","/Sequencer/State/SCurrent line number","/Sequencer/State/Filename","/Sequencer/State/SFilename","/Sequencer/State/Path"]).then(function(rpc) {
723 let seqLines = rpc.result.data[0];
724 let seqState = rpc.result.data[1];
725 let currLine = rpc.result.data[2];
726 let filename = rpc.result.data[3];
727 //if (rpc.result.data[4] === null) dlgAlert("You may be using an old sequencer.<br>Please recompile and/or restart it.");
728 let sfilename = rpc.result.data[4] ? rpc.result.data[4].split('userfiles/sequencer/')[1] : "";
729 filename = (fnPath + "/" + rpc.result.data[5] + "/" + filename).replace("//","/");
732 editor.innerHTML = syntax_msl(seqLines).join(lc);
733 updateLineNumbers(editor.previousElementSibling,editor);
734 if (seqState) hlLine(currLine);
736 let file = filename.split("/").pop();
737 let path = filename.substring(0, filename.lastIndexOf("/") + 1).replace(/\/+/g, '/').replace(/^\/+|\/+$/g, '');
738 sessionStorage.setItem("fileName",file);
739 sessionStorage.setItem("pathName",path);
740 // change button title to add sfilename if present
741 btn.title = (sfilename && sfilename !== filename) ? filename + "\n" + sfilename : filename;
742 }).catch (function (error) {
743 console.error(error);
747// Highlight (background color) and scroll to current line
748function hlLine(lineNum,color) {
750 lineNum - the line number to be highlighted
751 color - (optional) background color
753 const lineId = "sline" + lineNum;
754 const lineHTML = document.getElementById(lineId);
756 // Remove highlight from all lines with the class "msl_current_line"
757 const highlightedLines = document.querySelectorAll(".msl_current_line");
758 highlightedLines.forEach((line) => line.classList.remove("msl_current_line"));
761 lineHTML.classList.add("msl_current_line");
762 if (color) lineHTML.style.backgroundColor = color;
763 // Scroll to the highlighted line if the checkbox is checked
764 const scrollToCurrCheckbox = document.getElementById("scrollToCurr");
765 if (scrollToCurrCheckbox && scrollToCurrCheckbox.checked) {
766 lineHTML.scrollIntoView({ block: "center" });
771// Scroll to make line appear in the center of editor
772function scrollToCurr(scrToCur) {
773 if (scrToCur.checked) {
774 localStorage.setItem("scrollToCurr",true);
775 const currLine = document.querySelector(".msl_current_line");
777 currLine.scrollIntoView({ block: "center" });
780 localStorage.removeItem("scrollToCurr",true);
784// Open files in new tabs
785function toggleCheck(e) {
787 localStorage.setItem(e.id,true);
789 localStorage.removeItem(e.id);
793// shortcut event handling to overtake default behaviour
794function shortCutEvent(event) {
795 const parEditor = editorElements();
796 const notFirstTab = (parEditor[1].id !== "editorTab1");
798 // Check these only for editors
799 if (event.altKey && event.key === 's') {
800 event.preventDefault();
801 //save as with file_picker
803 event.preventDefault();
804 } else if ((event.ctrlKey || event.metaKey) && event.key === 's') {
805 event.preventDefault();
806 //save to the same filename
808 event.preventDefault();
809 } else if ((event.ctrlKey || event.metaKey) && event.key === 'z') {
810 event.preventDefault();
811 undoEdit(event.target);
812 event.preventDefault();
813 } else if ((event.ctrlKey || event.metaKey) && event.key === 'r') {
814 event.preventDefault();
815 redoEdit(event.target);
816 event.preventDefault();
820 // Check these only for first tab
821 if ((event.ctrlKey || event.metaKey) && event.key === 'e') {
822 // open new tab and load current sequence
823 event.preventDefault();
824 addETab(document.getElementById("addETab"));
825 seqOpen(parEditor[3].title.split("\n")[0]);
826 event.preventDefault();
829 // Check these for all tabs
830 if (event.altKey && event.key === 'n') {
832 event.preventDefault();
833 addETab(document.getElementById("addETab"));
834 event.preventDefault();
835 } else if (event.altKey && event.key === 'o') {
836 event.preventDefault();
838 event.preventDefault();
843// Trigger syntax highlighting on keyup events
844function checkSyntaxEventUp(event) {
845 if (event.ctrlKey || event.altKey || event.metaKey || MetaCombo) return;
846 if (event.keyCode >= 0x30 || event.key === ' '
847 || event.key === 'Backspace' || event.key === 'Delete'
848 || event.key === 'Enter'
850 const e = event.target;
851 let caretPos = getCurrentCursorPosition(e);
852 let currText = e.innerText;
853 // Unclear why sometimes two \n are added
854 if (currText === sessionStorage.getItem("keydown") + "\n\n") {
855 e.innerText = currText.slice(0,-1);
856 currText = currText.slice(0,-1);
858 // save current revision for undo
859 if (event.key === ' ' || event.key === 'Backspace' || event.key === 'Delete' || event.key === 'Enter') {
860 saveState(currText,e);
863 // Indent according to previous line
864 if (event.key === 'Enter') {
865 event.preventDefault();
866 e.innerHTML = syntax_msl(e.innerText).join(lc).slice(0,-1);
867 setCurrentCursorPosition(e, caretPos);
868 // get previous and current line elements (before and after enter)
869 let pline = whichLine(e,-1);
870 let cline = whichLine(e);
871 let plineText = (pline) ? pline.innerText : "";
872 let clineText = (cline) ? cline.innerText : "";
875 let indentString = "";
877 // indent line according to the previous line text
878 // if, loop, else, subroutine - increase indentation
879 const indentPlus = defIndent.indentplus.some(keyword => plineText.trim().toLowerCase().startsWith(keyword.toLowerCase()));
880 // else, endif, endloop, endsubroutine - decrease indentation
881 const indentMinos = defIndent.indentminos.some(keyword => plineText.trim().toLowerCase().startsWith(keyword.toLowerCase()));
882 /* (indentMinos/indentPlus)
883 true/false - pline indent -1, cline indent 0
884 fale/true - pline indent 0, cline indent +1
885 true/true - pline indent -1, cline indent +1
886 false/false- pline indent 0, cline indent 0
888 // Count number of white spaces at begenning of pline
889 preSpace = plineText.replace("\n","").search(/\S|$/);
890 pPreSpace = preSpace - indentMinos * 3;
891 if (pPreSpace < 0) pPreSpace = 0;
892 cPreSpace = pPreSpace + indentPlus * 3;
893 // Calculate and insert indentation
894 pIndentString = " ".repeat(pPreSpace);
895 cIndentString = " ".repeat(cPreSpace);
896 cline.innerText = cIndentString + clineText.trimStart();
897 caretPos += cline.innerText.length - clineText.length;
898 pline.innerText = pIndentString + plineText.trimStart();
899 caretPos += pline.innerText.length - plineText.length;
901 event.preventDefault();
903 e.innerHTML = syntax_msl(e.innerText).join(lc).slice(0,-1);
904 setCurrentCursorPosition(e, caretPos);
905 updateLineNumbers(e.previousElementSibling,e);
912// Trigger syntax highlighting on keydown events
913function checkSyntaxEventDown(event) {
914 sessionStorage.setItem("keydown",event.target.innerText);
915 // take care of Mac odd keyup behaviour
916 if (event.metaKey && (/^[a-z]$/.test(event.key) || event.shiftKey || event.altKey)) {
922 if (event.ctrlKey || event.altKey || event.metaKey) return;
923 // Only pass indentation related keys
924 if (event.key !== 'Tab' && event.key !== 'Escape') return;
925 event.preventDefault();
926 let e = event.target;
927 let caretPos = getCurrentCursorPosition(e);
928 let currText = e.innerText;
929 let lines = getLinesInSelection(e);
930 if (event.shiftKey && event.key === 'Tab') {
931 indent_msl(lines,-1);
932 } else if (event.key === 'Tab') {
933 indent_msl(lines,+1);
934 } else if (event.key === 'Escape') {
937 e.innerHTML = syntax_msl(e.innerText).join(lc).slice(0,-1);
938 let newText = e.innerText;
939 setCurrentCursorPosition(e, caretPos + newText.length - currText.length);
940 if (lines[0] !== lines[1]) selectLines(lines,e);
941 event.preventDefault();
945// Trigger syntax highlighting when you paste text
946function checkSyntaxEventPaste(event) {
947 // set time out to allow default to go first
949 let e = event.target;
950 // make sure you paste in the editor area
951 if (e.tagName !== 'PRE') e = e.parentElement;
952 let caretPos = getCurrentCursorPosition(e);
953 let currText = e.innerText;
954 // save current revision for undo
955 saveState(currText,e);
956 e.innerHTML = syntax_msl(currText).join(lc).slice(0,-1);
957 setCurrentCursorPosition(e, caretPos);
958 updateLineNumbers(e.previousElementSibling,e);
964// Find on which line is the current carret position in e
965// This assumes each line has an id="sline#" where # is the line number.
966function whichLine(e,offset = 0) {
967 // offset allows to pick previous line (after enter)
968 let pos = getCurrentCursorPosition(e);
970 let lineNum = e.innerText.substring(0,pos).split("\n").length + offset;
971 let sline = e.querySelector("#sline" + lineNum.toString());
975// Return an array with the first and last line numbers of the selected region
976/* This assumes that the lines are in a <pre> element and that
977 each line has an id="sline#" where # is the line number.
978 When the caret in in an empty line, the anchorNode is the <pre> element.
980function getLinesInSelection(e) {
981 const selection = window.getSelection();
982 if (selection.rangeCount === 0) return [0,0];
983 // is it a single line?
984 const singleLine = selection.isCollapsed;
986 const line = whichLine(e);
988 const startLine = parseInt(line.id.replace("sline",""));
989 return [startLine,startLine];
994 const anchorNode = selection.anchorNode;
995 const range = selection.getRangeAt(0);
996 let startNode,endNode;
997 if (anchorNode.tagName === 'PRE') {
998 let startOffset = range.startOffset;
999 let endOffset = range.endOffset;
1000 startNode = range.startContainer.childNodes[startOffset];
1001 endNode = range.startContainer.childNodes[endOffset-1];
1003 startNode = (range.startContainer && range.startContainer.parentElement.tagName !== 'PRE') ? range.startContainer : range.startContainer.nextSibling;
1004 if (startNode && startNode.tagName === 'PRE') startNode = startNode.firstChild;
1005 endNode = (range.endContainer && range.endContainer.parentElement.tagName !== 'PRE') ? range.endContainer : range.endContainer.previousSibling;
1006 if (endNode && endNode.tagName === 'PRE') endNode = endNode.lastChild;
1008 let startID = (startNode && startNode.id) ? startNode.id : "";
1009 let endID = (endNode && endNode.id) ? endNode.id : "";
1011 while (startNode && !startID.startsWith("sline") && startNode.tagName !== 'PRE') {
1012 startNode = (startNode.parentNode.tagName !== 'PRE') ? startNode.parentNode : startNode.nextSibling;
1013 startID = (startNode && startNode.id) ? startNode.id : "";
1016 while (endNode && !endID.startsWith("sline") && endNode.tagName !== 'PRE') {
1017 endNode = (endNode.parentNode.tagName !== 'PRE') ? endNode.parentNode : endNode.previousSibling;
1018 endID = (endNode && endNode.id) ? endNode.id : "";
1020 let startLine = (startNode && startNode.id) ? parseInt(startNode.id.replace("sline","")) : 0;
1021 let endLine = (endNode && endNode.id) ? parseInt(endNode.id.replace("sline","")) : 0;
1023 startLine = endLine = Math.min(startLine, endLine);
1025 return [startLine,endLine];
1028// get current caret position in chars within element parent
1029function getCurrentCursorPosition(parent) {
1030 let sel = window.getSelection();
1031 if (!sel.focusNode || !parent) return;
1032 const range = sel.getRangeAt(0);
1033 const prefix = range.cloneRange();
1034 prefix.selectNodeContents(parent);
1035 prefix.setEnd(range.endContainer, range.endOffset);
1036 return prefix.toString().length;
1039// set current caret position at pos within element parent
1040function setCurrentCursorPosition(parent,pos) {
1041 for (const node of parent.childNodes) {
1042 if (node.nodeType == Node.TEXT_NODE) {
1043 if (node.length >= pos) {
1044 const range = document.createRange();
1045 const sel = window.getSelection();
1046 range.setStart(node, pos);
1047 range.collapse(true);
1048 sel.removeAllRanges();
1049 sel.addRange(range);
1052 pos = pos - node.length;
1055 pos = setCurrentCursorPosition(node, pos);
1064// Update line numbers in lineNumbers div
1065function updateLineNumbers(lineNumbers,editor) {
1066 if (lineNumbers === undefined || editor === undefined)
1067 [lineNumbers,editor] = editorElements();
1068 // Clear existing line numbers
1069 lineNumbers.innerHTML = "";
1070 // Get the number of lines accurately
1071 let lineCount = editor.querySelectorAll('[id^="sline"]').length;
1072 let lineTextCount = editor.innerText.split("\n").length;
1073 lineCount = (lineTextCount - lineCount) < 2 ? lineTextCount : lineTextCount - 1;
1074 // Add line numbers to lineNumbers
1075 for (let i = 1; i <= lineCount; i++) {
1076 const lineNumber = document.createElement('span');
1077 lineNumber.id = "lNum" + i.toString();
1078 lineNumber.textContent = i;
1079 lineNumbers.appendChild(lineNumber);
1081 lineNumbers.scrollTop = editor.scrollTop;
1085// Utility function to escape special characters in a string for use in a regular expression
1086function escapeRegExp(s) {
1088 return s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
1091// Syntax highlight any text according to provided rules
1092function syntax_msl(seqLines,keywordGroups) {
1093 // If not provided use the default msl keywords and groups
1094 if (!keywordGroups) {
1096 keywordGroups = mslDefs.groups;
1099 let mslText = Array.isArray(seqLines) ? seqLines.join("\n") : seqLines;
1100 // Make some cleanup of illegal characters
1101 mslText = escapeSpecialCharacters(mslText);
1102 // Keep original sequence lines (as array)
1103 let seqLines_org = mslText.split(/\n/);
1104 // Make full text if you get an array of lines
1105 let nLines = (mslText.match(/\n/g) || []).length;
1106 // These can be done on the text in one go
1108 let reg = /(["'])(.*?)\1/g;
1109 mslText = mslText.replace(reg,'<span class="msl_string">$1$2$1</span>');
1112 //reg = /^(COMMENT|#.*?)(.*)$/gim;
1113 //mslText = mslText.replace(reg,'<span class="msl_comment">$&</span>');
1116 //reg = /(\$[\w]+|^\s*[\w]+(?=\s*=))/gm;
1117 reg = /(?:\$[\w]+|^\b\w+(?=\s*=))/gm; // starting with $ or something =
1118 mslText = mslText.replace(reg,'<span class="msl_variable">$&</span>');
1119 reg = new RegExp("(^(?:\\s*)\\b(PARAM|CAT|SET)\\s+)(\\w+)\\b", "gim"); // after PARAM, CAT and SET
1120 mslText = mslText.replace(reg,'$1<span class="msl_variable">$3</span>');
1122 // Data Management group excluding variables (must be after variables)
1123 reg = new RegExp("^(\\s*)\\b(" + keywordGroups.dataManagement.keywords.join("|") + ")\\b", "gim");
1124 mslText = mslText.replace(reg, "$1<span class='msl_data_management'>$2</span>");
1126 // Data Type group (must have comma before the keyword)
1127 reg = new RegExp("(?<=,\\s*)\\b(" + keywordGroups.dataTypes.keywords.join("|") + ")\\b", "gim");
1128 mslText = mslText.replace(reg, "<span class='msl_data_types'>$1</span>");
1130 // Loops group (must be at the begenning of the line)
1131 reg = new RegExp("^(\\s*)\\b(" + keywordGroups.loops.keywords.join("|") + ")\\b", "gim");
1132 mslText = mslText.replace(reg, "$1<span class='msl_loops'>$2</span>");
1134 // Control Flow group
1135 reg = new RegExp("^(\\s*)\\b(" + keywordGroups.controlFlow.keywords.join("|") + ")\\b", "gim");
1136 mslText = mslText.replace(reg, "$1<span class='msl_control_flow'>$2</span>");
1138 // Data Management group
1139 reg = new RegExp("^(\\s*)\\b(" + keywordGroups.dataManagement.keywords.join("|") + ")\\b", "gim");
1140 mslText = mslText.replace(reg, "$1<span class='msl_data_managemen'>$2</span>");
1142 // Information group
1143 reg = new RegExp("^(\\s*)\\b(" + keywordGroups.info.keywords.join("|") + ")\\b", "gim");
1144 mslText = mslText.replace(reg, "$1<span class='msl_info'>$2</span>");
1146 // Conditional group
1147 reg = new RegExp("^(\\s*)\\b(" + keywordGroups.cond.keywords.join("|") + ")\\b", "gim");
1148 mslText = mslText.replace(reg,"$1<span class='msl_cond'>$2</span>");
1151 reg = new RegExp("\\b(" + keywordGroups.units.keywords.join("|") + ")\\b", "gi");
1152 mslText = mslText.replace(reg, "<span class='msl_units'>$1</span>");
1155 reg = new RegExp("\\b(" + keywordGroups.actions.keywords.join("|") + ")\\b(\\s*)$", "gim");
1156 mslText = mslText.replace(reg, "<span class='msl_actions'>$1</span>$2");
1158 // Numbers/boolean group
1159 reg = /\b(\d+(\.\d+)?([eE][-+]?\d+)?)\b/g;
1160 mslText = mslText.replace(reg, '<span class="msl_number">$1</span>');
1161 reg = /\b(true|false)\b/gi;
1162 mslText = mslText.replace(reg, '<span class="msl_bool">$1</span>');
1164 // Break lines and handle one by one
1165 seqLines = mslText.split("\n");
1167 // This is important for Firefox
1168 let emptyClass = "";
1169 let isChrome = browserType();
1170 if (isChrome === 1) emptyClass = "esline";
1171 // Loop and restore comment lines and empty lines
1172 for (let j = 0; j < seqLines_org.length ; j++) {
1173 let line = seqLines_org[j];
1174 commentIndex = line.indexOf("#");
1175 if (line.trim().startsWith("#") || line.trim().toLowerCase().startsWith("comment")) {
1176 // Restore comment lines without highlighting
1177 seqLines[j] = `<span class='msl_comment'>${line}</span>`;
1178 } else if (commentIndex > 0) {
1179 // Restore comment section at end of line
1180 const comment = line.slice(commentIndex);
1181 seqLines[j] = seqLines[j].slice(0, seqLines[j].indexOf("#")) + `</span><span class='msl_comment'>${comment}</span>`;
1184 // empty class is needed for cursor movement in Firefox
1185 // for Chrome empty lines are skipped with arrow up??
1187 if ((j === seqLines_org.length - 1) && (isChrome === 1)) {
1188 seqLines[j] = "<span class=' ' id='sline" + (j+1).toString() + "'></span>";
1189 } else if (isChrome === 1) {
1190 seqLines[j] = "<span class='" + emptyClass + "' id='sline" + (j+1).toString() + "'></span>";
1192 seqLines[j] = "<span class='" + emptyClass + "' id='sline" + (j+1).toString() + "'></span>";
1195 seqLines[j] = "<span class='sline' id='sline" + (j+1).toString() + "'>" + seqLines[j] + "</span>";
1201// Adjust indentation of a selection of lines
1202function indent_msl(lines,addTab) {
1204 lines - an array of two elements, first and last line numbers
1205 addTab - (opt) +/-1 to add/subtract three spaces to selected lines
1207 let indentLevel = 0;
1208 let singleLine = false;
1209 let editor = editorElements()[1];
1210 // Avoid issues of begenning of single line
1211 if (lines[0] > lines[1] || lines[0] == lines[1]) {
1212 lines[0] = lines[0] > 0 ? lines[0] : 1;
1213 lines[1] = lines[0];
1216 for (let j = lines[0]; j <= lines[1] ; j++) {
1217 let lineId = "#sline" + j.toString();
1218 let prevLineId = "#sline" + (j-1).toString();
1219 let lineEl = editor.querySelector(lineId);
1221 if (lineEl) line = lineEl.innerText;
1223 let indentString = " ".repeat(3);
1224 lineEl.innerText = indentString + line;
1225 } else if (addTab === -1) {
1226 lineEl.innerText = line.replace(/^\s{1,3}/, '');
1227 } else if (singleLine && editor.querySelector(prevLineId)) {
1228 let prevLineEl = editor.querySelector(prevLineId);
1229 let prevLine = prevLineEl.innerText;
1230 const indentMinos = defIndent.indentminos.some(keyword => prevLine.trim().toLowerCase().startsWith(keyword.toLowerCase()));
1231 const indentPlus = defIndent.indentplus.some(keyword => prevLine.trim().toLowerCase().startsWith(keyword.toLowerCase()));
1234 } else if (indentPlus) {
1237 let preSpace = prevLine.search(/\S|$/) + (indentLevel * 3);
1238 if (preSpace < 0) preSpace = 0;
1239 let indentString = " ".repeat(preSpace);
1240 lineEl.innerText = indentString + line.trimStart();
1242 const indentMinos = defIndent.indentminos.some(keyword => line.trim().toLowerCase().startsWith(keyword.toLowerCase()));
1243 if (indentMinos && indentLevel > 0) indentLevel--;
1244 let indentString = " ".repeat(indentLevel * 3);
1245 if (line !== "" || indentString !== "") {
1246 lineEl.innerText = indentString + line.trimStart();
1248 const indentPlus = defIndent.indentplus.some(keyword => line.trim().toLowerCase().startsWith(keyword.toLowerCase()));
1249 if (indentPlus) indentLevel++;
1254// Prepare the parameters/variables (if present) from the ODB as an html table
1255// Also return default ODB Paths and their respective values
1256function varTable(id,odbTreeVar) {
1258 id - ID of div to fill with the table of variables
1259 odbTreeVar - values of /Sequencer/Variables
1262 let e = document.getElementById(id);
1264 dlgAlert("Container ID was not give.");
1267 let nVars = 1,nVarsOld = 0;
1268 let oldTable = document.getElementById("varTable");
1269 if (oldTable) nVarsOld = oldTable.rows.length;
1270 // If /Sequencer/Variables are empty return empty
1272 // Clear container row
1276 let html = "<table id='varTable' class='mtable infotable'>\n";
1277 html += "<tr><th style='width: 120px'>Variable </th><th style='width: 200px'>Current value </th></tr>\n";
1279 // Go over all variables in ODB and count them
1280 let varCount = Object.keys(odbTreeVar).length;
1281 for (let key in odbTreeVar) {
1282 const match = key.match(/([^/]+)\/name$/);
1285 const name = match[1];
1286 const value = odbTreeVar[name];
1287 let isBool = (typeof(value) === "boolean");
1289 html += `<tr><td>${name}</td><td><input type="checkbox" class="modbcheckbox" data-odb-path="/Sequencer/Variables/${name}"></span></td></tr>\n`;
1291 html += `<tr><td>${name}</td><td><span class="modbvalue" data-odb-path="/Sequencer/Variables/${name}"></span></td></tr>\n`;
1296 if (nVars !== nVarsOld) {
1302// Prepare the parameters/variables (if present) from the ODB as an html table
1303// Also return default ODB Paths and their respective values
1304function parTable(odbTreeV,odbTreeC,odbTreeD,odbTreeO,odbTreeVar,editOnSeq) {
1306 odbTree... - Objects of ODB values
1309 let odbDefPaths = [];
1310 let odbDefValues = [];
1313 html += "<table id='paramTable' class='mtable infotable'>";
1314 html += "<tr><th>Parameter </th><th>Initial value </th><th>Comment</th></tr>";
1316 const processParam = (name, value, isBool, defValue, optValue, comment) => {
1317 let parLine = `<tr><td>${name}</td>`;
1320 // if not given the default is the first option
1321 if (defValue === undefined || defValue === "") defValue = optValue[0];
1322 const optionsHtml = optValue.map(option => `<option value="${option}" ${option === defValue ? 'selected' : ''}>${option}</option>`).join('');
1323 inParLine += `<select class="modbselect" data-odb-path="/Sequencer/Param/Value/${name}" data-odb-editable="1">${optionsHtml}</select>`;
1324 } else if (isBool) {
1325 inParLine += `<input type="checkbox" class="modbcheckbox" data-odb-path="/Sequencer/Param/Value/${name}" data-odb-editable="1"></input>`;
1327 inParLine += `<span class="modbvalue" data-odb-path="/Sequencer/Param/Value/${name}" data-odb-editable="1" data-input="1"></span>`;
1329 if (defValue !== undefined) {
1331 odbDefPaths.push(`/Sequencer/Param/Value/${name}`);
1332 odbDefValues.push(defValue);
1335 parLine += `<td>${inParLine}</td>`;
1336 parLine += `<td>${comment}</td></tr>`;
1340 // Go over all parameters in ODB
1341 for (let key in odbTreeV) {
1342 const match = key.match(/([^/]+)\/name$/);
1344 const name = match[1];
1345 // if variable is found use its value
1346 let value = (odbTreeVar && odbTreeVar[name]) ? odbTreeVar[name] : odbTreeV[name];
1347 let isBool = (typeof(value) === "boolean");
1348 let defValue = (value !== null && value !== undefined && value !== '') ? value : (odbTreeD && odbTreeD[name]) || value;
1349 let optValue = odbTreeO ? odbTreeO[name] : undefined;
1350 let comment = odbTreeC[name] || '';
1351 if (typeof value !== "object") {
1352 processParam(name, value, isBool, defValue, optValue, comment);
1357 // Go over Edit on sequence links
1358 for (let key in editOnSeq) {
1359 const match = key.match(/([^/]+)\/name$/);
1361 const name = match[1];
1362 const value = editOnSeq[name];
1363 let isBool = (typeof(value) === "boolean");
1365 html += `<tr><td>${name}</td><td><input type="checkbox" class="modbcheckbox" data-odb-path="/Experiment/Edit on sequence/${name}" data-odb-editable="1"></input></td><td></td></tr>\n`;
1367 html += `<tr><td>${name}</td><td><span class="modbvalue" data-odb-path="/Experiment/Edit on sequence/${name}" data-odb-editable="1" data-input="1"></span></td><td></td></tr>\n`;
1376 values: odbDefValues,
1382// Prepare the parameters/variables (if present) from the ODB as a table
1383function dlgParam(debugFlag) {
1385 debugFlag - (opt) true/false run in debug/normal mode
1388 let odbTree = JSON.parse(sessionStorage.getItem('parameters'));
1389 const editOnSeq = JSON.parse(sessionStorage.getItem('editonseq'));
1390 // If /Sequencer/Param are empty, start and return
1391 if (odbTree === null && editOnSeq === null ) {
1392 //if ((odbTree === null || Object.keys(odbTree).length) && (editOnSeq === null || Object.keys(editOnSeq).length)) {
1394 modbset('/Sequencer/Command/Debug script',true);
1396 modbset('/Sequencer/Command/Start script',true);
1401 let odbTreeV = null;
1402 let odbTreeC = null;
1403 let odbTreeD = null;
1404 let odbTreeO = null;
1405 let odbTreeVar = null;
1408 odbTreeV = odbTree.value;
1409 odbTreeC = odbTree.comment;
1410 odbTreeD = odbTree.defaults;
1411 odbTreeO = odbTree.options;
1412 odbTreeVar = JSON.parse(sessionStorage.getItem('variables'));
1415 // Go over all parameters in ODB
1416 let seqParTable = parTable(odbTreeV,odbTreeC,odbTreeD,odbTreeO,odbTreeVar,editOnSeq);
1417 let html = seqParTable.html;
1418 // set all default values and once finished produce dialog
1419 // Collect paths where values start with "/"
1420 let valuesLinkODB = seqParTable.values.map((valueLinkODB, indexLinkODB) => {
1421 if (valueLinkODB && valueLinkODB.startsWith("/")) {
1422 return seqParTable.values[indexLinkODB];
1426 }).filter(path => path !== null);
1427 mjsonrpc_db_get_values(valuesLinkODB).then(function (rpc) {
1428 if (rpc.result.status.every(status => status === 1)) {
1429 // substitute values
1430 rpc.result.data.forEach((newData, index) => {
1431 let pathIndex = seqParTable.values.indexOf(valuesLinkODB[index]);
1432 seqParTable.values[pathIndex] = `${newData}`; // Update corresponding value
1434 //console.log('Substituted seqParTable.values:', seqParTable.values);
1435 mjsonrpc_db_paste(seqParTable.paths,seqParTable.values).then(function (rpc) {
1436 if ((rpc.result.status.every(status => status === 1)) || seqParTable.values.length === 0) {
1437 //console.log("db_paste finished.\n",seqParTable.paths,seqParTable.values);
1438 // if parContainer not given produce a dialog
1439 let htmlDlg = `${html}<br><button class="dlgButtonDefault" id="dlgParamStart" type="button">Start</button><button class="dlgButton" id="dlgParamCancel" type="button">Cancel</button><br>`;
1440 let d = dlgGeneral({html: htmlDlg,iddiv: "Parameters",minWidth:500});
1441 let e = document.getElementById("parContainer");
1442 // Append the table to a container
1443 let startBtn = document.getElementById("dlgParamStart");
1444 let cancelBtn = document.getElementById("dlgParamCancel");
1445 cancelBtn.addEventListener("click", function () {d.remove();});
1446 startBtn.addEventListener("click", function () {
1449 modbset('/Sequencer/Command/Debug script',true);
1451 modbset('/Sequencer/Command/Start script',true);
1455 console.log("db_paste failed:",rpc.result.status," Trying again...");
1456 dlgParam(debugFlag);
1458 }).catch(function (error) {
1459 console.error(error);
1462 let message = `ODB "${valuesLinkODB}" was not found.<br>Cannot start sequence!`;
1465 }).catch(function (error) {
1466 console.error(error);
1471// helper debug function
1472function debugSeq(parContainer) {
1473 startSeq(parContainer,true);
1476// helper start function
1477function startSeq(parContainer,debugFlag) {
1478 const [lineNumbers,editor,label] = editorElements();
1479 if (!debugFlag) debugFlag = false;
1480 if (editor.id !== "editorTab1" && parContainer === undefined) {
1481 let filename = label.title;
1483 dlgAlert("Please give the file a name first (Save as).");
1486 const message = debugFlag ? `Save and debug ${filename}?` : `Save and start ${filename}?`;
1487 dlgConfirm(message,function(resp) {
1490 openETab(document.getElementById("etab1-btn"));
1492 // Make sure to load file an reset parameters
1493 mjsonrpc_db_paste(["/Sequencer/Command/Load new file"],[true]).then(function (rpc) {
1494 if (rpc.result.status[0] === 1) {
1495 dlgParam(debugFlag);
1501 // make sure to load file first
1502 mjsonrpc_db_paste(["/Sequencer/Command/Load new file"],[true]).then(function (rpc) {
1503 if (rpc.result.status[0] === 1) {
1504 dlgParam(debugFlag);
1510// Helper function to add the current file to next files queue
1511function setAsNext() {
1512 const [lineNumbers,editor,label] = editorElements();
1513 // Not allowed from console
1514 if (editor.id === "editorTab1") return;
1515 // This is the addAsNext button cell
1516 const e = document.getElementById("addAsNext");
1517 let filename = label.title.split("\n")[0].replace(/^sequencer\//,'');
1518 let message = `Save and put ${filename} in the next file queue?`;
1519 dlgConfirm(message,function(resp) {
1522 let order = chngNextFilename(e,filename);
1523 //dlgAlert(`File save and placed in position ${order} in the queue.`);
1528// helper stop function
1530 const message = `Are you sure you want to stop the sequence?`;
1531 dlgConfirm(message,function(resp) {
1533 modbset('/Sequencer/Command/Stop immediately',true);
1538// Show or hide parameters table
1539function showParTable(varContainer) {
1540 const e = document.getElementById(varContainer);
1541 const infoColumn = document.getElementById("infoColumn");
1542 const edtColumn = document.getElementById("edtColumn");
1543 let vis = document.getElementById("showParTable").checked;
1544 let visNF = (document.getElementById("nextFNContainer").style.display === "none");
1545 //let calcWidth = (window.innerWidth - e.parentElement.previousElementSibling.getBoundingClientRect().left - 28);
1547 //e.parentElement.previousElementSibling.style.width = `calc(${calcWidth}px - 19.5em)`;
1548 //e.parentElement.previousElementSibling.style.maxWidth = `calc(${calcWidth}px - 19.5em)`;
1549 infoColumn.style.display = "block";
1550 infoColumn.style.width = "19.5em";
1551 edtColumn.style.width = "100%";
1552 infoColumn.children[0].style.height = edtColumn.style.height;
1555 infoColumn.style.display = "none";
1556 edtColumn.style.width = "100%";
1557 //e.parentElement.style.width = "0";
1558 //e.parentElement.previousElementSibling.style.width = `calc(${calcWidth}px )`;
1559 //e.parentElement.previousElementSibling.style.maxWidth = `calc(${calcWidth}px )`;
1564// Show or hide next file list
1565function showFNTable(nextFNContainer) {
1566 let e = document.getElementById(nextFNContainer);
1567 let vis = document.getElementById("showNextFile").checked;
1568 let visVar = (document.getElementById("varContainer").style.display === "none");
1570 let addFileRow = "";
1572 mjsonrpc_db_get_values(["/Sequencer/State/Next Filename"]).then(function(rpc) {
1573 if (rpc.result.status[0] !== 1) return;
1574 let fList = rpc.result.data[0];
1575 for (let i = 0; i < fList.length; i++) {
1576 if (fList[i] && fList[i].trim() !== "") {
1577 html += `<tr class="dragrow" draggable="true"><td style="cursor: all-scroll;"><img draggable="false" style="cursor: all-scroll;" src="icons/menu.svg" title="Drag and drop to reorder"></td><td><img draggable="false" src="icons/folder-open.svg" title="Change file" onclick="chngNextFilename(this.parentElement);"></td><td style="cursor: all-scroll;" title="Drag and drop to reorder" ondblclick="chngNextFilename(this);">${fList[i]}</td><td><img draggable="false" title="Remove file from list" onclick="remNextFilename(this.parentElement);" src="icons/trash-2.svg"></td></tr>`;
1581 //let calcWidth = (window.innerWidth - e.parentElement.previousElementSibling.getBoundingClientRect().left - 28);
1582 if (vis && html !== "") {
1583 //e.parentElement.previousElementSibling.style.width = `calc(${calcWidth}px - 19.5em)`;
1584 //e.parentElement.previousElementSibling.style.maxWidth = `calc(${calcWidth}px - 19.5em)`;
1585 //e.style.display = "flex";
1586 //e.parentElement.style.width = "19.5em";
1588 addFileRow = `<tr><td id="addAsNext"><img src="icons/file-plus.svg" title="Add file" onclick="chngNextFilename(this.parentElement);"></td><td></td><td></td><td></td></tr>`;
1590 e.innerHTML = `<table class="mtable infotable" style="width:100%"><tr><th style="width:10px;"></th><th style="width:10px;"></th><th>Next files</th><th style="width:10px;"></th></tr>${html}${addFileRow}</table>`;
1592 e.style.display = "none";
1594 //e.parentElement.style.width = "0";
1595 //e.parentElement.previousElementSibling.style.width = `calc(${calcWidth}px )`;
1596 //e.parentElement.previousElementSibling.style.maxWidth = `calc(${calcWidth}px )`;
1599 activateDragDrop(e);
1600 }).catch (function (error) {
1601 console.error(error);
1605// Show error state of sequencer
1606function checkError(element) {
1607 let e = element.parentElement.parentElement;
1608 if (element.value === "") {
1609 e.style.display = "none";
1611 e.style.display = "table-row";
1615// Show extra rows for wait and loop
1616function extraRows(e) {
1618 e - triggering element to identify wait or loop
1620 // get current row, table and dialog
1621 let rIndex = e.parentElement.parentElement.rowIndex;
1622 let table = e.parentElement.parentElement.parentElement;
1623 let progressDlg = table.parentElement.parentElement.parentElement.parentElement.parentElement;
1624 // check if there is a wait or loop commands (if non-zero)
1627 if (e.id === "waitTrig") {
1628 // Make sure there is only one wait row
1629 const waitTRs = document.querySelectorAll('.waitTR');
1630 const waitFormula = (e.value === "Seconds") ? 'data-formula="x/1000"' : '';
1631 if (waitTRs.length) waitTRs.forEach(element => element.remove());
1633 let tr = table.insertRow(rIndex+1);
1634 tr.className = "waitTR";
1635 tr.innerHTML = `<td style="position: relative;width:80%;" colspan="2">
1636 <span class="modbhbar waitloopbar" style="position: absolute; top: 0; left: 0; color: lightgreen;" data-odb-path="/Sequencer/State/Wait value" ${waitFormula} id="mwaitProgress"></span>
1637 <span class="waitlooptxt">
1638 Wait: [<span class="modbvalue" data-odb-path="/Sequencer/State/Wait value" ${waitFormula}></span>/<span class="modbvalue" data-odb-path="/Sequencer/State/Wait limit" ${waitFormula} onchange="if (document.getElementById('mwaitProgress')) document.getElementById('mwaitProgress').dataset.maxValue = this.value;"></span>] <span class="modbvalue" data-odb-path="/Sequencer/State/Wait type"></span>
1642 } else if (e.id === "loopTrig") {
1643 mjsonrpc_db_get_values(["/Sequencer/State/Loop n"]).then(function(rpc) {
1644 let loopArray = rpc.result.data[0];
1645 for (let i = 0; i < loopArray.length; i++) {
1646 if (loopArray[i] === 0) break;
1647 let tr = table.insertRow(rIndex+1);
1648 tr.className = "loopTR";
1649 tr.innerHTML = `<td style="position: relative;width:80%;" colspan="2">
1650 <span class="modbhbar waitloopbar" style="position: absolute; top: 0; left: 0; color: #CBC3E3;" data-odb-path="/Sequencer/State/Loop counter[${i}]" id="mloopProgress${i}"></span>
1651 <span class="waitlooptxt">
1652 Loop ${i}: [<span class="modbvalue" data-odb-path="/Sequencer/State/Loop counter[${i}]"></span>/<span class="modbvalue" data-odb-path="/Sequencer/State/Loop n[${i}]" onchange="if (document.getElementById('mloopProgress${i}')) document.getElementById('mloopProgress${i}').dataset.maxValue = this.value;"></span>]
1657 }).catch (function (error) {
1658 console.error(error);
1663 if (e.id === "waitTrig") document.querySelectorAll('.waitTR').forEach(element => element.remove());
1664 if (e.id === "loopTrig") document.querySelectorAll('.loopTR').forEach(element => element.remove());
1665 // hide progress div
1666 //dlgHide(progressDlg);
1671// Helper function to identify browser, 1 FF, 2 Chrome, 3, other
1672function browserType() {
1673 if (navigator.userAgent.indexOf("Chrome") !== -1) {
1675 } else if (navigator.userAgent.indexOf("Firefox") !== -1) {
1682// make visual hint that file is changes
1683function seqIsChanged(flag) {
1684 // flag - true is change, false is saved
1685 let fileChangedId = "filechanged" + editorElements()[0].id.replace("lineNumbers","");
1686 let filechanged = document.getElementById(fileChangedId);
1689 filechanged.innerHTML = " ✎";
1690 } else if (flag === undefined) {
1691 // true if text has changed false if not
1692 return (filechanged.innerHTML !== "");
1694 filechanged.innerHTML = "";
1699// save history of edits in element editor
1700function saveState(mslText,editor) {
1701 editor = (editor) ? editor : editorElements()[1];
1702 const editorId = editor.id;
1703 if (saveRevision[editorId] === false) {
1705 } else if (saveRevision[editorId] === undefined){
1706 saveRevision[editorId] = true;
1709 if (!previousRevisions[editorId]) {
1710 previousRevisions[editorId] = [''];
1711 revisionIndex[editorId] = 0;
1714 // Add one more revision, and trim array if we had some undos
1715 revisionIndex[editorId]++;
1716 if (revisionIndex[editorId] < previousRevisions[editorId].length - 1) {
1717 previousRevisions[editorId].splice(revisionIndex[editorId] + 1);
1719 // Push new revision and keep only nRevisions revisions
1720 previousRevisions[editorId].push(mslText)
1721 if (previousRevisions[editorId].length > nRevisions) {
1722 previousRevisions[editorId].shift();
1724 revisionIndex[editorId] = previousRevisions[editorId].length - 1;
1728function undoEdit(editor) {
1729 editor = (editor) ? editor : editorElements()[1];
1730 const editorId = editor.id;
1731 if (revisionIndex[editorId] < 1) {
1732 // disable menu item
1733 disableMenuItems("undoMenu",true);
1737 disableMenuItems("undoMenu",false);
1738 revisionIndex[editorId]--;
1741 disableMenuItems("redoMenu",false);
1743 let caretPos = getCurrentCursorPosition(editor);
1744 let currText = editor.innerText;
1745 saveRevision[editorId] = false;
1746 editor.innerHTML = syntax_msl(previousRevisions[editorId][revisionIndex[editorId]]).join(lc).slice(0,-1);
1747 updateLineNumbers(editor.previousElementSibling,editor);
1748 saveRevision[editorId] = true;
1749 // calculate change in caret position based on length
1750 caretPos = caretPos + previousRevisions[editorId][revisionIndex[editorId]].length - currText.length;
1751 setCurrentCursorPosition(editor, caretPos);
1755function redoEdit(editor) {
1756 editor = (editor) ? editor : editorElements()[1];
1757 const editorId = editor.id;
1758 if (revisionIndex[editorId] >= previousRevisions[editorId].length - 1) {
1759 // disable menu item
1760 disableMenuItems("redoMenu",true);
1764 disableMenuItems("redoMenu",false);
1765 revisionIndex[editorId]++;
1768 disableMenuItems("undoMenu",false);
1770 let caretPos = getCurrentCursorPosition(editor);
1771 let currText = editor.innerText;
1772 saveRevision[editorId] = false;
1773 editor.innerHTML = syntax_msl(previousRevisions[editorId][revisionIndex[editorId]]).join(lc).slice(0,-1);
1774 updateLineNumbers(editor.previousElementSibling,editor);
1775 saveRevision[editorId] = true;
1776 // calculate change in caret position based on length
1777 caretPos = caretPos + previousRevisions[editorId][revisionIndex[editorId]].length - currText.length;
1778 setCurrentCursorPosition(editor, caretPos);
1781// Select slines from startLine to endLine
1782function selectLines([startLine, endLine],e) {
1783 const selection = window.getSelection();
1784 // Remove existing selections
1785 selection.removeAllRanges();
1786 let startElementId = '#sline' + startLine;
1787 let endElementId = '#sline' + endLine;
1788 let startElement = null, endElement = null;
1789 if (e.querySelector(startElementId)) startElement = e.querySelector(startElementId).firstChild;
1790 if (e.querySelector(endElementId)) endElement = e.querySelector(endElementId).lastChild;
1791 // we need startElement and endElement with first/lastChild
1792 // the following prevents loosing selection but not ideal
1793 while (startElement === null && startLine <= endLine) {
1795 startElementId = '#sline' + startLine;
1796 startElement = e.querySelector(startElementId).firstChild;
1798 while (endElement === null && endLine > 0) {
1800 endElementId = '#sline' + endLine;
1801 endElement = e.querySelector(endElementId).lastChild;
1803 if (startElement && endElement) {
1804 const range = document.createRange();
1805 // Set the start of the range to the startElement at offset 0
1806 range.setStart(startElement, 0);
1807 // Set the end of the range to the endElement at its length
1808 range.setEnd(endElement, endElement.childNodes.length);
1809 // Add the range to the selection
1810 selection.addRange(range);
1814// switch between dark and light modes on request
1815function lightToDark(lToDcheck) {
1816 const edt_areas = document.querySelectorAll('.edt_area');
1817 if (lToDcheck.checked) {
1818 localStorage.setItem("darkMode", true);
1819 edt_areas.forEach(area => {
1820 area.style.backgroundColor = "black";
1821 area.style.color = "white";
1823 updateCSSRule(".etab button:hover","background-color","black");
1824 updateCSSRule(".etab button:hover","color","white");
1825 updateCSSRule(".etab button:hover","border-bottom","5px solid black");
1826 updateCSSRule(".etab button.edt_active","background-color","black");
1827 updateCSSRule(".etab button.edt_active","color","white");
1828 updateCSSRule(".etab button.edt_active","border-bottom","5px solid black");
1829 updateCSSRule(".etab button.edt_active:hover","background-color","black");
1830 updateCSSRule(".etab button.edt_active:hover","border-bottom","5px solid black");
1832 localStorage.removeItem("darkMode");
1833 edt_areas.forEach(area => {
1834 area.style.backgroundColor = "white";
1835 area.style.color = "black";
1837 updateCSSRule(".etab button:hover","background-color","white");
1838 updateCSSRule(".etab button:hover","color","black");
1839 updateCSSRule(".etab button:hover","border-bottom","5px solid white");
1840 updateCSSRule(".etab button.edt_active","background-color","white");
1841 updateCSSRule(".etab button.edt_active","color","black");
1842 updateCSSRule(".etab button.edt_active","border-bottom","5px solid white");
1843 updateCSSRule(".etab button.edt_active:hover","background-color","white");
1844 updateCSSRule(".etab button.edt_active:hover","border-bottom","5px solid white");
1848function updateCSSRule(selector, property, value) {
1849 for (let i = 0; i < document.styleSheets.length; i++) {
1850 let styleSheet = document.styleSheets[i];
1851 let rules = styleSheet.cssRules || styleSheet.rules;
1852 if (!rules) continue;
1853 for (let j = 0; j < rules.length; j++) {
1854 let rule = rules[j];
1855 if (rule.selectorText === selector) {
1856 rule.style[property] = value;
1863// show/hide wait and loop progress
1864function showProgress(e) {
1865 //const progressDiv = document.getElementById("progressDiv");
1866 const progressDiv = document.getElementById("Progress");
1867 if (e === undefined) e = document.getElementById("showProgressBars");
1869 localStorage.setItem("showProgress",true);
1870 progressDiv.style.display = "block";
1871 //dlgShow(progressDiv);
1873 localStorage.removeItem("showProgress");
1874 progressDiv.style.display = "none";
1875 //dlgHide(progressDiv);
1879// Mark the current line number
1880function markCurrLineNum() {
1881 const [lineNumbers,editor] = editorElements();
1882 const currLines = lineNumbers.querySelectorAll(".edt_linenum_curr");
1883 const [startLine,endLine] = getLinesInSelection(editor);
1884 if (startLine === 0 && endLine === 0) return;
1885 currLines.forEach((line) => line.classList.remove("edt_linenum_curr"));
1886 for (let i = startLine; i <= endLine; i++) {
1887 let lineNumId = "#lNum" + i.toString();
1888 let lineNum = lineNumbers.querySelector(lineNumId);
1890 lineNum.className = "edt_linenum_curr";
1894// Check if sequencer program is in ODB and running.
1895// If not, try to get it going
1896function checkSequencer() {
1897 mjsonrpc_call('cm_exist', {"name":"Sequencer","unique":true}).then(function (rpc1) {
1898 mjsonrpc_db_get_values(["/Programs/Sequencer/Start command"]).then(function(rpc2) {
1899 let isRunning = (rpc1.result.status === 1);
1900 let isDefined = ((rpc2.result.data[0] !== null) && (rpc2.result.data[0] !== ""));
1901 if (isRunning && isDefined) return;
1902 // sequencer not running or not defined, stop it just in case and check the reason
1903 mjsonrpc_stop_program("Sequencer");
1906 message = "Sequencer program is not running.<br>Should I start it?"
1908 message = "Sequencer program is not configured and not running.<br>Should I try to start it anyway?"
1910 dlgConfirm(message,function(resp) {
1913 // assume that sequencer in path and create a start command, sleep 2s,
1914 // set value to "msequencer -D", sleep 2s, start program
1915 mjsonrpc_db_create([{"path" : "/Programs/Sequencer/Start command", "type" : TID_STRING}]).then(function (rpc3) {
1916 setTimeout(function(){
1917 mjsonrpc_db_paste(["/Programs/Sequencer/Start command"],["msequencer -D"]).then(function (rpc4) {
1918 if (rpc4.result.status[0] === 1) {
1919 mjsonrpc_start_program("Sequencer");
1921 }).catch(function (error) {
1922 console.error(error);
1925 }).catch(function (error) {
1926 console.error(error);
1929 mjsonrpc_start_program("Sequencer");
1931 // take 3 seconds and check that it actually started
1932 setTimeout(function(){
1933 mjsonrpc_call('cm_exist', {"name":"Sequencer","unique":true}).then(function (rpc5) {
1934 if (rpc5.result.status === 1) {
1935 dlgAlert("Sequencer started successfully.");
1937 dlgAlert("Failed to start Sequencer!<br>Try to start it manually (msequencer -D)");
1943 }).catch (function (error) {
1944 console.error(error);
1946 }).catch(function (error) {
1947 console.error(error);
1951function captureSelected() {
1952 if (window.getSelection) {
1953 let selection = window.getSelection();
1954 let text = selection.toString();
1955 let range = selection.getRangeAt(0);
1956 if (text && range) {
1958 startPath: range.startContainer,
1959 startOffset: range.startOffset,
1960 endPath: range.endContainer,
1961 endOffset: range.endOffset
1963 sessionStorage.setItem("tempSelText", text);
1964 sessionStorage.setItem("tempSelRange", JSON.stringify(rangeData));
1969function editMenu(action) {
1970 let text = sessionStorage.getItem("tempSelText") ?? "";
1971 let storedRange = sessionStorage.getItem("tempSelRange") ?? "";
1974 let rangeData = JSON.parse(storedRange);
1975 let startContainer = nodeFromPath(rangeData.startPath);
1976 let endContainer = nodeFromPath(rangeData.endPath);
1978 // Create a new range
1979 let newRange = new Range();
1980 newRange.setStart(startContainer, rangeData.startOffset);
1981 newRange.setEnd(endContainer, rangeData.endOffset);
1983 // Select the new range
1984 let selection = window.getSelection();
1985 selection.removeAllRanges();
1986 selection.addRange(newRange);
1989 if (action === "Copy") {
1991 sessionStorage.setItem("copiedText",text);
1993 } else if (action === "Paste") {
1994 const copiedText = sessionStorage.getItem("copiedText");
1996 newRange.deleteContents();
1997 newRange.insertNode(document.createTextNode(copiedText));
1999 } else if (action === "Cut") {
2001 sessionStorage.setItem("copyText",text);
2002 //document.execCommand("cut");
2003 newRange.deleteContents();
2005 } else if (action === "Undo") {
2007 } else if (action === "Redo") {
2012// Switch to the clicked tab
2013function openETab(btn) {
2014 const tabcontent = document.querySelectorAll(".etabcontent");
2015 const tablinks = document.querySelectorAll(".etablinks");
2016 const tab = btn ? btn : document.querySelectorAll(".edt_active")[0];
2017 const tabID = tab.id.replace("-btn","")
2018 tabcontent.forEach(content => {
2019 content.style.display = "none";
2021 tablinks.forEach(link => {
2022 link.classList.remove("edt_active");
2024 tab.className += " edt_active";
2025 document.getElementById(tabID).style.display = "flex";
2026 // For the main sequence tab disable Save and Save as...
2027 if (tabID === "etab1") {
2028 disableMenuItems("noteditor",true);
2030 disableMenuItems("noteditor",false);
2032 // Adjust height of active editor
2036// Close the clicked tab
2037function closeTab(tab,event) {
2038 const tablinks = document.querySelectorAll(".etablinks");
2039 if (tablinks.length < 3) return;
2040 const tabCount = parseInt(tab.parentElement.id.replace("-btn","").replace("etab",""));
2041 const tabBtn = document.getElementById(`etab${tabCount}-btn`);
2042 const tabContent = document.getElementById(`etab${tabCount}`);
2043 if (seqIsChanged()) {
2044 dlgConfirm("File was changed, close anyway?", function(resp) {
2047 tabContent.remove();
2048 // switch to previous tab
2049 openETab(document.getElementById(`etab${(tabCount-1)}-btn`));
2054 tabContent.remove();
2055 // switch to previous tab
2056 openETab(document.getElementById(`etab${(tabCount-1)}-btn`));
2058 // need this since the close button is inside the tab button
2059 event.stopPropagation();
2062// Create and add a new editor tab
2063function addETab(btn) {
2064 // Create tab button
2065 const tabBtn = document.createElement("button");
2066 tabBtn.className = "etablinks";
2067 const tabCount = (btn.previousElementSibling) ? parseInt(btn.previousElementSibling.id.replace("-btn","").replace("etab","")) + 1 : 1;
2068 tabBtn.id = "etab" + tabCount + "-btn";
2069 tabBtn.innerHTML = `<span id="etab${tabCount}-lbl">new file${tabCount}</span><span style="color: var(--mred);" id="filechanged${tabCount}"></span><span onclick="closeTab(this,event);" class="closebtn">×</span>`;
2070 btn.parentNode.insertBefore(tabBtn,btn);
2071 tabBtn.onclick = function () { openETab(this);};
2073 // Create editor area
2074 const tabContent = document.createElement("div");
2075 tabContent.id = "etab" + tabCount;
2076 tabContent.className = "etabcontent";
2078 if (localStorage.getItem("darkMode")) makeDark = "style='background-color: black; color: white;'";
2080 `<pre id="lineNumbers${tabCount}" class="edt_linenum"></pre><pre id="editorTab${tabCount}" ${makeDark} class="edt_area" spellcheck="false" contenteditable="false"></pre>`;
2081 tabContent.innerHTML = html;
2082 const lastETab = document.getElementById("lastETab");
2083 lastETab.parentNode.insertBefore(tabContent,lastETab);
2086 // Add event listeners
2087 editorEventListeners();
2090// Return the pre of lineNumbers, editor, tab label and tab button element of the active tab
2091function editorElements() {
2092 const btn = (document.querySelectorAll(".edt_active")[0]) ? document.querySelectorAll(".edt_active")[0] : document.getElementById("etab1-btn");
2093 const tab = document.getElementById(btn.id.replace("-btn",""));
2094 const [lineNumbers,editor] = tab.children;
2095 const btnLabel = (btn.id !== "etab1-btn") ? btn.children[0] : btn.children[1];
2096 return [lineNumbers,editor,btnLabel,btn];
2099// disable and enable clicking on menu item
2100function disableMenuItems(className,flag) {
2102 className - the class name of the item
2103 flag - true/false to enable/disable item
2105 const els = document.querySelectorAll(`.${className}`);
2108 e.style.opacity = 0.5;
2109 e.style.pointerEvents = "none";
2111 e.style.opacity = "";
2112 e.style.pointerEvents = "";
2117// Function to replace some special characters in the text
2118function escapeSpecialCharacters(text) {
2119 return text.replace(/&/g, "&")
2120 .replace(/</g, "<")
2121 .replace(/>/g, ">")
2122 .replace(/\r\n|\n\r/g, '\n')
2123 //.replace(/"/g, """)
2124 //.replace(/'/g, "'")
2125 .replace(/\t/g, " ");
2128// Produce a help window
2131<span style='text-align: left;'>
2132<b>Hidden features of the sequencer editor</b>
2133<ul style='white-space: pre;font-family: monospace;'>
2134<li>Double click on the edit area of the first (main) tab to edit the currently loaded sequence.</li>
2135<li>Tab - Indent selected lines.</li>
2136<li>Shift+Tab - Unindent selected lines.</li>
2137<li>Escape - Autoindent selected lines according to syntax rules.</li>
2138<li>Ctrl+C - Copy selected text.</li>
2139<li>Ctrl+V - Paste selected text.</li>
2140<li>Ctrl+A - Select all text.</li>
2141<li>Ctrl+Z - Undo last change.</li>
2142<li>Ctrl+R - Redo last undo.</li>
2146 const d = dlgMessage("Editor help",help, false, false);
2147 const btn = d.querySelectorAll('.dlgButton')[0];
2148 btn.className = "dlgButtonDefault";
2153// Activate drag and drop events on next files table
2154function activateDragDrop(table) {
2156 table - The table element containing the list of next files.
2158 // collect all rows with class dragrow
2159 const rows = table.querySelectorAll('.dragrow');
2160 let dragStartIndex,dragEndIndex;
2162 // add event listeners
2163 rows.forEach(row => {
2164 row.addEventListener('dragstart', dragStart);
2165 row.addEventListener('dragover', dragOver);
2166 row.addEventListener('dragend', dragEnd);
2169 function dragStart(e) {
2170 dragStartIndex = Array.from(rows).indexOf(this);
2171 rows.forEach(row => row.classList.remove('dragstart'));
2172 this.classList.add('dragstart');
2174 function dragOver(e) {
2176 dragEndIndex = Array.from(rows).indexOf(this);
2177 // Create or update the empty row element
2179 emptyRow = document.createElement('tr');
2180 emptyRow.innerHTML = "<td colspan=4> </td>";
2181 emptyRow.classList.add('empty-dragrow');
2183 // Insert the empty row element at the appropriate position
2184 if (dragEndIndex > dragStartIndex) {
2185 this.parentNode.insertBefore(emptyRow, this.nextSibling);
2186 } else if (dragEndIndex < dragStartIndex) {
2187 this.parentNode.insertBefore(emptyRow, this);
2190 function dragEnd(e) {
2195 reorderNextFilenames(dragStartIndex,dragEndIndex);
2196 rows.forEach(row => {
2197 row.classList.remove('dragstart');
2202// Move next filename from dragStarIndex to dragEndIndex position
2203function reorderNextFilenames(dragStarIndex,dragEndIndex) {
2205 dragStarIndex - move file from this indext.
2206 dragEndIndex - destination index of the file.
2208 if (dragStarIndex === dragEndIndex) return;
2210 let istart = dragStarIndex;
2211 let iend = dragEndIndex;
2212 let odbpath = "/Sequencer/State/Next Filename";
2213 if (istart > iend) {
2215 istart = dragEndIndex;
2216 iend = dragStarIndex;
2218 mjsonrpc_db_get_values([odbpath]).then(function(rpc) {
2219 if (rpc.result.status[0] !== 1) return;
2220 let fList = rpc.result.data[0];
2221 let draggedFile = fList.splice(dragStarIndex,1)[0];
2222 fList.splice(dragEndIndex,0,draggedFile);
2226 for (let i = istart; i <= iend; i=i+istep) {
2227 paths[j] = odbpath + "[" + i.toString() + "]";
2228 values[j] = fList[i];
2231 mjsonrpc_db_paste(paths,values).then(function (rpc2) {
2232 if (rpc2.result.status[0] !== 1) {
2233 dlgAlert("Failed to move the file!<br>Please check.");
2234 } else if (rpc2.result.status[0] === 1) {
2235 showFNTable('nextFNContainer');
2237 }).catch(function (error) {
2238 console.error(error);
2240 }).catch (function (error) {
2241 console.error(error);
2245// Change the next file name in the clicked row
2246function chngNextFilename(e,filename) {
2248 e - (optional) cell element on the same row of "next file" to be changed.
2249 If last row (or undefined) add a file to the end of the queue.
2250 filename - (optional) The file name to add to the end of the queue.
2252 if (e === undefined) e = document.getElementById("addAsNext");
2253 // file index from table row index
2254 let index = e ? e.parentElement.rowIndex - 1 : 0;
2255 // Only 10 files are allowed
2257 dlgAlert("Maximum number (10) of next files reached!");
2260 let odbpath = "/Sequencer/State/Next Filename";
2261 mjsonrpc_db_get_values(["/Sequencer/State/Path"]).then(function(rpc) {
2262 let path = fnPath + "/" + rpc.result.data[0].replace(/\/+/g, '/').replace(/^\/+|\/+$/g, '');
2263 sessionStorage.setItem("pathName",path);
2265 odbpath = odbpath + "[" + index.toString() + "]";
2266 modbset(odbpath,filename);
2268 file_picker(path,fnExt,function(filename) {
2269 filename = filename.replace(/^sequencer\//,'').replace(/^\//,'');
2270 odbpath = odbpath + "[" + index.toString() + "]";
2272 modbset(odbpath,filename);
2277 }).catch(function (error) {
2278 mjsonrpc_error_alert(error);
2282// Reomve the next file name from the queue
2283function remNextFilename(e) {
2285 e - cell element on the same row of "next file" to be removed
2287 // file index from table row index
2288 let index = e.parentElement.rowIndex - 1;
2289 let odbpath = "/Sequencer/State/Next Filename";
2290 mjsonrpc_db_get_values([odbpath]).then(function(rpc) {
2291 if (rpc.result.status[0] !== 1) return;
2292 let fList = rpc.result.data[0];
2293 // empty last element
2298 for (let i = index+1; i < fList.length; i++) {
2299 paths[j] = odbpath + "[" + (i-1).toString() + "]";
2300 values[j] = fList[i];
2303 mjsonrpc_db_paste(paths,values).then(function (rpc2) {
2304 if (rpc2.result.status[0] !== 1) {
2305 dlgAlert("Failed to remove file from list!<br>Please check.");
2306 } else if (rpc2.result.status[0] === 1) {
2307 showFNTable('nextFNContainer');
2309 }).catch(function (error) {
2310 console.error(error);
2312 }).catch (function (error) {
2313 console.error(error);