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:
25 hlLine(lineNum,color,editor,msgs)
26 - lineNums- the line number to be highlighted (or an array of numbers)
27 - color - (optional) background color
28 - editor - (optional) if provided, scroll to highlighted line in editor
29 - msgs - (optional) if provided, use as title/s for the highlighted line/s
32 - syntax validate ODBSET, ODBGET etc. have a valid ODB parameter
35// Using Solarized color scheme - https://ethanschoonover.com/solarized/
37// Default msl definitions, keywords, indentation and file name
41 keywords: [/(["'])(.*?)\1/g],
44 mslclass: "msl_string",
47 //keywords: [/(\$[\w]+|^\s*[\w]+(?=\s*=))/gm],
48 keywords: ["PARAM","CAT","SET"],
51 mslclass: "msl_variable",
54 keywords: ["GOTO", "CALL", "SCRIPT", "SUBROUTINE", "ENDSUBROUTINE", "TRANSITION", "INCLUDE", "EXIT"],
55 color: "#268bd2", // blue
57 mslclass: "msl_control_flow",
60 keywords: ["ODBSET", "ODBGET", "ODBCREATE", "ODBDELETE", "ODBINC", "ODBLOAD", "ODBSAVE", "PARAM", "SET", "CAT"],
61 color: "#2aa198", // cyan
63 mslclass: "msl_data_management",
66 keywords: ["RUNDESCRIPTION", "LIBRARY", "MESSAGE", "MSG"],
67 color: "#6c71c4", // violet
72 keywords: ["IF", "ELSE", "ENDIF", "WAIT"],
73 color: "#c577f6", // pink (not solarized)
78 keywords: ["BREAK", "LOOP", "ENDLOOP", "ODBSUBDIR", "ENDODBSUBDIR"],
79 color: "#d33682", // magenta
81 mslclass: "msl_loops",
84 keywords: ["UNIT8", "INT8", "UNIT16", "INT16", "UNIT32", "INT32", "BOOL", "FLOAT", "DOUBLE", "STRING"],
85 color: "#859900", // green
87 mslclass: "msl_data_types",
90 keywords: ["SECONDS", "EVENTS", "ODBVALUE"],
91 color: "#cb4b16", // orange
93 mslclass: "msl_units",
96 keywords: ["start", "stop", "pause", "resume"],
97 color: "#dc322f", // red
99 mslclass: "msl_actions",
102 keywords: ["true","false"],
105 mslclass: "msl_bool",
108 keywords: [/\b(?<![0-9a-fA-F#])\d+(\.\d+)?([eE][-+]?\d+)?\b/g],
109 color: "#b58900", // yellow
111 mslclass: "msl_number",
114 keywords: ["#","COMMENT"],
115 color: "#839496", // base0
116 fontWeight: "italic",
117 mslclass: "msl_comment",
121 indentplus: ["IF", "LOOP", "ELSE", "SUBROUTINE", "ODBSUBDIR"],
122 indentminos: ["ENDIF", "ENDLOOP", "ELSE", "ENDSUBROUTINE", "ENDODBSUBDIR"],
127 next : "/Sequencer/State/Next Filename",
147 background-color: #f0f0f0;
153 padding: 5px 5px 20px 5px;
154 box-sizing: border-box;
156 display: inline-block;
158 font-family: monospace;
160 -moz-user-select: text;
161 pointer-events: none;
171 width: calc(100% - 3em);
172 max-width: calc(100% - 3em);
174 /* resize: horizontal; */
175 background-color:white;
177 padding: 5px 5px 20px 5px;
178 box-sizing: border-box;
180 display: inline-block;
182 border-top: 1px solid gray;
183 font-family: monospace;
185 -moz-user-select: text;
189 display: inline-block;
192 display: inline-block;
196 background-color: #FFFF00;
199.msl_current_line:hover {
203 background-color: #FF0000;
207 background-color: #FF8800;
211 background-color: #11FF11;
215 background-color: #FFFF00;
220 /*resize: horizontal;*/
227 border: 1px solid black;
232 line-height: 30px; /* since modbvalue forces resize */
237.infotable tr:first-child th:first-child,
238.infotable tr:first-child td:first-child {
239 border-top-left-radius: 5px;
241.infotable tr:first-child th:last-child,
242.infotable tr:first-child td:last-child {
243 border-top-right-radius: 5px;
245.infotable tr:last-child td:first-child {
246 border-bottom-left-radius: 5px;
248.infotable tr:last-child td:last-child {
249 border-bottom-right-radius: 5px;
256 display: inline-block;
260 width: calc(100% - 2px);
265/* Dropdown button styles*/
267 background-color: Transparent;
269 font-family: verdana,tahoma,sans-serif;
278 background-color: #C0D0D0;
281/* Style the dropdown content (hidden by default) */
284 display: inline-block;
289 background-color: #f9f9f9;
291 box-shadow: 0 8px 16px rgba(0,0,0,0.2);
302.dropdown-content div{
304 justify-content: space-between;
308.dropdown-content a:hover {
309 background-color: #C0D0D0;
311.dropdown-content div:hover {
312 background-color: #C0D0D0;
314.dropdown:hover .dropdown-content {
327 -webkit-overflow-scrolling: touch;
331 background-color: #D0D0D0;
333 margin: 4px 2px 0px 2px;
334 border-top-left-radius: 10px;
335 border-top-right-radius: 10px;
337 border-top: 1px solid Transparent;
338 border-right: 1px solid Transparent;
339 border-left: 1px solid Transparent;
341 padding: 3px 5px 3px 5px;
344 background-color: #FFFFFF;
345 border-bottom: 5px solid #FFFFFF;
347.etab button.edt_active:hover {
348 background-color: #FFFFFF;
349 border-bottom: 5px solid #FFFFFF;
351.etab button.edt_active {
352 background-color: white;/*Transparent;*/
353 border-top: 1px solid gray;
354 border-right: 1px solid gray;
355 border-left: 1px solid gray;
356 border-bottom: 5px solid white;
366 background-color: #f0f0f0;
367 border: 1px solid black;
370 padding: 0px 2px 0px 2px;
374 background-color: #C0D0D0;
377 font-family: verdana,tahoma,sans-serif;
385 background-color: #FD5E59;
393 border: 1px solid black;
394 box-shadow: 6px 6px 10px 4px rgba(0,0,0,0.2);
400 border: 2px dashed #6bb28c;
401 background-color: white;
404 background-color: gray;
409 text-overflow: ellipsis;
410 max-width: calc(10em - 30px);
412#nextFNContainer img {
415 vertical-align: center;
419// Implement colors and styles from KeywordsGroups in CSS
420for (const group in mslDefs.groups) {
421 const { mslclass, color, fontWeight } = mslDefs.groups[group];
425 seq_css += `.${mslclass} { color: ${color}; font-weight: ${fontWeight}; }\n`;
427 seq_css += `.${mslclass} { color: ${color};}\n`;
432const seqStyle = document.createElement('style');
433seqStyle.textContent = seq_css;
434document.head.appendChild(seqStyle);
436// line connector string
438// revisions array, maximum nRevisions
439const previousRevisions = {};
440const nRevisions = 20;
441var revisionIndex = {};
442var saveRevision = {};
443// Meta combo keydown flag
444var MetaCombo = false;
445// Deal with Chrome issues
446var isChrome = browserType();
447// Make current state global
448var stateText = "Unknown";
450// -- Sequencer specific functions --
451// Setup the correct sequencer state visually
452function seqState(funcCall) {
454 funcCall - (optional) a function to be called when the state is set (with the state text)
456 stateText = "Stopped";
457 // Check sequence state
458 mjsonrpc_db_get_values(["/Sequencer/State/Running","/Sequencer/State/Paused","/Sequencer/State/Finished","/Sequencer/State/Debug","/Sequencer/State/Stop after run"]).then(function(rpc) {
459 if (rpc.result.data[0] && !rpc.result.data[1] && !rpc.result.data[2] && !rpc.result.data[4]) {
460 stateText = "Running";
461 } else if (rpc.result.data[1] && rpc.result.data[0] && !rpc.result.data[2] && !rpc.result.data[4]) {
462 stateText = "Paused";
463 } else if (rpc.result.data[0] && !rpc.result.data[1] && !rpc.result.data[2] && rpc.result.data[4]) {
464 // Check if stop after run is set and running
465 stateText = "Running.StopAfter"
466 } else if (rpc.result.data[0] && rpc.result.data[1] && !rpc.result.data[2] && rpc.result.data[4]) {
467 // Check if stop after run is set and paused
468 stateText = "Paused.StopAfter"
470 stateText = "Stopped";
477 }).catch (function (error) {
478 console.error(error);
483// Ask user to edit current sequence
484function askToEdit(flag,event) {
486 openETab(document.getElementById("etab1-btn"));
487 const [lineNumbers,editor,btnLabel,label] = editorElements();
488 // make editable and add event listeners
489 //editor.contentEditable = true;
490 addETab(document.getElementById("addETab"));
491 seqOpen(label.title.split("\n")[0]);
492 event.stopPropagation();
495 const message = "To edit the sequence it must be opened in an editor tab.<br>Would you like to proceed?";
496 dlgConfirm(message,function(resp) {
498 const label = editorElements()[3];
499 addETab(document.getElementById("addETab"));
500 seqOpen(label.title.split("\n")[0]);
505// Enable editing of sequence
506function editorEventListeners() {
507 let [lineNumbers,editor] = editorElements();
508 editor.contentEditable = true;
509 // Attached syntax highlight event editor
510 editor.addEventListener("keydown",checkSyntaxEventDown);
511 editor.addEventListener("keyup",checkSyntaxEventUp);
512 editor.addEventListener("paste", checkSyntaxEventPaste);
514 editor.addEventListener("input", function() {
517 document.addEventListener("selectionchange", function(event) {
518 if (event.target.activeElement === editor) markCurrLineNum();
520 // Synchronize the scroll position of lineNumbers with editor
521 editor.addEventListener("scroll", function() {
522 lineNumbers.scrollTop = editor.scrollTop;
524 if (isChrome === 1) {
525 editor.addEventListener("keydown", arrowKeysChrome);
529// apply changes of filename in the ODB (triggers reload)
530function seqChange(filename) {
532 filename - full file name with path to change
534 if (!filename) return;
535 const lastIndex = filename.lastIndexOf('/');
536 const path = filename.substring(0, lastIndex).replace(new RegExp('^' + mslDefs.filename.path),"").replace(/\/+/g, '/').replace(/^\/+|\/+$/g, '');
537 const file = filename.substring(lastIndex + 1);
538 // set path and filename, wait for completion and return
539 mjsonrpc_db_paste(["/Sequencer/State/Path","/Sequencer/State/Filename"],[path,file]).then(function (rpc1) {
540 sessionStorage.removeItem("depthDir");
541 if (rpc1.result.status[0] === 1 && rpc1.result.status[1] === 1) {
542 mjsonrpc_db_paste(["/Sequencer/Command/Load new file"],[true]).then(function (rpc2) {
543 if (rpc2.result.status[0] === 1) {
546 }).catch(function (error) {console.error(error);});
548 dlgAlert("Something went wrong, I could not set the filename!");
550 }).catch(function (error) {console.error(error);});
553// Save sequence text in filename.
554function seqSave(filename) {
556 filename (opt) - save to provided filename with path. If undefined save to original
557 filename and if empty trigger file_picker.
559 let [lineNumbers,editor,label] = editorElements();
560 let text = editor.innerText;
561 if (editor.id !== "editorTab1") {
562 if (filename === undefined) {
563 // take name from button title
564 filename = label.title;
565 if (filename.endsWith(".msl")) {
566 file_save_ascii_overwrite(filename,text);
571 } else if (filename === "") {
572 filename = label.title;
573 let file = filename.split("/").pop();
574 let path = filename.substring(0, filename.lastIndexOf("/") + 1).replace(/\/+/g, '/').replace(/^\/+|\/+$/g, '');
575 // If file/path are empty start with default value
577 path = sessionStorage.getItem("pathName") ? sessionStorage.getItem("pathName") : mslDefs.filename.path + "/";
579 file = sessionStorage.getItem("fileName") ? sessionStorage.getItem("fileName") : "filename.msl";
580 file_picker(path,mslDefs.filename.ext,seqSave,true,{},true);
582 file_save_ascii_overwrite(filename,text);
583 let file = filename.split("/").pop();
584 let path = filename.substring(0, filename.lastIndexOf("/") + 1).replace(/\/+/g, '/').replace(/^\/+|\/+$/g, '');
585 label.title = filename;
586 label.innerText = file;
587 sessionStorage.setItem("fileName",file);
588 sessionStorage.setItem("pathName",path);
591 // Check if filename is currently in editorTab1 and reload
592 let currFilename = document.getElementById("etab1-btn").title;
593 if (filename == currFilename) {
594 modbset("/Sequencer/Command/Load new file",true);
601function seqOpen(filename) {
603 filename - file name to open (empty trigger file_picker)
605 // if a full filename is provided, open and return
606 if (filename && filename !== "") {
607 // Identify active tab
608 let [lineNumbers,editor,label] = editorElements();
609 // Check the option to open in new tab, also open in new tab if sequence is running
610 if ((document.getElementById("inNewTab").checked && (label.title !== "" || editor.id !== "editorTab1")) ||
611 (editor.id === "editorTab1" && stateText === "Running")) {
612 addETab(document.getElementById("addETab"));
613 [lineNumbers,editor,label] = editorElements();
615 let file = filename.split("/").pop();
616 let path = filename.substring(0, filename.lastIndexOf("/") + 1).replace(/\/+/g, '/').replace(/^\/+|\/+$/g, '');
617 label.title = filename.replaceAll(/\/+/g, '/');
618 label.innerText = file;
619 sessionStorage.setItem("fileName",file);
620 sessionStorage.setItem("pathName",path);
621 if (editor.id === "editorTab1") {
624 file_load_ascii(filename, function(text) {
625 editor.innerHTML = syntax_msl(text).join(lc).slice(0,-1);
626 updateLineNumbers(lineNumbers,editor);
627 // Change state to not edited
630 saveState(text,editor);
635 // empty or undefined file name
636 mjsonrpc_db_get_values(["/Sequencer/State/Path"]).then(function(rpc) {
637 let path = mslDefs.filename.path + "/" + rpc.result.data[0].replace(/\/+/g, '/').replace(/^\/+|\/+$/g, '');
638 sessionStorage.setItem("pathName",path);
639 file_picker(path,mslDefs.filename.ext,seqOpen,false);
640 }).catch(function (error) {
641 mjsonrpc_error_alert(error);
646// Show/hide buttons according to sequencer state
647function updateBtns(state) {
649 state - the state of the sequencer
653 color: "var(--mgreen)",
656 color: "var(--mred)",
659 color: "var(--mred)",
662 color: "var(--myellow)",
665 const color = seqState[state.split(".")[0]].color;
666 const nclass = state.split(".").length;
667 const seqStateSpans = document.querySelectorAll('.seqstate');
668 seqStateSpans.forEach(e => {e.style.backgroundColor = color;});
670 const hideBtns = document.querySelectorAll('.seqbtn');
671 hideBtns.forEach(button => {
672 button.style.display = "none";
674 // then show only those belonging to the current state
675 const showBtns = document.querySelectorAll('.seqbtn.' + state);
676 showBtns.forEach(button => {
677 if (button.tagName === "IMG") {
678 button.style.display = "inline-block";
680 button.style.display = "flex";
683 // Hide exclusive buttons
685 const exclBtns = document.querySelectorAll('.seqbtn.Exclusive');
686 exclBtns.forEach(button => {
687 button.style.display = "none";
690 // Hide progress modal when stopped
691 const hideProgress = document.getElementById("Progress");
692 if (state === "Stopped" && hideProgress) hideProgress.style.display = "none";
695// Show sequencer messages if present
696function mslMessage(message) {
697 // Empty message, return
698 if (!message) return;
699 // Check message and message wait
700 mjsonrpc_db_get_values(["/Sequencer/State/Message","/Sequencer/State/Message Wait"]).then(function(rpc) {
701 const message = rpc.result.data[0];
702 const hold = rpc.result.data[1];
704 dlgMessage("Message", message, true, false,clrMessage);
708 }).catch (function (error) {
709 console.error(error);
713// Clear sequencer messages
714function clrMessage() {
715 mjsonrpc_db_paste(["Sequencer/State/Message"], [""]).then(function (rpc) {
717 }).catch(function (error) {
718 console.error(error);
722// Adjust size of sequencer editor according to browser window size
723function windowResize() {
724 const [lineNumbers, editor] = editorElements();
725 const seqTable = document.getElementById("seqTable");
726 const infoColumn = document.getElementById("infoColumn");
728 // Calculate scrollbar width
729 const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
731 // Visible width of the window accounting for scrollbar
732 let winWidth = Math.max(document.documentElement.clientWidth, window.innerWidth) - scrollbarWidth;
734 // Visible height of the window
735 let winHeight = Math.max(document.documentElement.clientHeight, window.innerHeight);
737 // Set seqTable height and width to fit the remaining space in the viewport
738 seqTable.style.width = winWidth - seqTable.getBoundingClientRect().left + "px";
739 seqTable.style.height = winHeight - seqTable.getBoundingClientRect().top - 20 + "px";
741 // Adjust editor dimensions
742 const editorTop = editor.getBoundingClientRect().top;
743 editor.style.height = winHeight - editorTop - 20 + "px";
744 editor.style.width = winWidth - editor.getBoundingClientRect().left - infoColumn.getBoundingClientRect().width - 10 + "px";
745 editor.style.maxWidth = editor.style.width;
747 // Adjust infoColumn and lineNumbers heights to match the editor height
748 infoColumn.style.height = editor.style.height;
749 lineNumbers.style.height = editor.style.height;
752// Load the current sequence from ODB (only on main tab)
754 const editor = document.getElementById("editorTab1");
755 const btn = document.getElementById("etab1-btn");
756 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) {
757 let seqLines = rpc.result.data[0];
758 let seqState = rpc.result.data[1];
759 let currLine = rpc.result.data[2];
760 let filename = rpc.result.data[3];
761 let sfilename = rpc.result.data[4] ? rpc.result.data[4].split('userfiles/sequencer/')[1] : "";
762 filename = (mslDefs.filename.path + "/" + rpc.result.data[5] + "/" + filename).replace("//","/");
765 editor.innerHTML = syntax_msl(seqLines).join(lc);
766 updateLineNumbers(editor.previousElementSibling,editor);
767 if (seqState) hlLine(currLine);
769 let file = filename.split("/").pop();
770 let path = filename.substring(0, filename.lastIndexOf("/") + 1).replace(/\/+/g, '/').replace(/^\/+|\/+$/g, '');
771 sessionStorage.setItem("fileName",file);
772 sessionStorage.setItem("pathName",path);
773 // change button title to add sfilename if present
774 btn.title = (sfilename && sfilename !== filename) ? filename + "\n" + sfilename : filename;
775 // also change validation icon to defaule
776 let vldBtn = document.getElementById("vldBtn");
777 vldBtn.src = "icons/validate-syntax.svg";
778 vldBtn.style.backgroundColor = "";
779 }).catch (function (error) {
780 console.error(error);
784// Highlight (background color) and scroll to current line
785function hlLine(lineNums,color,editor,msgs = []) {
787 lineNums- the line number to be highlighted (or an array of numbers)
788 color - (optional) background color
789 editor - (optional) if provided, scroll to highlighted line in editor
790 msgs - (optional) if provided, use as title/s for the highlighted line/s
793 // Remove highlight from all lines with the class "msl_current_line"
794 const highlightedLines = document.querySelectorAll(".msl_current_line");
795 highlightedLines.forEach((line) => line.classList.remove("msl_current_line"));
797 // If single value make an array
798 lineNums = Array.isArray(lineNums) ? lineNums : [lineNums];
799 msgs = Array.isArray(msgs) ? msgs : [msgs];
801 lineNums.forEach(lineNum => {
802 const lineId = "sline" + lineNum;
803 const lineHTML = (editor) ? editor.querySelector(`#${lineId}`) : document.getElementById(lineId);
806 lineHTML.classList.add("msl_current_line");
807 if (color) lineHTML.style.backgroundColor = color;
808 if (msgs[counter]) lineHTML.title = msgs[counter];
809 // Scroll to the highlighted line if the checkbox is checked
810 const scrollToCurrCheckbox = document.getElementById("scrollToCurr");
811 if (((scrollToCurrCheckbox && scrollToCurrCheckbox.checked) || editor) && (counter == 0)) {
812 lineHTML.scrollIntoView({ block: "center" });
819// Scroll to make line appear in the center of editor
820function scrollToCurr(scrToCur) {
821 if (scrToCur.checked) {
822 localStorage.setItem("scrollToCurr",true);
823 const currLine = document.querySelector(".msl_current_line");
825 currLine.scrollIntoView({ block: "center" });
828 localStorage.removeItem("scrollToCurr",true);
832// Open files in new tabs
833function toggleCheck(e) {
835 localStorage.setItem(e.id,true);
837 localStorage.removeItem(e.id);
841// shortcut event handling to overtake default behaviour
842function shortCutEvent(event) {
843 const parEditor = editorElements();
844 const notFirstTab = (parEditor[1].id !== "editorTab1");
846 // Check these only for editors
847 if (event.altKey && event.key === 's') {
848 event.preventDefault();
849 //save as with file_picker
851 event.preventDefault();
852 } else if ((event.ctrlKey || event.metaKey) && event.key === 's') {
853 event.preventDefault();
854 //save to the same filename
856 event.preventDefault();
857 } else if ((event.ctrlKey || event.metaKey) && event.key === 'z') {
858 event.preventDefault();
859 undoEdit(event.target);
860 event.preventDefault();
861 } else if ((event.ctrlKey || event.metaKey) && event.key === 'r') {
862 event.preventDefault();
863 redoEdit(event.target);
864 event.preventDefault();
868 // Check these only for first tab
869 if ((event.ctrlKey || event.metaKey) && event.key === 'e') {
870 // open new tab and load current sequence
871 event.preventDefault();
872 addETab(document.getElementById("addETab"));
873 seqOpen(parEditor[3].title.split("\n")[0]);
874 event.preventDefault();
877 // Check these for all tabs
878 if (event.altKey && event.key === 'n') {
880 event.preventDefault();
881 addETab(document.getElementById("addETab"));
882 event.preventDefault();
883 } else if (event.altKey && event.key === 'o') {
884 event.preventDefault();
886 event.preventDefault();
891// Trigger syntax highlighting on keyup events
892function checkSyntaxEventUp(event) {
893 if (event.ctrlKey || event.altKey || event.metaKey || MetaCombo) return;
894 if (event.keyCode >= 0x30 || event.key === ' '
895 || event.key === 'Backspace' || event.key === 'Delete'
896 || event.key === 'Enter'
899 const e = event.target;
900 let caretPos = getCurrentCursorPosition(e);
901 let currText = e.innerText;
902 // Indentation keywords
903 const defIndent = mslDefs.defIndent;
905 // save current revision for undo
906 if (event.key === ' ' || event.key === 'Backspace' || event.key === 'Delete' || event.key === 'Enter') {
907 saveState(currText,e);
911 // Indent according to previous line
912 if (event.key === 'Enter') {
913 event.preventDefault();
914 // get previous and current line elements (before and after enter)
915 let pline = whichLine(e,-1);
916 let cline = whichLine(e);
917 let plineText = (pline) ? pline.innerText : "";
918 let clineText = (cline) ? cline.innerText : "";
921 let indentString = "";
923 // indent line according to the previous line text
924 // if, loop, else, subroutine - increase indentation
925 const indentPlus = defIndent.indentplus.some(keyword => plineText.trim().toLowerCase().startsWith(keyword.toLowerCase()));
926 // else, endif, endloop, endsubroutine - decrease indentation
927 const indentMinos = defIndent.indentminos.some(keyword => plineText.trim().toLowerCase().startsWith(keyword.toLowerCase()));
928 /* (indentMinos/indentPlus)
929 true/false - pline indent -1, cline indent 0
930 fale/true - pline indent 0, cline indent +1
931 true/true - pline indent -1, cline indent +1
932 false/false- pline indent 0, cline indent 0
934 // Count number of white spaces at begenning of pline
935 preSpace = plineText.replace("\n","").search(/\S|$/);
936 pPreSpace = preSpace - indentMinos * 3;
937 if (pPreSpace < 0) pPreSpace = 0;
938 cPreSpace = pPreSpace + indentPlus * 3;
939 // Calculate and insert indentation
940 pIndentString = " ".repeat(pPreSpace);
941 cIndentString = " ".repeat(cPreSpace);
942 cline.innerText = cIndentString + clineText.trimStart();
943 caretPos += cline.innerText.length - clineText.length;
944 pline.innerText = pIndentString + plineText.trimStart();
945 caretPos += pline.innerText.length - plineText.length;
947 event.preventDefault();
949 e.innerHTML = syntax_msl(e.innerText).join(lc).slice(0,-1);
950 setCurrentCursorPosition(e, caretPos);
951 updateLineNumbers(e.previousElementSibling,e);
957// Handle arrow up/down keys in Chrome
958function arrowKeysChrome(event) {
959 // Skip combos with special keys
960 if (event.ctrlKey || event.metaKey || event.altKey || event.shiftKey) return;
961 if ((event.key === "ArrowUp" || event.key === "ArrowDown") && event.shiftKey ) {
962 event.preventDefault();
963 startChromeSelecting(event.target);
964 } else if ((event.key === "ArrowUp" || event.key === "ArrowDown") && !event.shiftKey ) {
965 event.preventDefault();
966 const e = event.target;
967 let caretPos = getCurrentCursorPosition(e);
968 let ncaretPos = caretPos;
969 let currText = e.innerText;
970 let lines = currText.split("\n");
971 // Determine the current line number (0-based index)
972 let clineNum = getLinesInSelection(e)[0] - 1;
973 // Determine the direction: -1 for up, +1 for down
974 let addLine = (event.key === "ArrowDown") ? 1 : -1;
975 // Determine the target line number
976 let nlineNum = clineNum + addLine;
977 if (lines[nlineNum] !== undefined) {
978 // line exists can move cursor
979 let clineStart = currText.split("\n").slice(0, clineNum).join("\n").length + (clineNum > 0 ? 1 : 0);
980 let caretInLine = caretPos - clineStart;
982 let nlength = lines[nlineNum].length;
983 let nlineStart = currText.split("\n").slice(0, nlineNum).join("\n").length + (nlineNum > 0 ? 1 : 0);
984 if (nlength >= caretInLine && caretInLine >= 0) {
985 // If the target line is longer or equal to the caret's position in the current line
986 ncaretPos = nlineStart + caretInLine;
987 } else if (caretInLine < 0) {
988 ncaretPos = nlineStart + nlength + 1;
990 // If the target line is shorter than the caret's position in the current line
991 ncaretPos = nlineStart + nlength;
993 setCurrentCursorPosition(e, ncaretPos);
999// Trigger syntax highlighting on keydown events
1000function checkSyntaxEventDown(event) {
1001 sessionStorage.setItem("keydown",event.target.innerText);
1002 // take care of Mac odd keyup behaviour
1003 if (event.metaKey && (/^[a-z]$/.test(event.key) || event.shiftKey || event.altKey)) {
1009 if (event.ctrlKey || event.altKey || event.metaKey) return;
1010 // Quickly return for anything but these keys
1011 if (event.key !== 'Tab' && event.key !== 'Escape' && event.key !== 'Backspace' && event.key !== 'Delete' && event.key !== 'Enter') return;
1012 //if (event.key !== 'Tab' && event.key !== 'Escape' && event.key !== 'Enter') return;
1013 const e = event.target;
1014 event.preventDefault();
1015 const textSelected = isTextSelected(e);
1016 if (textSelected && event.key !== 'Tab' && event.key !== 'Escape') {
1017 deleteSelectedText(e);
1019 let caretPos = getCurrentCursorPosition(e);
1020 let currText = e.innerText;
1022 if (event.key === 'Backspace' || event.key === 'Delete' || event.key === 'Enter') {
1023 //if (event.key === 'Enter') {
1024 // Handle Backspace, Delete and Enter manually for better compatibility and control
1025 if (event.key === 'Enter') {
1026 currText = currText.substring(0,caretPos) + "\n" + currText.substring(caretPos);
1027 caretPos = caretPos + 1 <= currText.length ? caretPos + 1 : caretPos;
1028 if (currText.substring(caretPos) == "" && isChrome === 1) currText = currText + "\n";
1029 } else if (event.key === 'Backspace') {
1030 currText = currText.substring(0,caretPos).slice(0,-1) + currText.substring(caretPos);
1031 caretPos = caretPos - 1 >= 0 ? caretPos - 1 : caretPos;
1032 } else if (event.key === 'Delete') {
1034 currText = currText.substring(0,caretPos) + currText.substring(caretPos).slice(0);
1036 currText = currText.substring(0,caretPos) + currText.substring(caretPos).slice(1);
1039 e.innerHTML = syntax_msl(currText).join(lc).slice(0,-1);
1040 setCurrentCursorPosition(e, caretPos);
1041 keepCaretVisible(e);
1043 } else if (event.key === 'Tab' || event.key === 'Escape') {
1044 let lines = getLinesInSelection(e);
1045 if (event.shiftKey && event.key === 'Tab') {
1046 indent_msl(lines,-1);
1047 } else if (event.key === 'Tab') {
1048 indent_msl(lines,+1);
1049 } else if (event.key === 'Escape') {
1052 e.innerHTML = syntax_msl(e.innerText).join(lc).slice(0,-1);
1053 let newText = e.innerText;
1054 setCurrentCursorPosition(e, caretPos + newText.length - currText.length);
1055 if (lines[0] !== lines[1]) selectLines(lines,e);
1060// Trigger syntax highlighting when you paste text
1061function checkSyntaxEventPaste(event) {
1062 // set time out to allow default to go first
1064 let e = event.target;
1065 // make sure you paste in the editor area
1066 if (e.tagName !== 'PRE') e = e.parentElement;
1067 let caretPos = getCurrentCursorPosition(e);
1068 let currText = e.innerText;
1069 // save current revision for undo
1070 saveState(currText,e);
1071 e.innerHTML = syntax_msl(currText).join(lc).slice(0,-1);
1072 setCurrentCursorPosition(e, caretPos);
1073 updateLineNumbers(e.previousElementSibling,e);
1079// Find on which line is the current carret position in e
1080// This assumes each line has an id="sline#" where # is the line number.
1081function whichLine(e,offset = 0) {
1082 // offset allows to pick previous line (after enter)
1083 let pos = getCurrentCursorPosition(e);
1085 let lineNum = e.innerText.substring(0,pos).split("\n").length + offset;
1086 let sline = e.querySelector("#sline" + lineNum.toString());
1090// Return an array with the first and last line numbers of the selected region
1091/* This assumes that the lines are in a <pre> element and that
1092 each line has an id="sline#" where # is the line number.
1093 When the caret in in an empty line, the anchorNode is the <pre> element.
1095function getLinesInSelection(e) {
1096 const selection = window.getSelection();
1097 if (selection.rangeCount === 0) return [0,0];
1098 // is it a single line?
1099 const singleLine = selection.isCollapsed;
1101 const line = whichLine(e);
1103 const startLine = parseInt(line.id.replace("sline",""));
1104 return [startLine,startLine];
1109 const anchorNode = selection.anchorNode;
1110 const range = selection.getRangeAt(0);
1111 let startNode,endNode;
1112 if (anchorNode.tagName === 'PRE') {
1113 let startOffset = range.startOffset;
1114 let endOffset = range.endOffset;
1115 startNode = range.startContainer.childNodes[startOffset];
1116 endNode = range.startContainer.childNodes[endOffset-1];
1118 startNode = (range.startContainer && range.startContainer.parentElement.tagName !== 'PRE') ? range.startContainer : range.startContainer.nextSibling;
1119 if (startNode && startNode.tagName === 'PRE') startNode = startNode.firstChild;
1120 endNode = (range.endContainer && range.endContainer.parentElement.tagName !== 'PRE') ? range.endContainer : range.endContainer.previousSibling;
1121 if (endNode && endNode.tagName === 'PRE') endNode = endNode.lastChild;
1123 let startID = (startNode && startNode.id) ? startNode.id : "";
1124 let endID = (endNode && endNode.id) ? endNode.id : "";
1126 while (startNode && !startID.startsWith("sline") && startNode.tagName !== 'PRE') {
1127 startNode = (startNode.parentNode.tagName !== 'PRE') ? startNode.parentNode : startNode.nextSibling;
1128 startID = (startNode && startNode.id) ? startNode.id : "";
1131 while (endNode && !endID.startsWith("sline") && endNode.tagName !== 'PRE') {
1132 endNode = (endNode.parentNode.tagName !== 'PRE') ? endNode.parentNode : endNode.previousSibling;
1133 endID = (endNode && endNode.id) ? endNode.id : "";
1135 let startLine = (startNode && startNode.id) ? parseInt(startNode.id.replace("sline","")) : 0;
1136 let endLine = (endNode && endNode.id) ? parseInt(endNode.id.replace("sline","")) : 0;
1138 startLine = endLine = Math.min(startLine, endLine);
1140 return [startLine,endLine];
1143// get current caret position in chars within element parent
1144function getCurrentCursorPosition(parent) {
1145 let sel = window.getSelection();
1146 if (!sel.focusNode || !parent) return;
1147 const range = sel.getRangeAt(0);
1148 const prefix = range.cloneRange();
1149 prefix.selectNodeContents(parent);
1150 prefix.setEnd(range.endContainer, range.endOffset);
1151 return prefix.toString().length;
1154// set current caret position at pos within element parent
1155function setCurrentCursorPosition(parent,pos) {
1156 for (const node of parent.childNodes) {
1157 if (node.nodeType === Node.TEXT_NODE) {
1158 if (node.length >= pos) {
1159 const range = document.createRange();
1160 const sel = window.getSelection();
1161 range.setStart(node, pos);
1162 range.collapse(true);
1163 sel.removeAllRanges();
1164 sel.addRange(range);
1167 pos = pos - node.length;
1170 pos = setCurrentCursorPosition(node, pos);
1179// Update line numbers in lineNumbers div
1180function updateLineNumbers(lineNumbers,editor) {
1181 if (lineNumbers === undefined || editor === undefined)
1182 [lineNumbers,editor] = editorElements();
1183 // Clear existing line numbers
1184 lineNumbers.innerHTML = "";
1185 // Get the number of lines accurately
1186 let lineCount = editor.querySelectorAll('[id^="sline"]').length;
1187 let lineTextCount = editor.innerText.split("\n").length;
1188 lineCount = (lineTextCount - lineCount) < 2 ? lineTextCount : lineTextCount - 1;
1189 // Add line numbers to lineNumbers
1190 for (let i = 1; i <= lineCount; i++) {
1191 const lineNumber = document.createElement('span');
1192 lineNumber.id = "lNum" + i.toString();
1193 lineNumber.textContent = i;
1194 lineNumbers.appendChild(lineNumber);
1196 lineNumbers.scrollTop = editor.scrollTop;
1200// Utility function to escape special characters in a string for use in a regular expression
1201function escapeRegExp(s) {
1203 return s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
1206// Syntax highlight any text according to provided rules
1207function syntax_msl(seqLines,keywordGroups) {
1208 // If not provided use the default msl keywords and groups
1209 if (!keywordGroups) {
1211 keywordGroups = mslDefs.groups;
1214 let mslText = Array.isArray(seqLines) ? seqLines.join("\n") : seqLines;
1215 // Make some cleanup of illegal characters
1216 mslText = escapeSpecialCharacters(mslText);
1217 // Keep original sequence lines (as array)
1218 let seqLines_org = mslText.split(/\n/);
1219 // Make full text if you get an array of lines
1220 let nLines = (mslText.match(/\n/g) || []).length;
1221 // These can be done on the text in one go
1223 let reg = /(["'])(.*?)\1/g;
1224 mslText = mslText.replace(reg,'<span class="msl_string">$1$2$1</span>');
1227 //reg = /^(COMMENT|#.*?)(.*)$/gim;
1228 //mslText = mslText.replace(reg,'<span class="msl_comment">$&</span>');
1231 //reg = /(\$[\w]+|^\s*[\w]+(?=\s*=))/gm;
1232 reg = /(?:\$[\w]+|^\b\w+(?=\s*=))/gm; // starting with $ or something =
1233 mslText = mslText.replace(reg,'<span class="msl_variable">$&</span>');
1234 reg = new RegExp("(^(?:\\s*)\\b(" + keywordGroups.variables.keywords.join("|") + ")\\s+)(\\w+)\\b", "gim"); // after PARAM, CAT and SET
1235 mslText = mslText.replace(reg,'$1<span class="msl_variable">$3</span>');
1237 // Data Management group excluding variables (must be after variables)
1238 reg = new RegExp("^(\\s*)\\b(" + keywordGroups.dataManagement.keywords.join("|") + ")\\b", "gim");
1239 mslText = mslText.replace(reg, "$1<span class='msl_data_management'>$2</span>");
1241 // Data Type group (must have comma before the keyword)
1242 reg = new RegExp("(?<=,\\s*)\\b(" + keywordGroups.dataTypes.keywords.join("|") + ")\\b", "gim");
1243 mslText = mslText.replace(reg, "<span class='msl_data_types'>$1</span>");
1245 // Loops group (must be at the begenning of the line)
1246 reg = new RegExp("^(\\s*)\\b(" + keywordGroups.loops.keywords.join("|") + ")\\b", "gim");
1247 mslText = mslText.replace(reg, "$1<span class='msl_loops'>$2</span>");
1249 // Control Flow group
1250 reg = new RegExp("^(\\s*)\\b(" + keywordGroups.controlFlow.keywords.join("|") + ")\\b", "gim");
1251 mslText = mslText.replace(reg, "$1<span class='msl_control_flow'>$2</span>");
1253 // Data Management group
1254 reg = new RegExp("^(\\s*)\\b(" + keywordGroups.dataManagement.keywords.join("|") + ")\\b", "gim");
1255 mslText = mslText.replace(reg, "$1<span class='msl_data_managemen'>$2</span>");
1257 // Information group
1258 reg = new RegExp("^(\\s*)\\b(" + keywordGroups.info.keywords.join("|") + ")\\b", "gim");
1259 mslText = mslText.replace(reg, "$1<span class='msl_info'>$2</span>");
1261 // Conditional group
1262 reg = new RegExp("^(\\s*)\\b(" + keywordGroups.cond.keywords.join("|") + ")\\b", "gim");
1263 mslText = mslText.replace(reg,"$1<span class='msl_cond'>$2</span>");
1266 reg = new RegExp("\\b(" + keywordGroups.units.keywords.join("|") + ")\\b", "gi");
1267 mslText = mslText.replace(reg, "<span class='msl_units'>$1</span>");
1270 reg = new RegExp("\\b(" + keywordGroups.actions.keywords.join("|") + ")\\b(\\s*)$", "gim");
1271 mslText = mslText.replace(reg, "<span class='msl_actions'>$1</span>$2");
1273 // Numbers/boolean group
1274 reg = /\b(\d+(\.\d+)?([eE][-+]?\d+)?)\b/g;
1275 mslText = mslText.replace(reg, '<span class="msl_number">$1</span>');
1276 reg = new RegExp("\\b(" + keywordGroups.bool.keywords.join("|") + ")\\b", "gi");
1277 //reg = /\b(true|false)\b/gi;
1278 mslText = mslText.replace(reg, '<span class="msl_bool">$1</span>');
1280 // Break lines and handle one by one
1281 seqLines = mslText.split("\n");
1283 // This is important for Firefox
1284 let emptyClass = "";
1285 if (isChrome === 1) emptyClass = "esline";
1286 // Loop and restore comment lines and empty lines
1287 for (let j = 0; j < seqLines_org.length ; j++) {
1288 let line = seqLines_org[j];
1289 let inlineComment = mslDefs.groups.comments.keywords[0];
1290 commentIndex = line.indexOf(inlineComment);
1291 if (line.trim().startsWith(inlineComment) || line.trim().toLowerCase().startsWith("comment")) {
1292 // Restore comment lines without highlighting
1293 seqLines[j] = `<span class='msl_comment'>${line}</span>`;
1294 } else if (commentIndex > 0) {
1295 // Restore comment section at end of line
1296 const comment = line.slice(commentIndex);
1297 seqLines[j] = seqLines[j].slice(0, seqLines[j].indexOf("#")) + `</span><span class='msl_comment'>${comment}</span>`;
1300 // empty class is needed for cursor movement in Firefox
1301 // for Chrome empty lines are skipped with arrow up??
1303 if ((j === seqLines_org.length - 1) && (isChrome === 1)) {
1304 seqLines[j] = "<span class=' ' id='sline" + (j+1).toString() + "'></span>";
1305 } else if (isChrome === 1) {
1306 seqLines[j] = "<span class='" + emptyClass + "' id='sline" + (j+1).toString() + "'></span>";
1308 seqLines[j] = "<span class='" + emptyClass + "' id='sline" + (j+1).toString() + "'></span>";
1311 seqLines[j] = "<span class='sline' id='sline" + (j+1).toString() + "'>" + seqLines[j] + "</span>";
1317// Adjust indentation of a selection of lines
1318function indent_msl(lines,addTab) {
1320 lines - an array of two elements, first and last line numbers
1321 addTab - (opt) +/-1 to add/subtract three spaces to selected lines
1323 let indentLevel = 0;
1324 let singleLine = false;
1325 let editor = editorElements()[1];
1326 // Indentation keywords
1327 const defIndent = mslDefs.defIndent;
1328 // Avoid issues of begenning of single line
1329 if (lines[0] > lines[1] || lines[0] == lines[1]) {
1330 lines[0] = lines[0] > 0 ? lines[0] : 1;
1331 lines[1] = lines[0];
1334 for (let j = lines[0]; j <= lines[1] ; j++) {
1335 let lineId = "#sline" + j.toString();
1336 let prevLineId = "#sline" + (j-1).toString();
1337 let lineEl = editor.querySelector(lineId);
1339 if (lineEl) line = lineEl.innerText;
1341 let indentString = " ".repeat(3);
1342 lineEl.innerText = indentString + line;
1343 } else if (addTab === -1) {
1344 lineEl.innerText = line.replace(/^\s{1,3}/, '');
1345 } else if (singleLine && editor.querySelector(prevLineId)) {
1346 let prevLineEl = editor.querySelector(prevLineId);
1347 let prevLine = prevLineEl.innerText;
1348 const indentMinos = defIndent.indentminos.some(keyword => prevLine.trim().toLowerCase().startsWith(keyword.toLowerCase()));
1349 const indentPlus = defIndent.indentplus.some(keyword => prevLine.trim().toLowerCase().startsWith(keyword.toLowerCase()));
1352 } else if (indentPlus) {
1355 let preSpace = prevLine.search(/\S|$/) + (indentLevel * 3);
1356 if (preSpace < 0) preSpace = 0;
1357 let indentString = " ".repeat(preSpace);
1358 lineEl.innerText = indentString + line.trimStart();
1360 const indentMinos = defIndent.indentminos.some(keyword => line.trim().toLowerCase().startsWith(keyword.toLowerCase()));
1361 if (indentMinos && indentLevel > 0) indentLevel--;
1362 let indentString = " ".repeat(indentLevel * 3);
1363 if (line !== "" || indentString !== "") {
1364 lineEl.innerText = indentString + line.trimStart();
1366 const indentPlus = defIndent.indentplus.some(keyword => line.trim().toLowerCase().startsWith(keyword.toLowerCase()));
1367 if (indentPlus) indentLevel++;
1372// Prepare the parameters/variables (if present) from the ODB as an html table
1373// Also return default ODB Paths and their respective values
1374function varTable(id,odbTreeVar) {
1376 id - ID of div to fill with the table of variables
1377 odbTreeVar - values of /Sequencer/Variables
1380 let e = document.getElementById(id);
1382 dlgAlert("Container ID was not give.");
1385 let nVars = 1,nVarsOld = 0;
1386 let oldTable = document.getElementById("varTable");
1387 if (oldTable) nVarsOld = oldTable.rows.length;
1388 // If /Sequencer/Variables are empty return empty
1389 if (!odbTreeVar || (odbTreeVar && Object.keys(odbTreeVar).length === 0)) {
1390 // Clear container row
1395 let html = "<table id='varTable' class='mtable infotable'>\n";
1396 html += "<tr><th style='min-width: 120px'>Variable </th><th>Current value </th></tr>\n";
1398 // Go over all variables in ODB and count them
1399 let varCount = Object.keys(odbTreeVar).length;
1400 for (let key in odbTreeVar) {
1401 const match = key.match(/([^/]+)\/name$/);
1404 const name = match[1];
1405 const value = odbTreeVar[name];
1406 let isBool = (typeof(value) === "boolean");
1408 html += `<tr><td>${name}</td><td><input type="checkbox" class="modbcheckbox" data-odb-path="/Sequencer/Variables/${name}"></span></td></tr>\n`;
1410 html += `<tr><td>${name}</td><td><span class="modbvalue" data-odb-path="/Sequencer/Variables/${name}"></span></td></tr>\n`;
1415 if (nVars !== nVarsOld) {
1421// Prepare the parameters/variables (if present) from the ODB as an object with
1422// html table, default ODB Paths, their respective values and their number
1423function parTable(odbTreeV,odbTreeC,odbTreeD,odbTreeO,odbTreeVar,editOnSeq) {
1425 odbTree... - Objects of ODB values
1428 let odbDefPaths = [];
1429 let odbDefValues = [];
1432 html += "<table id='paramTable' class='mtable infotable'>";
1433 html += "<tr><th>Parameter </th><th>Initial value </th><th>Comment</th></tr>";
1435 const processParam = (name, value, isBool, defValue, optValue, comment) => {
1436 let parLine = `<tr><td>${name}</td>`;
1439 // if not given the default is the first option
1440 if (defValue === undefined || defValue === "") defValue = optValue[0];
1441 const optionsHtml = optValue.map(option => `<option value="${option}" ${option === defValue ? 'selected' : ''}>${option}</option>`).join('');
1442 inParLine += `<select class="modbselect" data-odb-path="/Sequencer/Param/Value/${name}" data-odb-editable="1">${optionsHtml}</select>`;
1443 } else if (isBool) {
1444 inParLine += `<input type="checkbox" class="modbcheckbox" data-odb-path="/Sequencer/Param/Value/${name}" data-odb-editable="1"></input>`;
1446 inParLine += `<span class="modbvalue" data-odb-path="/Sequencer/Param/Value/${name}" data-odb-editable="1" data-input="1"></span>`;
1448 if (defValue !== undefined) {
1450 odbDefPaths.push(`/Sequencer/Param/Value/${name}`);
1451 odbDefValues.push(defValue);
1454 parLine += `<td>${inParLine}</td>`;
1455 parLine += `<td>${comment}</td></tr>`;
1459 // Go over all parameters in ODB
1460 for (let key in odbTreeV) {
1461 const match = key.match(/([^/]+)\/name$/);
1463 const name = match[1];
1464 // if variable is found use its value
1465 let value = (odbTreeVar && odbTreeVar[name]) ? odbTreeVar[name] : odbTreeV[name];
1466 let isBool = (typeof(value) === "boolean");
1467 let defValue = (value !== null && value !== undefined && value !== '') ? value : (odbTreeD && odbTreeD[name]) || value;
1468 let optValue = odbTreeO ? odbTreeO[name] : undefined;
1469 let comment = odbTreeC[name] || '';
1470 if (typeof value !== "object") {
1471 processParam(name, value, isBool, defValue, optValue, comment);
1476 // Go over Edit on sequence links
1477 for (let key in editOnSeq) {
1478 const match = key.match(/([^/]+)\/name$/);
1480 const name = match[1];
1481 const value = editOnSeq[name];
1482 let isBool = (typeof(value) === "boolean");
1484 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`;
1486 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`;
1495 values: odbDefValues,
1501// Prepare the parameters/variables (if present) from the ODB as a table
1502function dlgParam(debugFlag) {
1504 debugFlag - (opt) true/false run in debug/normal mode
1507 let odbTree = JSON.parse(sessionStorage.getItem('parameters'));
1508 const editOnSeq = JSON.parse(sessionStorage.getItem('editonseq'));
1509 // If /Sequencer/Param are empty, start and return
1510 if ((odbTree === null || Object.keys(odbTree.value).length === 0) && editOnSeq === null ) {
1511 //if ((odbTree === null || Object.keys(odbTree).length) && (editOnSeq === null || Object.keys(editOnSeq).length)) {
1513 modbset('/Sequencer/Command/Debug script',true);
1515 modbset('/Sequencer/Command/Start script',true);
1520 let odbTreeV = null;
1521 let odbTreeC = null;
1522 let odbTreeD = null;
1523 let odbTreeO = null;
1524 let odbTreeVar = null;
1527 odbTreeV = odbTree.value;
1528 odbTreeC = odbTree.comment;
1529 odbTreeD = odbTree.defaults;
1530 odbTreeO = odbTree.options;
1531 odbTreeVar = JSON.parse(sessionStorage.getItem('variables'));
1534 // Go over all parameters in ODB
1535 let seqParTable = parTable(odbTreeV,odbTreeC,odbTreeD,odbTreeO,odbTreeVar,editOnSeq);
1536 let html = seqParTable.html;
1537 // set all default values and once finished produce dialog
1538 // Collect paths where values start with "/"
1539 let valuesLinkODB = seqParTable.values.map((valueLinkODB, indexLinkODB) => {
1540 if (valueLinkODB !== undefined && typeof(valueLinkODB) === "string") {
1541 if (valueLinkODB.startsWith("/")) {
1542 return seqParTable.values[indexLinkODB];
1546 }).filter(path => path !== null);
1547 mjsonrpc_db_get_values(valuesLinkODB).then(function (rpc) {
1548 if (rpc.result.status.every(status => status === 1)) {
1549 // substitute values
1550 rpc.result.data.forEach((newData, index) => {
1551 let pathIndex = seqParTable.values.indexOf(valuesLinkODB[index]);
1552 seqParTable.values[pathIndex] = `${newData}`; // Update corresponding value
1554 mjsonrpc_db_paste(seqParTable.paths,seqParTable.values).then(function (rpc) {
1555 if ((rpc.result.status.every(status => status === 1)) || seqParTable.values.length === 0) {
1556 // if parContainer not given produce a dialog
1557 let htmlDlg = `${html}<br><button class="dlgButtonDefault" id="dlgParamStart" type="button">Start</button><button class="dlgButton" id="dlgParamCancel" type="button">Cancel</button><br>`;
1558 let d = dlgGeneral({html: htmlDlg,iddiv: "Parameters",minWidth:500});
1559 let e = document.getElementById("parContainer");
1560 // Append the table to a container
1561 let startBtn = document.getElementById("dlgParamStart");
1562 let cancelBtn = document.getElementById("dlgParamCancel");
1563 cancelBtn.addEventListener("click", function () {d.remove();});
1564 startBtn.addEventListener("click", function () {
1567 modbset('/Sequencer/Command/Debug script',true);
1569 modbset('/Sequencer/Command/Start script',true);
1573 // refresh immediately modbvalue elements
1576 dlgAlert("Something went wrong. Please try again!");
1578 }).catch(function (error) {
1579 console.error(error);
1582 let message = `ODB "${valuesLinkODB}" was not found.<br>Cannot start sequence!`;
1585 }).catch(function (error) {
1586 console.error(error);
1591// helper debug function
1592function debugSeq(parContainer) {
1593 startSeq(parContainer,true);
1596// helper start function
1597function startSeq(parContainer,debugFlag) {
1598 const [lineNumbers,editor,label] = editorElements();
1599 if (!debugFlag) debugFlag = false;
1600 if (editor.id !== "editorTab1" && parContainer === undefined) {
1601 let filename = label.title;
1603 dlgAlert("Please give the file a name first (Save as).");
1606 const message = debugFlag ? `Save and debug ${filename}?` : `Save and start ${filename}?`;
1607 dlgConfirm(message,function(resp) {
1610 openETab(document.getElementById("etab1-btn"));
1612 // Make sure to load file and reset parameters
1613 mjsonrpc_db_paste(["/Sequencer/Command/Load new file"],[true]).then(function (rpc) {
1614 if (rpc.result.status[0] === 1) {
1615 // Wait for Load new file to turn false
1616 checkODBValue("/Sequencer/Command/Load new file",false,dlgParam,debugFlag);
1622 // make sure to load file first
1623 mjsonrpc_db_paste(["/Sequencer/Command/Load new file"],[true]).then(function (rpc) {
1624 if (rpc.result.status[0] === 1) {
1625 // Wait for Load new file to turn false
1626 checkODBValue("/Sequencer/Command/Load new file",false,dlgParam,debugFlag);
1632// Helper function to add the current file to next files queue
1633function setAsNext() {
1634 const [lineNumbers,editor,label] = editorElements();
1635 // This is the addAsNext button cell
1636 const e = document.getElementById("addAsNext");
1637 let filename = label.title.split("\n")[0].replace(/^sequencer\//,'');
1638 let message = `Save and put ${filename} in the next file queue?`;
1639 dlgConfirm(message,function(resp) {
1641 let order = chngNextFilename(e,filename);
1642 if (order !== -1 && order !== undefined) {
1644 dlgAlert(`File saved and placed in position ${order} in the queue.`);
1650// helper stop function
1652 const message = `Are you sure you want to stop the sequence?`;
1653 dlgConfirm(message,function(resp) {
1655 modbset('/Sequencer/Command/Stop immediately',true);
1660// Show or hide parameters table
1661function showParTable(varContainer) {
1662 let e = document.getElementById(varContainer);
1663 let varTable = document.getElementById("varTable");
1664 let vis = document.getElementById("showParTable").checked;
1665 let visNF = (document.getElementById("nextFNContainer").style.display === "none");
1667 e.style.display = "flex";
1668 e.parentElement.style.width = "285px";
1670 e.style.display = "none";
1672 e.parentElement.style.width = "0px";
1678// Show or hide next file list
1679function showFNTable(nextFNContainer) {
1680 let e = document.getElementById(nextFNContainer);
1681 let vis = document.getElementById("showNextFile").checked;
1682 let visVar = document.getElementById("varContainer") ? (document.getElementById("varContainer").style.display === "none") : false;
1684 let addFileRow = "";
1686 mjsonrpc_db_get_values([mslDefs.filename.next]).then(function(rpc) {
1687 if (rpc.result.status[0] !== 1) return;
1688 let fList = rpc.result.data[0];
1689 for (let i = 0; i < fList.length; i++) {
1690 if (fList[i] && fList[i].trim() !== "") {
1691 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>`;
1695 if (vis && html !== "") {
1696 e.style.display = "flex";
1697 e.parentElement.style.width = "285px";
1699 disableMenuItems("SetAsNext",false);
1700 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>`;
1702 disableMenuItems("SetAsNext",true);
1704 e.innerHTML = `<table class="mtable infotable"><tr><th style="width:10px;"></th><th style="width:10px;"></th><th>Next files</th><th style="width:10px;"></th></tr>${html}${addFileRow}</table>`;
1707 e.style.display = "none";
1709 e.parentElement.style.width = "0px";
1713 activateDragDrop(e);
1714 }).catch (function (error) {
1715 console.error(error);
1719// Show extra rows for wait and loop
1720function extraRows(e) {
1722 e - triggering element to identify wait or loop
1724 // get current row, table and dialog
1725 let rIndex = e.parentElement.parentElement.rowIndex;
1726 let table = e.parentElement.parentElement.parentElement;
1727 let progressDlg = table.parentElement.parentElement.parentElement.parentElement.parentElement;
1728 // check if there is a wait or loop commands (if non-zero)
1731 if (e.id === "waitTrig") {
1732 // Make sure there is only one wait row
1733 const waitTRs = document.querySelectorAll('.waitTR');
1734 const waitFormula = (e.value === "Seconds") ? 'data-formula="x/1000"' : '';
1735 if (waitTRs.length) waitTRs.forEach(element => element.remove());
1737 let tr = table.insertRow(rIndex+1);
1738 tr.className = "waitTR";
1739 tr.innerHTML = `<td style="position: relative;width:80%;" colspan="2">
1740 <span class="modbhbar waitloopbar" style="position: absolute; top: 0; left: 0; color: lightgreen;" data-odb-path="/Sequencer/State/Wait value" ${waitFormula} id="mwaitProgress"></span>
1741 <span class="waitlooptxt">
1742 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>
1746 } else if (e.id === "loopTrig") {
1747 mjsonrpc_db_get_values(["/Sequencer/State/Loop n"]).then(function(rpc) {
1748 let loopArray = rpc.result.data[0];
1749 for (let i = 0; i < loopArray.length; i++) {
1750 if (loopArray[i] === 0) break;
1751 let tr = table.insertRow(rIndex+1);
1752 tr.className = "loopTR";
1753 tr.innerHTML = `<td style="position: relative;width:80%;" colspan="2">
1754 <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>
1755 <span class="waitlooptxt">
1756 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>]
1761 }).catch (function (error) {
1762 console.error(error);
1767 if (e.id === "waitTrig") document.querySelectorAll('.waitTR').forEach(element => element.remove());
1768 if (e.id === "loopTrig") document.querySelectorAll('.loopTR').forEach(element => element.remove());
1769 // hide progress div
1770 //dlgHide(progressDlg);
1775// Helper function to identify browser, 1 FF, 2 Chrome, 3, other
1776function browserType() {
1777 if (navigator.userAgent.indexOf("Chrome") !== -1) {
1779 } else if (navigator.userAgent.indexOf("Firefox") !== -1) {
1786// make visual hint that file is changes
1787function seqIsChanged(flag) {
1788 // flag - true is change, false is saved
1789 let fileChangedId = "filechanged" + editorElements()[0].id.replace("lineNumbers","");
1790 let filechanged = document.getElementById(fileChangedId);
1793 filechanged.innerHTML = " ✎";
1794 } else if (flag === undefined) {
1795 // true if text has changed false if not
1796 return (filechanged.innerHTML !== "");
1798 filechanged.innerHTML = "";
1801 // also change validation icon to defaule
1802 let vldBtn = document.getElementById("vldBtn");
1803 vldBtn.src = "icons/validate-syntax.svg";
1804 vldBtn.style.backgroundColor = "";
1807// save history of edits in element editor
1808function saveState(mslText,editor) {
1809 editor = (editor) ? editor : editorElements()[1];
1810 const editorId = editor.id;
1811 if (saveRevision[editorId] === false) {
1813 } else if (saveRevision[editorId] === undefined){
1814 saveRevision[editorId] = true;
1817 if (!previousRevisions[editorId]) {
1818 previousRevisions[editorId] = [];
1819 revisionIndex[editorId] = -1;
1822 // Add one more revision, and trim array if we had some undos
1823 revisionIndex[editorId]++;
1824 if (revisionIndex[editorId] < previousRevisions[editorId].length - 1) {
1825 previousRevisions[editorId].splice(revisionIndex[editorId] + 1);
1827 // Push new revision and keep only nRevisions revisions
1828 previousRevisions[editorId].push(mslText)
1829 if (previousRevisions[editorId].length > nRevisions) {
1830 previousRevisions[editorId].shift();
1832 revisionIndex[editorId] = previousRevisions[editorId].length - 1;
1836function undoEdit(editor) {
1837 editor = (editor) ? editor : editorElements()[1];
1838 const editorId = editor.id;
1839 if (revisionIndex[editorId] === 0) {
1840 // disable menu item
1841 disableMenuItems("undoMenu",true);
1842 seqIsChanged(false);
1846 disableMenuItems("undoMenu",false);
1847 revisionIndex[editorId]--;
1850 disableMenuItems("redoMenu",false);
1852 let caretPos = getCurrentCursorPosition(editor);
1853 let currText = editor.innerText;
1854 saveRevision[editorId] = false;
1855 editor.innerHTML = syntax_msl(previousRevisions[editorId][revisionIndex[editorId]]).join(lc).slice(0,-1);
1856 updateLineNumbers(editor.previousElementSibling,editor);
1857 saveRevision[editorId] = true;
1858 // calculate change in caret position based on length
1859 caretPos = caretPos + previousRevisions[editorId][revisionIndex[editorId]].length - currText.length;
1860 setCurrentCursorPosition(editor, caretPos);
1864function redoEdit(editor) {
1865 editor = (editor) ? editor : editorElements()[1];
1866 const editorId = editor.id;
1867 if (revisionIndex[editorId] >= previousRevisions[editorId].length - 1) {
1868 // disable menu item
1869 disableMenuItems("redoMenu",true);
1873 disableMenuItems("redoMenu",false);
1874 revisionIndex[editorId]++;
1877 disableMenuItems("undoMenu",false);
1880 let caretPos = getCurrentCursorPosition(editor);
1881 let currText = editor.innerText;
1882 saveRevision[editorId] = false;
1883 editor.innerHTML = syntax_msl(previousRevisions[editorId][revisionIndex[editorId]]).join(lc).slice(0,-1);
1884 updateLineNumbers(editor.previousElementSibling,editor);
1885 saveRevision[editorId] = true;
1886 // calculate change in caret position based on length
1887 caretPos = caretPos + previousRevisions[editorId][revisionIndex[editorId]].length - currText.length;
1888 setCurrentCursorPosition(editor, caretPos);
1891// Select slines from startLine to endLine
1892function selectLines([startLine, endLine],e) {
1893 const selection = window.getSelection();
1894 // Remove existing selections
1895 selection.removeAllRanges();
1896 let startElementId = '#sline' + startLine;
1897 let endElementId = '#sline' + endLine;
1898 let startElement = null, endElement = null;
1899 if (e.querySelector(startElementId)) startElement = e.querySelector(startElementId).firstChild;
1900 if (e.querySelector(endElementId)) endElement = e.querySelector(endElementId).lastChild;
1901 // we need startElement and endElement with first/lastChild
1902 // the following prevents loosing selection but not ideal
1903 while (startElement === null && startLine <= endLine) {
1905 startElementId = '#sline' + startLine;
1906 startElement = e.querySelector(startElementId).firstChild;
1908 while (endElement === null && endLine > 0) {
1910 endElementId = '#sline' + endLine;
1911 endElement = e.querySelector(endElementId).lastChild;
1913 if (startElement && endElement) {
1914 const range = document.createRange();
1915 // Set the start of the range to the startElement at offset 0
1916 range.setStart(startElement, 0);
1917 // Set the end of the range to the endElement at its length
1918 range.setEnd(endElement, endElement.childNodes.length);
1919 // Add the range to the selection
1920 selection.addRange(range);
1924// switch between dark and light modes on request
1925function lightToDark(lToDcheck) {
1926 if (!lToDcheck) return;
1927 const edt_areas = document.querySelectorAll('.edt_area');
1928 if (lToDcheck.checked) {
1929 localStorage.setItem("darkMode", true);
1930 edt_areas.forEach(area => {
1931 area.style.backgroundColor = "black";
1932 area.style.color = "white";
1934 updateCSSRule(".etab button:hover","background-color","black");
1935 updateCSSRule(".etab button:hover","color","white");
1936 updateCSSRule(".etab button:hover","border-bottom","5px solid black");
1937 updateCSSRule(".etab button.edt_active","background-color","black");
1938 updateCSSRule(".etab button.edt_active","color","white");
1939 updateCSSRule(".etab button.edt_active","border-bottom","5px solid black");
1940 updateCSSRule(".etab button.edt_active:hover","background-color","black");
1941 updateCSSRule(".etab button.edt_active:hover","border-bottom","5px solid black");
1943 localStorage.removeItem("darkMode");
1944 edt_areas.forEach(area => {
1945 area.style.backgroundColor = "white";
1946 area.style.color = "black";
1948 updateCSSRule(".etab button:hover","background-color","white");
1949 updateCSSRule(".etab button:hover","color","black");
1950 updateCSSRule(".etab button:hover","border-bottom","5px solid white");
1951 updateCSSRule(".etab button.edt_active","background-color","white");
1952 updateCSSRule(".etab button.edt_active","color","black");
1953 updateCSSRule(".etab button.edt_active","border-bottom","5px solid white");
1954 updateCSSRule(".etab button.edt_active:hover","background-color","white");
1955 updateCSSRule(".etab button.edt_active:hover","border-bottom","5px solid white");
1959function updateCSSRule(selector, property, value) {
1960 for (let i = 0; i < document.styleSheets.length; i++) {
1961 let styleSheet = document.styleSheets[i];
1962 let rules = styleSheet.cssRules || styleSheet.rules;
1963 if (!rules) continue;
1964 for (let j = 0; j < rules.length; j++) {
1965 let rule = rules[j];
1966 if (rule.selectorText === selector) {
1967 rule.style[property] = value;
1974// show/hide wait and loop progress
1975function showProgress(e) {
1976 //const progressDiv = document.getElementById("progressDiv");
1977 const progressDiv = document.getElementById("Progress");
1978 if (e === undefined) e = document.getElementById("showProgressBars");
1980 localStorage.setItem("showProgress",true);
1981 progressDiv.style.display = "block";
1982 //dlgShow(progressDiv);
1984 localStorage.removeItem("showProgress");
1985 progressDiv.style.display = "none";
1986 //dlgHide(progressDiv);
1990// Mark the current line number
1991function markCurrLineNum() {
1992 const [lineNumbers,editor] = editorElements();
1993 const currLines = lineNumbers.querySelectorAll(".edt_linenum_curr");
1994 const [startLine,endLine] = getLinesInSelection(editor);
1995 if (startLine === 0 && endLine === 0) return;
1996 currLines.forEach((line) => line.classList.remove("edt_linenum_curr"));
1997 for (let i = startLine; i <= endLine; i++) {
1998 let lineNumId = "#lNum" + i.toString();
1999 let lineNum = lineNumbers.querySelector(lineNumId);
2001 lineNum.className = "edt_linenum_curr";
2006// Check if program is progName is running.
2007// If not, try to get it going
2008function checkProgram(progName = "Sequencer") {
2009 mjsonrpc_call('cm_exist', {"name":progName,"unique":true}).then(function (rpc1) {
2010 mjsonrpc_db_get_values([`/Programs/${progName}/Start command`]).then(function(rpc2) {
2011 let isRunning = (rpc1.result.status === 1);
2012 let isDefined = ((rpc2.result.data[0] !== null) && (rpc2.result.data[0] !== ""));
2013 if (isRunning && isDefined) return;
2014 // progName is not running or not defined, stop it just in case and check the reason
2015 mjsonrpc_stop_program(progName);
2018 message = `${progName} program is not running.<br>Should I start it?`
2020 message = `${progName} program is not configured and not running.<br>Should I try to start it anyway?`
2022 dlgConfirm(message,function(resp) {
2023 // Guess the name of the actual binary
2024 let binName = (progName === "Sequencer") ? "msequencer" : progName.toLowerCase();
2027 // assume that progName is in path and create a start command, sleep 2s,
2028 // set value to "progName -D", sleep 2s, start program
2029 mjsonrpc_db_create([{"path" : `/Programs/${progName}/Start command`, "type" : TID_STRING}]).then(function (rpc3) {
2030 setTimeout(function(){
2031 mjsonrpc_db_paste([`/Programs/${progName}/Start command`],[`${binName} -D`]).then(function (rpc4) {
2032 if (rpc4.result.status[0] === 1) {
2033 mjsonrpc_start_program(progName);
2035 }).catch(function (error) {
2036 console.error(error);
2039 }).catch(function (error) {
2040 console.error(error);
2043 mjsonrpc_start_program(progName);
2045 // take 3 seconds and check that it actually started
2046 setTimeout(function(){
2047 mjsonrpc_call('cm_exist', {"name":progName,"unique":true}).then(function (rpc5) {
2048 if (rpc5.result.status === 1) {
2049 dlgAlert(`${progName} started successfully.`);
2051 dlgAlert(`Failed to start ${progName}!<br>Try to start it manually (${binName} -D)`);
2057 }).catch (function (error) {
2058 console.error(error);
2060 }).catch(function (error) {
2061 console.error(error);
2065function captureSelected() {
2066 if (window.getSelection) {
2067 let selection = window.getSelection();
2068 let text = selection.toString();
2069 let range = selection.getRangeAt(0);
2070 if (text && range) {
2072 startPath: range.startContainer,
2073 startOffset: range.startOffset,
2074 endPath: range.endContainer,
2075 endOffset: range.endOffset
2077 sessionStorage.setItem("tempSelText", text);
2078 sessionStorage.setItem("tempSelRange", JSON.stringify(rangeData));
2083function editMenu(action) {
2084 let text = sessionStorage.getItem("tempSelText") ?? "";
2085 let storedRange = sessionStorage.getItem("tempSelRange") ?? "";
2088 let rangeData = JSON.parse(storedRange);
2089 let startContainer = nodeFromPath(rangeData.startPath);
2090 let endContainer = nodeFromPath(rangeData.endPath);
2092 // Create a new range
2093 let newRange = new Range();
2094 newRange.setStart(startContainer, rangeData.startOffset);
2095 newRange.setEnd(endContainer, rangeData.endOffset);
2097 // Select the new range
2098 let selection = window.getSelection();
2099 selection.removeAllRanges();
2100 selection.addRange(newRange);
2103 if (action === "Copy") {
2105 sessionStorage.setItem("copiedText",text);
2107 } else if (action === "Paste") {
2108 const copiedText = sessionStorage.getItem("copiedText");
2110 newRange.deleteContents();
2111 newRange.insertNode(document.createTextNode(copiedText));
2113 } else if (action === "Cut") {
2115 sessionStorage.setItem("copyText",text);
2116 //document.execCommand("cut");
2117 newRange.deleteContents();
2119 } else if (action === "Undo") {
2121 } else if (action === "Redo") {
2126// Switch to the clicked tab
2127function openETab(btn) {
2128 const tabcontent = document.querySelectorAll(".etabcontent");
2129 const tablinks = document.querySelectorAll(".etablinks");
2130 const tab = btn ? btn : document.querySelectorAll(".edt_active")[0];
2131 const tabID = tab.id.replace("-btn","")
2132 tabcontent.forEach(content => {
2133 content.style.display = "none";
2135 tablinks.forEach(link => {
2136 link.classList.remove("edt_active");
2138 tab.className += " edt_active";
2139 //document.getElementById(tabID).style.display = "inline-flex";
2140 document.getElementById(tabID).style.display = "flex";
2141 // For the main sequence tab disable Save and Save as...
2142 if (tabID === "etab1") {
2143 disableMenuItems("noteditor",true);
2145 disableMenuItems("noteditor",false);
2147 // Change validation icon to defaule
2148 let vldBtn = document.getElementById("vldBtn");
2149 vldBtn.src = "icons/validate-syntax.svg";
2150 vldBtn.style.backgroundColor = "";
2152 // Adjust height of active editor
2156// Close the clicked tab
2157function closeTab(tab,event) {
2158 const tablinks = document.querySelectorAll(".etablinks");
2159 if (tablinks.length < 3) return;
2160 const tabCount = parseInt(tab.parentElement.id.replace("-btn","").replace("etab",""));
2161 const tabBtn = document.getElementById(`etab${tabCount}-btn`);
2162 const tabContent = document.getElementById(`etab${tabCount}`);
2163 if (seqIsChanged()) {
2164 dlgConfirm("File was changed, close anyway?", function(resp) {
2167 tabContent.remove();
2168 // switch to previous tab
2169 openETab(document.getElementById(`etab${(tabCount-1)}-btn`));
2174 tabContent.remove();
2175 // switch to previous tab
2176 openETab(document.getElementById(`etab${(tabCount-1)}-btn`));
2178 // need this since the close button is inside the tab button
2179 event.stopPropagation();
2182// Create and add a new editor tab
2183function addETab(btn) {
2184 // Create tab button
2185 const tabBtn = document.createElement("button");
2186 tabBtn.className = "etablinks";
2187 const tabCount = (btn.previousElementSibling) ? parseInt(btn.previousElementSibling.id.replace("-btn","").replace("etab","")) + 1 : 1;
2188 tabBtn.id = "etab" + tabCount + "-btn";
2189 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>`;
2190 btn.parentNode.insertBefore(tabBtn,btn);
2191 tabBtn.onclick = function () { openETab(this);};
2193 // Create editor area
2194 const tabContent = document.createElement("div");
2195 tabContent.id = "etab" + tabCount;
2196 tabContent.className = "etabcontent";
2198 if (localStorage.getItem("darkMode")) makeDark = "style='background-color: black; color: white;'";
2200 `<pre id="lineNumbers${tabCount}" class="edt_linenum"></pre><pre id="editorTab${tabCount}" ${makeDark} class="edt_area" spellcheck="false" contenteditable="false"></pre>`;
2201 tabContent.innerHTML = html;
2202 const lastETab = document.getElementById("lastETab");
2203 lastETab.parentNode.insertBefore(tabContent,lastETab);
2206 // Add event listeners
2207 editorEventListeners();
2210// Return the pre of lineNumbers, editor, tab label and tab button element of the active tab
2211function editorElements() {
2212 const btn = (document.querySelectorAll(".edt_active")[0]) ? document.querySelectorAll(".edt_active")[0] : document.getElementById("etab1-btn");
2213 const tab = document.getElementById(btn.id.replace("-btn",""));
2214 const [lineNumbers,editor] = tab.children;
2215 const btnLabel = (btn.id !== "etab1-btn") ? btn.children[0] : btn.children[1];
2216 return [lineNumbers,editor,btnLabel,btn];
2219// disable and enable clicking on menu item
2220function disableMenuItems(className,flag) {
2222 className - the class name of the item
2223 flag - true/false to enable/disable item
2225 const els = document.querySelectorAll(`.${className}`);
2228 e.style.opacity = 0.5;
2229 e.style.pointerEvents = "none";
2231 e.style.opacity = "";
2232 e.style.pointerEvents = "";
2237// Function to replace some special characters in the text
2238function escapeSpecialCharacters(text) {
2239 return text.replace(/&/g, "&")
2240 .replace(/</g, "<")
2241 .replace(/>/g, ">")
2242 .replace(/\r\n|\n\r/g, '\n')
2243 //.replace(/"/g, """)
2244 //.replace(/'/g, "'")
2245 .replace(/\t/g, " ");
2248// Produce a help window
2251<span style='text-align: left;'>
2252<b>Hidden features of the sequencer editor</b>
2253<ul style='white-space: pre;font-family: monospace;'>
2254<li>Double click on the edit area of the first (main) tab to edit the currently loaded sequence.</li>
2255<li>Tab - Indent selected lines.</li>
2256<li>Shift+Tab - Unindent selected lines.</li>
2257<li>Escape - Autoindent selected lines according to syntax rules.</li>
2258<li>Ctrl+C - Copy selected text.</li>
2259<li>Ctrl+V - Paste selected text.</li>
2260<li>Ctrl+A - Select all text.</li>
2261<li>Ctrl+Z - Undo last change.</li>
2262<li>Ctrl+R - Redo last undo.</li>
2266 const d = dlgMessage("Editor help",help, false, false);
2267 const btn = d.querySelectorAll('.dlgButton')[0];
2268 btn.className = "dlgButtonDefault";
2273// Activate drag and drop events on next files table
2274function activateDragDrop(table) {
2276 table - The table element containing the list of next files.
2278 // collect all rows with class dragrow
2279 const rows = table.querySelectorAll('.dragrow');
2280 let dragStartIndex,dragEndIndex;
2282 // add event listeners
2283 rows.forEach(row => {
2284 row.addEventListener('dragstart', dragStart);
2285 row.addEventListener('dragover', dragOver);
2286 row.addEventListener('dragend', dragEnd);
2289 function dragStart(e) {
2290 dragStartIndex = Array.from(rows).indexOf(this);
2291 rows.forEach(row => row.classList.remove('dragstart'));
2292 this.classList.add('dragstart');
2294 function dragOver(e) {
2296 dragEndIndex = Array.from(rows).indexOf(this);
2297 // Create or update the empty row element
2299 emptyRow = document.createElement('tr');
2300 emptyRow.innerHTML = "<td colspan=4> </td>";
2301 emptyRow.classList.add('empty-dragrow');
2303 // Insert the empty row element at the appropriate position
2304 if (dragEndIndex > dragStartIndex) {
2305 this.parentNode.insertBefore(emptyRow, this.nextSibling);
2306 } else if (dragEndIndex < dragStartIndex) {
2307 this.parentNode.insertBefore(emptyRow, this);
2310 function dragEnd(e) {
2315 reorderNextFilenames(dragStartIndex,dragEndIndex);
2316 rows.forEach(row => {
2317 row.classList.remove('dragstart');
2322// Move next filename from dragStarIndex to dragEndIndex position
2323function reorderNextFilenames(dragStarIndex,dragEndIndex) {
2325 dragStarIndex - move file from this indext.
2326 dragEndIndex - destination index of the file.
2328 if (dragStarIndex === dragEndIndex) return;
2329 let odbpath = mslDefs.filename.next;
2330 mjsonrpc_db_get_values([odbpath]).then(function(rpc) {
2331 if (rpc.result.status[0] !== 1) return;
2332 let fList = rpc.result.data[0];
2334 // Remove the draggedFile from the starting index
2335 const draggedFile = fList.splice(dragStarIndex, 1)[0];
2336 // Insert the draggedFile at the new index
2337 fList.splice(dragEndIndex, 0, draggedFile);
2338 // Check that the list does not contain more than 10 files
2339 if (fList.length > 10) {
2340 dlgAlert("There are more than 10 files. Aborting!.");
2343 // Update values in ODB
2344 mjsonrpc_db_paste([odbpath],[fList]).then(function (rpc2) {
2345 if (rpc2.result.status[0] !== 1) {
2346 dlgAlert("Failed to move the file!<br>Please check.");
2347 } else if (rpc2.result.status[0] === 1) {
2348 showFNTable('nextFNContainer');
2350 }).catch(function (error) {
2351 console.error(error);
2353 }).catch (function (error) {
2354 console.error(error);
2358// Change the next file name in the clicked row
2359function chngNextFilename(e,filename) {
2361 e - (optional) cell element on the same row of "next file" to be changed.
2362 If last row (or undefined) add a file to the end of the queue.
2363 filename - (optional) The file name to add to the end of the queue.
2365 if (e === undefined) e = document.getElementById("addAsNext");
2366 // file index from table row index
2367 let index = e ? e.parentElement.rowIndex - 1 : 0;
2368 // Only 10 files are allowed
2370 dlgAlert("Maximum number (10) of next files reached!");
2374 let odbpath = mslDefs.filename.next;
2375 mjsonrpc_db_get_values(["/Sequencer/State/Path"]).then(function(rpc) {
2376 let path = mslDefs.filename.path + "/" + rpc.result.data[0].replace(/\/+/g, '/').replace(/^\/+|\/+$/g, '');
2377 sessionStorage.setItem("pathName",path);
2379 odbpath = odbpath + "[" + index.toString() + "]";
2380 modbset(odbpath,filename);
2382 file_picker(path,mslDefs.filename.ext,function(filename) {
2383 filename = filename.replace(/^sequencer\//,'').replace(/^\//,'');
2384 odbpath = odbpath + "[" + index.toString() + "]";
2386 modbset(odbpath,filename);
2391 }).catch(function (error) {
2392 mjsonrpc_error_alert(error);
2396// Reomve the next file name from the queue
2397function remNextFilename(e) {
2399 e - cell element on the same row of "next file" to be removed
2401 // file index from table row index
2402 let index = e.parentElement.rowIndex - 1;
2403 let odbpath = mslDefs.filename.next;
2404 mjsonrpc_db_get_values([odbpath]).then(function(rpc) {
2405 if (rpc.result.status[0] !== 1) return;
2406 let fList = rpc.result.data[0];
2407 // empty last element
2412 for (let i = index+1; i < fList.length; i++) {
2413 paths[j] = odbpath + "[" + (i-1).toString() + "]";
2414 values[j] = fList[i];
2417 mjsonrpc_db_paste(paths,values).then(function (rpc2) {
2418 if (rpc2.result.status[0] !== 1) {
2419 dlgAlert("Failed to remove file from list!<br>Please check.");
2420 } else if (rpc2.result.status[0] === 1) {
2421 showFNTable('nextFNContainer');
2423 }).catch(function (error) {
2424 console.error(error);
2426 }).catch (function (error) {
2427 console.error(error);
2432// Wait for ODB in path to have value
2433// If value is not reached, give up after 10s
2434function checkODBValue(path,value,funcCall,args) {
2436 path - ODB path to monitor for value
2437 value - the value to be reached and return success
2438 funcCall - function name to call when value is reached
2439 args - argument to pass to funcCall
2441 // Call the mjsonrpc_db_get_values function
2442 mjsonrpc_db_get_values([path]).then(function(rpc) {
2443 if (rpc.result.status[0] === 1 && rpc.result.data[0] !== value) {
2444 console.log("Value not reached yet", NcheckValue);
2446 if (NcheckValue < 100) {
2447 // Wait 0.1 second and then call checkODBValue again
2448 // Time out after 10 s
2450 checkODBValue(path,value,funcCall,args);
2454 if (funcCall) funcCall(args);
2455 console.log("Value reached, proceeding...");
2459 }).catch(function(error) {
2460 console.error(error);
2464function isTextSelected(element) {
2465 const selection = window.getSelection();
2466 if (!selection.rangeCount) return false;
2468 const range = selection.getRangeAt(0);
2469 const selectedText = selection.toString();
2471 // Check if the selection is within the specified element
2472 if (!element.contains(range.commonAncestorContainer)) return false;
2474 // Check if there is any selected text
2475 return selectedText.length > 0;
2478function deleteSelectedText(element) {
2479 const selection = window.getSelection();
2480 if (!selection.rangeCount) return; // No selection exists
2481 const range = selection.getRangeAt(0);
2482 // Ensure the selection is within the target element
2483 if (!element.contains(range.commonAncestorContainer)) return;
2484 // Delete the selected text
2485 range.deleteContents();
2488// Make sure that the caret stays visible
2489function keepCaretVisible(editor) {
2490 const selection = window.getSelection();
2491 const caretPosition = selection.getRangeAt(0).getBoundingClientRect();
2492 const containerRect = editor.getBoundingClientRect();
2493 const caretTop = caretPosition.top - containerRect.top;
2494 const caretBottom = caretPosition.bottom - containerRect.top;
2495 const containerHeight = containerRect.height;
2496 // Caclulate font size
2497 const emInPixels = parseFloat(window.getComputedStyle(editor).fontSize) * 1.5;
2499 // If the caret is near the bottom, scroll the container down with 1.5 fontSize
2500 if (caretBottom > containerHeight) {
2501 editor.scrollTop += caretBottom - containerHeight + emInPixels;
2504 // If the caret is near the top, scroll the container up
2506 editor.scrollTop -= Math.abs(caretTop);
2511// commands object for syntax validation
2512const mslCommands = {
2514 patterns: [/^INCLUDE\s+.+(\.msl)?$/i],
2515 description: "Include another MSL file.",
2519 patterns: [/^BREAK$/i],
2520 description: "Break (finish prematurely) a loop.",
2524 patterns: [/^CALL\s+\w+(\s*,\s*.+)*$/i],
2525 description: "Call a subroutine with optional parameters.",
2529 patterns: [/^CAT\s+\w+(\s*,\s*.+)*$/i],
2530 description: "Concatenate strings into a single variable.",
2534 patterns: [/^(?:COMMENT\s+.*|#.*)$/i],
2535 description: "A comment line starting with COMMENT or #.",
2539 patterns: [/^EXIT$/i],
2540 description: "Exit the script immediately.",
2544 patterns: [/^GOTO\s+\d+$/i],
2545 description: "Jump to a specific line in the script.",
2549 patterns: [/^IF\s*\(.+\)$/i],
2550 description: "Conditional execution of code blocks.",
2551 blockStart: true, // Marks this as a block start command
2555 patterns: [/^ELSE$/i],
2556 description: "Alternative block for an IF statement.",
2560 patterns: [/^ENDIF$/i],
2561 description: "Ends an IF block.",
2562 blockEnd: "IF", // Marks this as a block end command for IF
2566 patterns: [/^LIBRARY\s+.+$/i],
2567 description: "Declare the file as a library.",
2572 /^LOOP\s+(\d+|INFINITE|\$\w+)$/i, // LOOP 5, LOOP INFINITE, or LOOP $varName
2573 /^LOOP\s+\w+\s*,\s*.+$/i, // LOOP i, 1:10
2574 // /^LOOP\s+\w+\s*,\s*(?:\d+:\d+|\S+)$/i, // LOOP i, 1:10
2576 description: "Execute a loop a fixed number of times or with values.",
2577 blockStart: true, // Marks this as a block start command
2579 //argcond1: /^(\d+|INFINITE|\$\w+)$/, // First argument can be a number, INFINITE, or a variable
2582 patterns: [/^ENDLOOP$/i],
2583 description: "Ends a LOOP block.",
2584 blockEnd: "LOOP", // Marks this as a block end command for LOOP
2588 patterns: [/^MESSAGE\s+.+(\s*,\s*1)?$/i],
2589 description: "Display a message in the browser.",
2593 patterns: [/^MSG\s+.+(\s*,\s*(ERROR|INFO|DEBUG|LOG|TALK))?$/i],
2594 description: "Send a message to the MIDAS buffer.",
2598 patterns: [/^ODBCREATE\s+.+,\s+\w+(\s*,\s*\d+)?$/i],
2599 description: "Create an ODB key.",
2603 patterns: [/^ODBDELETE\s+.+$/i],
2604 description: "Delete an ODB key or subdirectory.",
2608 patterns: [/^ODBGET\s+".+",\s+\w+$/i],
2609 description: "Retrieve a value from the ODB.",
2611 argcond: [/^\/.+$/, // First argument must start with /
2612 /^.+$/], // Second argument can be anything
2615 patterns: [/^ODBINC\s+.+(\s*,\s*[+-]?\d+)?$/i],
2616 description: "Increment an ODB value.",
2620 patterns: [/^ODBLOOKUP\s+".+",\s+".+",\s+\w+$/i],
2621 description: "Lookup a string in the ODB array.",
2625 //patterns: [/^ODBSET\s+("[^"]*"|\/[^,]+),\s*("[^"]*"|[^,]+)(\s*,\s*[01])?$/i,],
2626 patterns: [/^ODBSET\s+.+,\s+.+(\s*,\s*[01])?$/i],
2627 description: "Set a value in the ODB.",
2630 /^.+$/, // First argument must start with / or be a quoted string
2631 /^.+$/, // Second argument can be a quoted string or unquoted value
2632 /^[01]$/, // Third argument (optional) must be 0 or 1
2636 patterns: [/^ODBLOAD\s+.+(\s*,\s*.+)?$/i],
2637 description: "Load an external file into the ODB.",
2641 patterns: [/^ODBSAVE\s+.+,\s+.+$/i],
2642 description: "Save part of the ODB to a file.",
2646 patterns: [/^ODBSUBDIR\s+("\/[^"]*"|\/[^,]+)$/i],
2647 description: "Set a base path for ODB commands.",
2648 blockStart: true, // Marks this as a block start command
2652 patterns: [/^ENDODBSUBDIR$/i],
2653 description: "Ends an ODBSUBDIR block.",
2654 blockEnd: "ODBSUBDIR", // Marks this as a block end command for ODBSUBDIR
2658 patterns: [/^PARAM\s+\w+(\s*,\s*.+)*$/i],
2659 description: "Define script parameters.",
2663 patterns: [/^RUNDESCRIPTION\s+.+$/i],
2664 description: "Set a run description.",
2668 patterns: [/^SCRIPT\s+.+(\s*,\s*.+)*$/i],
2669 description: "Call a server-side script with optional parameters.",
2673 patterns: [/^SET\s+\w+\s*,\s*.+$/i],
2674 description: "Set a variable to a value.",
2678 patterns: [/^SUBROUTINE\s+\w+$/i],
2679 description: "Start a subroutine block.",
2680 blockStart: true, // Marks this as a block start command
2684 patterns: [/^ENDSUBROUTINE$/i],
2685 description: "Ends a subroutine block.",
2686 blockEnd: "SUBROUTINE", // Marks this as a block end command for SUBROUTINE
2690 patterns: [/^TRANSITION\s+(start|stop|pause|resume)$/i],
2691 description: "Trigger a run state transition.",
2696 /^WAIT\s+seconds,\s*(\d+|\$\w+)$/i, // WAIT seconds,10
2697 /^WAIT\s+events,\s*(\d+|\$\w+)$/i, // WAIT events,10
2698 /^WAIT\s+ODBvalue,\s*\/.+,\s*[<>=!]+,\s*(\d+|\$\w+)$/i, // WAIT ODBvalue, /path, <, 100
2700 description: "Wait for a condition, time, or event.",
2704 patterns: [/^[a-zA-Z_]\w*\s*=\s*([0-9]+(\.\d+)?|\$[a-zA-Z_]\w*|[\d\w\$\s\+\-\*\/\^\(\)]+|[\w\$]+\(.*\)|".*"|'.*')$/i],
2705 description: "Variable assignment or equation.",
2710// Validation function to validate script content
2711function checkEditorContent(e, commands = mslCommands) {
2712 // Get the content from the current editor
2713 let [lineNumbers,editor] = editorElements();
2714 const editorContent = editor.innerText;
2716 // Validate the script content
2717 const errors = validateScript(editorContent,commands);
2719 if (errors.length > 0) {
2720 // Format and display errors
2721 let vldErrors = errors.map(err => {
2722 if (typeof err === "string") {
2723 return err; // Handle string errors directly
2724 } else if (err.line && err.message) {
2725 return `Line ${err.line}: ${err.message}`; // Format object errors
2727 return "Unknown error"; // Fallback for unexpected error formats
2730 // highlight error lines and scroll to first one
2731 hlLine(errors.map(error => error.line),"var(--mred)",editor,errors.map(error => error.message));
2732 setCurrentCursorPosition(editor, errors[0].line - 1 + editorContent.split("\n").slice(0,errors[0].line).reduce((sum, line) => sum + line.length, 0));
2735 // Show the error message
2736 //dlgMessage("Validation errors",vldErrors,false,true);
2737 console.error("Validation errors:\n",vldErrors);
2738 e.src = "icons/validate-syntax-red.svg";
2739 e.style.backgroundColor = "var(--mred)";
2741 //dlgAlert("The syntax of the sequence is valid.");
2742 e.src = "icons/validate-syntax-green.svg";
2743 e.style.backgroundColor = "var(--mgreen)";
2747// Function to validate the script against commands object
2748function validateScript(script, commands = mslCommands) {
2751 // Stack to track nested blocks
2752 const blockStack = [];
2753 const lineNums = [];
2754 // Stack to track delimiters
2755 const delimiterStack = [];
2756 const delimiterLineNums = [];
2758 // Delimiters and their matching pairs
2759 const delimiters = {
2767 // Regex to match and replace all comments by space
2768 const commentRegex = new RegExp(commands.COMMENT.patterns.map(pattern => pattern.source).join("|"), "gim");
2769 const lines = script.replace(commentRegex, "").split("\n");
2771 lines.forEach((line, index) => {
2772 const trimmedLine = line.trim();
2773 if (!trimmedLine) return; // Skip empty lines
2775 let isValid = false;
2777 // Check if the line matches any command pattern
2778 const cmdKey = Object.keys(commands).find(cmd => {
2779 const cmdData = commands[cmd];
2780 const cmdPattern = cmdData.patterns || null;
2781 if (Array.isArray(cmdPattern)) {
2782 isValid = cmdPattern.some(pattern => pattern.test(trimmedLine));
2784 // Handle block start commands
2785 if (isValid && cmdData.blockStart) {
2786 blockStack.push(cmd); // Push the block type to the stack
2787 lineNums.push(index + 1);
2790 // Handle block end commands
2791 if (isValid && cmdData.blockEnd) {
2792 const expectedBlock = cmdData.blockEnd;
2793 const lastBlock = blockStack.pop();
2796 if (lastBlock !== expectedBlock) {
2797 errors.push({ line: index + 1, message: `"${cmd}" without matching "${expectedBlock}".` });
2801 // Validate arguments if argcond is defined
2802 if (isValid && cmdData.argcond) {
2803 // Remove the command keyword from the line and split remaining arguments by comma
2804 const args = splitArguments(trimmedLine.replace(new RegExp(`^${cmd}`, "i"), "").trim());
2805 for (let i = 0; i < args.length; i++) {
2806 const arg = args[i].trim().replace(/^"(.*)"$/, "$1"); // Remove surrounding quotes
2807 const argCond = cmdData.argcond[i]; // Get the condition for this argument
2808 if (argCond && !argCond.test(arg)) {
2809 errors.push({ line: index + 1, message: `Argument ${i + 1} of "${cmd}" is invalid.` });
2816 return cmdPattern && cmdPattern.test(trimmedLine);
2820 // Valid command or comment, no error
2823 errors.push({ line: index + 1, message: `Syntax error or unknown command.` });
2826 // Delimiter validation
2827 for (let i = 0; i < trimmedLine.length; i++) {
2828 const char = trimmedLine[i];
2829 if (delimiters[char]) {
2830 // Opening delimiter
2831 delimiterStack.push({ char, line: index + 1, pos: i + 1 });
2832 } else if (Object.values(delimiters).includes(char)) {
2833 // Closing delimiter
2834 if (delimiterStack.length === 0) {
2835 errors.push({ line: index + 1, message: `Mismatched "${char}" at line ${index + 1}, column ${i + 1}.` });
2837 const lastDelimiter = delimiterStack.pop();
2838 if (delimiters[lastDelimiter.char] !== char) {
2839 errors.push({ line: index + 1, message: `Mismatched "${char}" at line ${index + 1}, column ${i + 1}. Expected "${delimiters[lastDelimiter.char]}".` });
2846 // Check for unclosed blocks at the end of the script
2847 if (blockStack.length > 0) {
2848 errors.push({ line: lineNums[0], message: `Unclosed "${blockStack[0]}" block starting at line ${lineNums[0]}.` });
2851 // Check for unclosed delimiters at the end of the script
2852 if (delimiterStack.length > 0) {
2853 errors.push({ line: delimiterLineNums[0], message: `Unclosed "${delimiterStack[0].char}" delimiter starting at line ${delimiterLineNums[0]}, column ${delimiterStack[0].pos}.` });
2859// Helper function to split arguments with quoted strings by commas
2860function splitArguments(input) {
2861 // Regex to match quoted strings
2862 const regex = /(["'])(.*?)\1/g;
2866 // Step 1: Find all quoted strings and split the input around them
2868 while ((match = regex.exec(input)) !== null) {
2869 // Add unquoted arguments before the current quoted string
2870 const beforeQuoted = input.slice(lastIndex, match.index).trim();
2872 args.push(...beforeQuoted.split(",").map(arg => arg.trim()).filter(arg => arg.length > 0));
2875 // Add the quoted argument
2876 args.push(match[2]); // Capture the content inside the quotes
2878 // Update lastIndex to the end of the current match
2879 lastIndex = regex.lastIndex;
2882 // Step 2: Add any remaining unquoted arguments after the last quoted string
2883 const remaining = input.slice(lastIndex).trim();
2885 args.push(...remaining.split(",").map(arg => arg.trim()).filter(arg => arg.length > 0));