MIDAS
Loading...
Searching...
No Matches
sequencer.js
Go to the documentation of this file.
1/*
2
3 Sequencer syntax highlighting and operation functions
4
5 Created by Zaher Salman on September 16th, 2023
6
7 Usage
8 =====
9
10 Some of these functions, although specific to the midas sequencer,
11 can be used for general syntax highlighting and editing.
12
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.
16
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
22 be used.
23
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
30
31 todo:
32 - syntax validate ODBSET, ODBGET etc. have a valid ODB parameter
33*/
34
35// Using Solarized color scheme - https://ethanschoonover.com/solarized/
36
37// Default msl definitions, keywords, indentation and file name
38const mslDefs = {
39 groups : {
40 strings: {
41 keywords: [/(["'])(.*?)\1/g],
42 color: "#008000",
43 fontWeight: "",
44 mslclass: "msl_string",
45 },
46 variables: {
47 //keywords: [/(\$[\w]+|^\s*[\w]+(?=\s*=))/gm],
48 keywords: ["PARAM","CAT","SET"],
49 color: "#8B0000",
50 fontWeight: "",
51 mslclass: "msl_variable",
52 },
53 controlFlow: {
54 keywords: ["GOTO", "CALL", "SCRIPT", "SUBROUTINE", "ENDSUBROUTINE", "TRANSITION", "INCLUDE", "EXIT"],
55 color: "#268bd2", // blue
56 fontWeight: "",
57 mslclass: "msl_control_flow",
58 },
59 dataManagement: {
60 keywords: ["ODBSET", "ODBGET", "ODBCREATE", "ODBDELETE", "ODBINC", "ODBLOAD", "ODBSAVE", "PARAM", "SET", "CAT"],
61 color: "#2aa198", // cyan
62 fontWeight: "",
63 mslclass: "msl_data_management",
64 },
65 info: {
66 keywords: ["RUNDESCRIPTION", "LIBRARY", "MESSAGE", "MSG"],
67 color: "#6c71c4", // violet
68 fontWeight: "",
69 mslclass: "msl_info",
70 },
71 cond: {
72 keywords: ["IF", "ELSE", "ENDIF", "WAIT"],
73 color: "#c577f6", // pink (not solarized)
74 fontWeight: "",
75 mslclass: "msl_cond",
76 },
77 loops: {
78 keywords: ["BREAK", "LOOP", "ENDLOOP", "ODBSUBDIR", "ENDODBSUBDIR"],
79 color: "#d33682", // magenta
80 fontWeight: "",
81 mslclass: "msl_loops",
82 },
83 dataTypes: {
84 keywords: ["UNIT8", "INT8", "UNIT16", "INT16", "UNIT32", "INT32", "BOOL", "FLOAT", "DOUBLE", "STRING"],
85 color: "#859900", // green
86 fontWeight: "",
87 mslclass: "msl_data_types",
88 },
89 units: {
90 keywords: ["SECONDS", "EVENTS", "ODBVALUE"],
91 color: "#cb4b16", // orange
92 fontWeight: "",
93 mslclass: "msl_units",
94 },
95 actions: {
96 keywords: ["start", "stop", "pause", "resume"],
97 color: "#dc322f", // red
98 fontWeight: "",
99 mslclass: "msl_actions",
100 },
101 bool: {
102 keywords: ["true","false"],
103 color: "#61b7b7",
104 fontWeight: "",
105 mslclass: "msl_bool",
106 },
107 numbers: {
108 keywords: [/\b(?<![0-9a-fA-F#])\d+(\.\d+)?([eE][-+]?\d+)?\b/g],
109 color: "#b58900", // yellow
110 fontWeight: "",
111 mslclass: "msl_number",
112 },
113 comments: {
114 keywords: ["#","COMMENT"],
115 color: "#839496", // base0
116 fontWeight: "italic",
117 mslclass: "msl_comment",
118 },
119 },
120 defIndent: {
121 indentplus: ["IF", "LOOP", "ELSE", "SUBROUTINE", "ODBSUBDIR"],
122 indentminos: ["ENDIF", "ENDLOOP", "ELSE", "ENDSUBROUTINE", "ENDODBSUBDIR"],
123 },
124 filename: {
125 ext : "*.msl",
126 path : "sequencer",
127 next : "/Sequencer/State/Next Filename",
128 },
129};
130
131var seq_css = `
132/* For buttons */
133.seqbtn {
134 display: none;
135}
136
137#iconsRow img {
138 cursor: pointer;
139 padding: 0.4em;
140 margin: 0;
141}
142#iconsRow img:hover {
143 background: #C0D0D0;
144}
145
146.edt_linenum {
147 background-color: #f0f0f0;
148 user-select: none;
149 width: 3em;
150 overflow: hidden;
151 color: gray;
152 text-align: right;
153 padding: 5px 5px 20px 5px;
154 box-sizing: border-box;
155 vertical-align: top;
156 display: inline-block;
157 margin: 0 0 0 0;
158 font-family: monospace;
159 white-space: pre;
160 -moz-user-select: text;
161 pointer-events: none;
162}
163.edt_linenum span {
164 display: block;
165}
166.edt_linenum_curr {
167 color: #000000;
168 font-weight: bold;
169}
170.edt_area {
171 width: calc(100% - 3em);
172 max-width: calc(100% - 3em);
173 overflow:auto;
174 /* resize: horizontal; */
175 background-color:white;
176 color: black;
177 padding: 5px 5px 20px 5px;
178 box-sizing: border-box;
179 vertical-align: top;
180 display: inline-block;
181 margin: -1px 0 0 0;
182 border-top: 1px solid gray;
183 font-family: monospace;
184 white-space: pre;
185 -moz-user-select: text;
186}
187
188.sline {
189 display: inline-block;
190}
191.esline {
192 display: inline-block;
193}
194
195.msl_current_line {
196 background-color: #FFFF00;
197 font-weight: bold;
198}
199.msl_current_line:hover {
200 opacity : 0.8;
201}
202.msl_fatal {
203 background-color: #FF0000;
204 font-weight: bold;
205}
206.msl_err {
207 background-color: #FF8800;
208 font-weight: bold;
209}
210.msl_msg {
211 background-color: #11FF11;
212 font-weight: bold;
213}
214.msl_current_cmd {
215 background-color: #FFFF00;
216 font-weight: bold;
217}
218.info_column{
219 overflow:auto;
220 /*resize: horizontal;*/
221 align-items: start;
222 padding-left: 5px;
223}
224.infotable {
225 width: 100%;
226 border-spacing: 0px;
227 border: 1px solid black;
228 border-radius: 5px;
229 text-align: left;
230 padding: 0px;
231 float: right;
232 line-height: 30px; /* since modbvalue forces resize */
233}
234.infotable th{
235 white-space: nowrap;
236}
237.infotable tr:first-child th:first-child,
238.infotable tr:first-child td:first-child {
239 border-top-left-radius: 5px;
240}
241.infotable tr:first-child th:last-child,
242.infotable tr:first-child td:last-child {
243 border-top-right-radius: 5px;
244}
245.infotable tr:last-child td:first-child {
246 border-bottom-left-radius: 5px;
247}
248.infotable tr:last-child td:last-child {
249 border-bottom-right-radius: 5px;
250}
251.waitlooptxt {
252 z-index: 33;
253 position: absolute;
254 top: 1px;
255 left: 5px;
256 display: inline-block;
257}
258.waitloopbar {
259 z-index: 32;
260 width: calc(100% - 2px);
261 height: 100%;
262 text-align: left;
263}
264
265/* Dropdown button styles*/
266.dropmenu {
267 background-color: Transparent;
268 font-size: 100%;
269 font-family: verdana,tahoma,sans-serif;
270 border: none;
271 cursor: pointer;
272 text-align: left;
273 padding: 5px;
274 line-height: 1;
275 color:#404040;
276}
277.dropmenu:hover {
278 background-color: #C0D0D0;
279}
280
281/* Style the dropdown content (hidden by default) */
282.dropdown {
283 position: relative;
284 display: inline-block;
285}
286.dropdown-content {
287 display: none;
288 position: absolute;
289 background-color: #f9f9f9;
290 min-width: 150px;
291 box-shadow: 0 8px 16px rgba(0,0,0,0.2);
292 z-index: 10;
293 font-size: 90%;
294 color: black;
295 white-space: nowrap;
296 cursor: pointer;
297}
298.dropdown-content a{
299 display: block;
300 padding: 4px 8px;
301}
302.dropdown-content div{
303 display: flex;
304 justify-content: space-between;
305 align-items: center;
306 padding: 4px 8px;
307}
308.dropdown-content a:hover {
309 background-color: #C0D0D0;
310}
311.dropdown-content div:hover {
312 background-color: #C0D0D0;
313}
314.dropdown:hover .dropdown-content {
315 display: block;
316}
317
318 /* Style the tab */
319.etab {
320 position: relative;
321 top: 2px;
322 z-index: 1;
323 overflow-x: auto;
324 white-space: nowrap;
325 width: 100%;
326 display: block;
327 -webkit-overflow-scrolling: touch;
328 border: none;
329}
330.etab button {
331 background-color: #D0D0D0;
332 float: left;
333 margin: 4px 2px 0px 2px;
334 border-top-left-radius: 10px;
335 border-top-right-radius: 10px;
336 border: none;
337 border-top: 1px solid Transparent;
338 border-right: 1px solid Transparent;
339 border-left: 1px solid Transparent;
340 cursor: pointer;
341 padding: 3px 5px 3px 5px;
342}
343.etab button:hover {
344 background-color: #FFFFFF;
345 border-bottom: 5px solid #FFFFFF;
346}
347.etab button.edt_active:hover {
348 background-color: #FFFFFF;
349 border-bottom: 5px solid #FFFFFF;
350}
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;
357 color: black;
358}
359.etabcontent {
360 display: none;
361 padding: 0;
362 border-top: none;
363 height: 100%;
364}
365.editbtn{
366 background-color: #f0f0f0;
367 border: 1px solid black;
368 border-radius: 3px;
369 cursor: pointer;
370 padding: 0px 2px 0px 2px;
371 margin-left: 3px;
372}
373.editbtn:hover{
374 background-color: #C0D0D0;
375}
376.closebtn {
377 font-family: verdana,tahoma,sans-serif;
378 border: none;
379 border-radius: 3px;
380 cursor: pointer;
381 padding: 2px;
382 margin-left: 5px;
383}
384.closebtn:hover {
385 background-color: #FD5E59;
386}
387
388.progress {
389 width: 200px;
390 font-size: 0.7em;
391}
392.dlgProgress {
393 border: 1px solid black;
394 box-shadow: 6px 6px 10px 4px rgba(0,0,0,0.2);
395 border-radius: 6px;
396 display: none;
397}
398
399.empty-dragrow td{
400 border: 2px dashed #6bb28c;
401 background-color: white;
402}
403tr.dragstart td{
404 background-color: gray;
405 color: white;
406}
407tr.dragrow td{
408 overflow: hidden;
409 text-overflow: ellipsis;
410 max-width: calc(10em - 30px);
411}
412#nextFNContainer img {
413 cursor: pointer;
414 height: 1em;
415 vertical-align: center;
416}
417`;
418
419// Implement colors and styles from KeywordsGroups in CSS
420for (const group in mslDefs.groups) {
421 const { mslclass, color, fontWeight } = mslDefs.groups[group];
422
423 if (mslclass) {
424 if (fontWeight) {
425 seq_css += `.${mslclass} { color: ${color}; font-weight: ${fontWeight}; }\n`;
426 } else {
427 seq_css += `.${mslclass} { color: ${color};}\n`;
428 }
429 }
430}
431
432const seqStyle = document.createElement('style');
433seqStyle.textContent = seq_css;
434document.head.appendChild(seqStyle);
435
436// line connector string
437const lc = "\n";
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";
449
450// -- Sequencer specific functions --
451// Setup the correct sequencer state visually
452function seqState(funcCall) {
453 /* Arguments:
454 funcCall - (optional) a function to be called when the state is set (with the state text)
455 */
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"
469 } else {
470 stateText = "Stopped";
471 }
472
473 if (funcCall) {
474 funcCall(stateText);
475 }
476 return stateText;
477 }).catch (function (error) {
478 console.error(error);
479 return "Unknown";
480 });
481}
482
483// Ask user to edit current sequence
484function askToEdit(flag,event) {
485 if (flag) {
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();
493 return;
494 }
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) {
497 if (resp) {
498 const label = editorElements()[3];
499 addETab(document.getElementById("addETab"));
500 seqOpen(label.title.split("\n")[0]);
501 }
502 });
503}
504
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);
513 // Change in text
514 editor.addEventListener("input", function() {
515 seqIsChanged(true);
516 });
517 document.addEventListener("selectionchange", function(event) {
518 if (event.target.activeElement === editor) markCurrLineNum();
519 });
520 // Synchronize the scroll position of lineNumbers with editor
521 editor.addEventListener("scroll", function() {
522 lineNumbers.scrollTop = editor.scrollTop;
523 });
524 if (isChrome === 1) {
525 editor.addEventListener("keydown", arrowKeysChrome);
526 }
527}
528
529// apply changes of filename in the ODB (triggers reload)
530function seqChange(filename) {
531 /* Arguments:
532 filename - full file name with path to change
533 */
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) {
544 return;
545 }
546 }).catch(function (error) {console.error(error);});
547 } else {
548 dlgAlert("Something went wrong, I could not set the filename!");
549 }
550 }).catch(function (error) {console.error(error);});
551}
552
553// Save sequence text in filename.
554function seqSave(filename) {
555 /* Arguments:
556 filename (opt) - save to provided filename with path. If undefined save to original
557 filename and if empty trigger file_picker.
558 */
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);
567 seqIsChanged(false);
568 } else {
569 seqSave("");
570 }
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
576 if (path === "")
577 path = sessionStorage.getItem("pathName") ? sessionStorage.getItem("pathName") : mslDefs.filename.path + "/";
578 if (file === "")
579 file = sessionStorage.getItem("fileName") ? sessionStorage.getItem("fileName") : "filename.msl";
580 file_picker(path,mslDefs.filename.ext,seqSave,true,{},true);
581 } else {
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);
589 seqIsChanged(false);
590 }
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);
595 }
596 return;
597 }
598}
599
600// Open filename
601function seqOpen(filename) {
602 /* Arguments:
603 filename - file name to open (empty trigger file_picker)
604 */
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();
614 }
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") {
622 seqChange(filename);
623 } else {
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
628 seqIsChanged(false);
629 // save first state
630 saveState(text,editor);
631 });
632 }
633 return;
634 } else {
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);
642 });
643}
644}
645
646// Show/hide buttons according to sequencer state
647function updateBtns(state) {
648 /* Arguments:
649 state - the state of the sequencer
650 */
651 const seqState = {
652 Running: {
653 color: "var(--mgreen)",
654 },
655 Stopped: {
656 color: "var(--mred)",
657 },
658 Editing: {
659 color: "var(--mred)",
660 },
661 Paused: {
662 color: "var(--myellow)",
663 },
664 }
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;});
669 // hide all buttons
670 const hideBtns = document.querySelectorAll('.seqbtn');
671 hideBtns.forEach(button => {
672 button.style.display = "none";
673 });
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";
679 } else {
680 button.style.display = "flex";
681 }
682 });
683 // Hide exclusive buttons
684 if (nclass === 1) {
685 const exclBtns = document.querySelectorAll('.seqbtn.Exclusive');
686 exclBtns.forEach(button => {
687 button.style.display = "none";
688 });
689 }
690 // Hide progress modal when stopped
691 const hideProgress = document.getElementById("Progress");
692 if (state === "Stopped" && hideProgress) hideProgress.style.display = "none";
693}
694
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];
703 if (hold) {
704 dlgMessage("Message", message, true, false,clrMessage);
705 } else {
706 dlgAlert(message);
707 }
708 }).catch (function (error) {
709 console.error(error);
710 });
711}
712
713// Clear sequencer messages
714function clrMessage() {
715 mjsonrpc_db_paste(["Sequencer/State/Message"], [""]).then(function (rpc) {
716 return;
717 }).catch(function (error) {
718 console.error(error);
719 });
720}
721
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");
727
728 // Calculate scrollbar width
729 const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
730
731 // Visible width of the window accounting for scrollbar
732 let winWidth = Math.max(document.documentElement.clientWidth, window.innerWidth) - scrollbarWidth;
733
734 // Visible height of the window
735 let winHeight = Math.max(document.documentElement.clientHeight, window.innerHeight);
736
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";
740
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;
746
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;
750}
751
752// Load the current sequence from ODB (only on main tab)
753function load_msl() {
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("//","/");
763 // syntax highlight
764 if (seqLines) {
765 editor.innerHTML = syntax_msl(seqLines).join(lc);
766 updateLineNumbers(editor.previousElementSibling,editor);
767 if (seqState) hlLine(currLine);
768 }
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);
781 });
782}
783
784// Highlight (background color) and scroll to current line
785function hlLine(lineNums,color,editor,msgs = []) {
786 /* Arguments:
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
791 */
792
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"));
796
797 // If single value make an array
798 lineNums = Array.isArray(lineNums) ? lineNums : [lineNums];
799 msgs = Array.isArray(msgs) ? msgs : [msgs];
800 let counter = 0;
801 lineNums.forEach(lineNum => {
802 const lineId = "sline" + lineNum;
803 const lineHTML = (editor) ? editor.querySelector(`#${lineId}`) : document.getElementById(lineId);
804
805 if (lineHTML) {
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" });
813 }
814 }
815 counter++;
816 });
817}
818
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");
824 if (currLine) {
825 currLine.scrollIntoView({ block: "center" });
826 }
827 } else {
828 localStorage.removeItem("scrollToCurr",true);
829 }
830}
831
832// Open files in new tabs
833function toggleCheck(e) {
834 if (e.checked) {
835 localStorage.setItem(e.id,true);
836 } else {
837 localStorage.removeItem(e.id);
838 }
839}
840
841// shortcut event handling to overtake default behaviour
842function shortCutEvent(event) {
843 const parEditor = editorElements();
844 const notFirstTab = (parEditor[1].id !== "editorTab1");
845 if (notFirstTab) {
846 // Check these only for editors
847 if (event.altKey && event.key === 's') {
848 event.preventDefault();
849 //save as with file_picker
850 seqSave('');
851 event.preventDefault();
852 } else if ((event.ctrlKey || event.metaKey) && event.key === 's') {
853 event.preventDefault();
854 //save to the same filename
855 seqSave();
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();
865 }
866 }
867 if (!notFirstTab) {
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();
875 }
876 }
877 // Check these for all tabs
878 if (event.altKey && event.key === 'n') {
879 // open new tab
880 event.preventDefault();
881 addETab(document.getElementById("addETab"));
882 event.preventDefault();
883 } else if (event.altKey && event.key === 'o') {
884 event.preventDefault();
885 seqOpen();
886 event.preventDefault();
887 }
888
889}
890
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'
897 ) {
898
899 const e = event.target;
900 let caretPos = getCurrentCursorPosition(e);
901 let currText = e.innerText;
902 // Indentation keywords
903 const defIndent = mslDefs.defIndent;
904
905 // save current revision for undo
906 if (event.key === ' ' || event.key === 'Backspace' || event.key === 'Delete' || event.key === 'Enter') {
907 saveState(currText,e);
908 seqIsChanged(true);
909 }
910
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 : "";
919 let indentLevel = 0;
920 let preSpace = 0;
921 let indentString = "";
922 if (plineText) {
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
933 */
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;
946 }
947 event.preventDefault();
948 }
949 e.innerHTML = syntax_msl(e.innerText).join(lc).slice(0,-1);
950 setCurrentCursorPosition(e, caretPos);
951 updateLineNumbers(e.previousElementSibling,e);
952 e.focus();
953 }
954 return;
955}
956
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;
981
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;
989 } else {
990 // If the target line is shorter than the caret's position in the current line
991 ncaretPos = nlineStart + nlength;
992 }
993 setCurrentCursorPosition(e, ncaretPos);
994 }
995 }
996}
997
998
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)) {
1004 MetaCombo = true;
1005 } else {
1006 MetaCombo = false;
1007 }
1008
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);
1018 }
1019 let caretPos = getCurrentCursorPosition(e);
1020 let currText = e.innerText;
1021
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') {
1033 if (textSelected) {
1034 currText = currText.substring(0,caretPos) + currText.substring(caretPos).slice(0);
1035 } else {
1036 currText = currText.substring(0,caretPos) + currText.substring(caretPos).slice(1);
1037 }
1038 }
1039 e.innerHTML = syntax_msl(currText).join(lc).slice(0,-1);
1040 setCurrentCursorPosition(e, caretPos);
1041 keepCaretVisible(e);
1042 return;
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') {
1050 indent_msl(lines);
1051 }
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);
1056 return;
1057 }
1058}
1059
1060// Trigger syntax highlighting when you paste text
1061function checkSyntaxEventPaste(event) {
1062 // set time out to allow default to go first
1063 setTimeout(() => {
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);
1074 e.focus();
1075 }, 0);
1076 return;
1077}
1078
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);
1084 if (!pos) return;
1085 let lineNum = e.innerText.substring(0,pos).split("\n").length + offset;
1086 let sline = e.querySelector("#sline" + lineNum.toString());
1087 return sline;
1088}
1089
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.
1094*/
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;
1100 if (singleLine) {
1101 const line = whichLine(e);
1102 if (line) {
1103 const startLine = parseInt(line.id.replace("sline",""));
1104 return [startLine,startLine];
1105 } else {
1106 return [0,0];
1107 }
1108 }
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];
1117 } else {
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;
1122 }
1123 let startID = (startNode && startNode.id) ? startNode.id : "";
1124 let endID = (endNode && endNode.id) ? endNode.id : "";
1125 // get first line
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 : "";
1129 }
1130 // get last line
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 : "";
1134 }
1135 let startLine = (startNode && startNode.id) ? parseInt(startNode.id.replace("sline","")) : 0;
1136 let endLine = (endNode && endNode.id) ? parseInt(endNode.id.replace("sline","")) : 0;
1137 if (singleLine) {
1138 startLine = endLine = Math.min(startLine, endLine);
1139 }
1140 return [startLine,endLine];
1141}
1142
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;
1152}
1153
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);
1165 return -1;
1166 } else {
1167 pos = pos - node.length;
1168 }
1169 } else {
1170 pos = setCurrentCursorPosition(node, pos);
1171 if (pos < 0) {
1172 return pos;
1173 }
1174 }
1175 }
1176 return pos;
1177}
1178
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);
1195 }
1196 lineNumbers.scrollTop = editor.scrollTop;
1197 windowResize();
1198}
1199
1200// Utility function to escape special characters in a string for use in a regular expression
1201function escapeRegExp(s) {
1202 if (!s) return "";
1203 return s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
1204}
1205
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) {
1210 // take msl default
1211 keywordGroups = mslDefs.groups;
1212 }
1213
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
1222 // Strings
1223 let reg = /(["'])(.*?)\1/g;
1224 mslText = mslText.replace(reg,'<span class="msl_string">$1$2$1</span>');
1225
1226 // Comments
1227 //reg = /^(COMMENT|#.*?)(.*)$/gim;
1228 //mslText = mslText.replace(reg,'<span class="msl_comment">$&</span>');
1229
1230 // Variables
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>');
1236
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>");
1240
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>");
1244
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>");
1248
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>");
1252
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>");
1256
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>");
1260
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>");
1264
1265 // Units group
1266 reg = new RegExp("\\b(" + keywordGroups.units.keywords.join("|") + ")\\b", "gi");
1267 mslText = mslText.replace(reg, "<span class='msl_units'>$1</span>");
1268
1269 // Action group
1270 reg = new RegExp("\\b(" + keywordGroups.actions.keywords.join("|") + ")\\b(\\s*)$", "gim");
1271 mslText = mslText.replace(reg, "<span class='msl_actions'>$1</span>$2");
1272
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>');
1279
1280 // Break lines and handle one by one
1281 seqLines = mslText.split("\n");
1282
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>`;
1298 }
1299
1300 // empty class is needed for cursor movement in Firefox
1301 // for Chrome empty lines are skipped with arrow up??
1302 if (line === "") {
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>";
1307 } else {
1308 seqLines[j] = "<span class='" + emptyClass + "' id='sline" + (j+1).toString() + "'></span>";
1309 }
1310 } else {
1311 seqLines[j] = "<span class='sline' id='sline" + (j+1).toString() + "'>" + seqLines[j] + "</span>";
1312 }
1313 }
1314 return seqLines;
1315}
1316
1317// Adjust indentation of a selection of lines
1318function indent_msl(lines,addTab) {
1319 /* Arguments:
1320 lines - an array of two elements, first and last line numbers
1321 addTab - (opt) +/-1 to add/subtract three spaces to selected lines
1322 */
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];
1332 singleLine = true;
1333 }
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);
1338 let line = "";
1339 if (lineEl) line = lineEl.innerText;
1340 if (addTab === 1) {
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()));
1350 if (indentMinos) {
1351 indentLevel = -1;
1352 } else if (indentPlus) {
1353 indentLevel = 1;
1354 }
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();
1359 } else {
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();
1365 }
1366 const indentPlus = defIndent.indentplus.some(keyword => line.trim().toLowerCase().startsWith(keyword.toLowerCase()));
1367 if (indentPlus) indentLevel++;
1368 }
1369 }
1370}
1371
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) {
1375 /* Arguments:
1376 id - ID of div to fill with the table of variables
1377 odbTreeVar - values of /Sequencer/Variables
1378 */
1379
1380 let e = document.getElementById(id);
1381 if (e === null) {
1382 dlgAlert("Container ID was not give.");
1383 return;
1384 }
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
1391 e.innerHTML = "";
1392 return;
1393 }
1394
1395 let html = "<table id='varTable' class='mtable infotable'>\n";
1396 html += "<tr><th style='min-width: 120px'>Variable&nbsp;&nbsp;</th><th>Current value&nbsp;&nbsp;</th></tr>\n";
1397
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$/);
1402 if (match) {
1403 nVars++;
1404 const name = match[1];
1405 const value = odbTreeVar[name];
1406 let isBool = (typeof(value) === "boolean");
1407 if (isBool) {
1408 html += `<tr><td>${name}</td><td><input type="checkbox" class="modbcheckbox" data-odb-path="/Sequencer/Variables/${name}"></span></td></tr>\n`;
1409 } else {
1410 html += `<tr><td>${name}</td><td><span class="modbvalue" data-odb-path="/Sequencer/Variables/${name}"></span></td></tr>\n`;
1411 }
1412 }
1413 }
1414 html += "</table>";
1415 if (nVars !== nVarsOld) {
1416 e.innerHTML = html;
1417 }
1418 return;
1419}
1420
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) {
1424 /* Arguments:
1425 odbTree... - Objects of ODB values
1426 */
1427 let html = "";
1428 let odbDefPaths = [];
1429 let odbDefValues = [];
1430 let NValues = 0;
1431
1432 html += "<table id='paramTable' class='mtable infotable'>";
1433 html += "<tr><th>Parameter&nbsp;&nbsp;</th><th>Initial value&nbsp;&nbsp;</th><th>Comment</th></tr>";
1434
1435 const processParam = (name, value, isBool, defValue, optValue, comment) => {
1436 let parLine = `<tr><td>${name}</td>`;
1437 let inParLine = "";
1438 if (optValue) {
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>`;
1445 } else {
1446 inParLine += `<span class="modbvalue" data-odb-path="/Sequencer/Param/Value/${name}" data-odb-editable="1" data-input="1"></span>`;
1447 }
1448 if (defValue !== undefined) {
1449 NValues++;
1450 odbDefPaths.push(`/Sequencer/Param/Value/${name}`);
1451 odbDefValues.push(defValue);
1452 }
1453
1454 parLine += `<td>${inParLine}</td>`;
1455 parLine += `<td>${comment}</td></tr>`;
1456 html += parLine;
1457 };
1458
1459 // Go over all parameters in ODB
1460 for (let key in odbTreeV) {
1461 const match = key.match(/([^/]+)\/name$/);
1462 if (match) {
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);
1472 }
1473 }
1474 }
1475
1476 // Go over Edit on sequence links
1477 for (let key in editOnSeq) {
1478 const match = key.match(/([^/]+)\/name$/);
1479 if (match) {
1480 const name = match[1];
1481 const value = editOnSeq[name];
1482 let isBool = (typeof(value) === "boolean");
1483 if (isBool) {
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`;
1485 } else {
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`;
1487 }
1488 }
1489 }
1490
1491 html += "</table>";
1492 return {
1493 html: html,
1494 paths: odbDefPaths,
1495 values: odbDefValues,
1496 NValues: NValues
1497 };
1498}
1499
1500
1501// Prepare the parameters/variables (if present) from the ODB as a table
1502function dlgParam(debugFlag) {
1503 /* Arguments:
1504 debugFlag - (opt) true/false run in debug/normal mode
1505 */
1506
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)) {
1512 if (debugFlag) {
1513 modbset('/Sequencer/Command/Debug script',true);
1514 } else {
1515 modbset('/Sequencer/Command/Start script',true);
1516 }
1517 return;
1518 }
1519
1520 let odbTreeV = null;
1521 let odbTreeC = null;
1522 let odbTreeD = null;
1523 let odbTreeO = null;
1524 let odbTreeVar = null;
1525
1526 if (odbTree) {
1527 odbTreeV = odbTree.value;
1528 odbTreeC = odbTree.comment;
1529 odbTreeD = odbTree.defaults;
1530 odbTreeO = odbTree.options;
1531 odbTreeVar = JSON.parse(sessionStorage.getItem('variables'));
1532 }
1533
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];
1543 }
1544 }
1545 return null;
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
1553 });
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 () {
1565 d.remove();
1566 if (debugFlag) {
1567 modbset('/Sequencer/Command/Debug script',true);
1568 } else {
1569 modbset('/Sequencer/Command/Start script',true);
1570 }
1571 });
1572
1573 // refresh immediately modbvalue elements
1574 mhttpd_refresh();
1575 } else {
1576 dlgAlert("Something went wrong. Please try again!");
1577 }
1578 }).catch(function (error) {
1579 console.error(error);
1580 });
1581 } else {
1582 let message = `ODB "${valuesLinkODB}" was not found.<br>Cannot start sequence!`;
1583 dlgAlert(message);
1584 }
1585 }).catch(function (error) {
1586 console.error(error);
1587 });
1588}
1589
1590
1591// helper debug function
1592function debugSeq(parContainer) {
1593 startSeq(parContainer,true);
1594}
1595
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;
1602 if (!filename) {
1603 dlgAlert("Please give the file a name first (Save as).");
1604 return;
1605 }
1606 const message = debugFlag ? `Save and debug ${filename}?` : `Save and start ${filename}?`;
1607 dlgConfirm(message,function(resp) {
1608 if (resp) {
1609 seqSave(filename);
1610 openETab(document.getElementById("etab1-btn"));
1611 seqOpen(filename);
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);
1617 }
1618 });
1619 }
1620 });
1621 } else {
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);
1627 }
1628 });
1629 }
1630}
1631
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) {
1640 if (resp) {
1641 let order = chngNextFilename(e,filename);
1642 if (order !== -1 && order !== undefined) {
1643 seqSave(filename);
1644 dlgAlert(`File saved and placed in position ${order} in the queue.`);
1645 }
1646 }
1647 });
1648}
1649
1650// helper stop function
1651function stopSeq() {
1652 const message = `Are you sure you want to stop the sequence?`;
1653 dlgConfirm(message,function(resp) {
1654 if (resp) {
1655 modbset('/Sequencer/Command/Stop immediately',true);
1656 }
1657 });
1658}
1659
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");
1666 if (vis) {
1667 e.style.display = "flex";
1668 e.parentElement.style.width = "285px";
1669 } else {
1670 e.style.display = "none";
1671 if (visNF) {
1672 e.parentElement.style.width = "0px";
1673 }
1674 }
1675 windowResize();
1676}
1677
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;
1683 let html = "";
1684 let addFileRow = "";
1685 let nFiles = 0;
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>`;
1692 nFiles++;
1693 }
1694 }
1695 if (vis && html !== "") {
1696 e.style.display = "flex";
1697 e.parentElement.style.width = "285px";
1698 if (nFiles < 10) {
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>`;
1701 } else {
1702 disableMenuItems("SetAsNext",true);
1703 }
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>`;
1705 windowResize();
1706 } else {
1707 e.style.display = "none";
1708 if (visVar) {
1709 e.parentElement.style.width = "0px";
1710 windowResize();
1711 }
1712 }
1713 activateDragDrop(e);
1714 }).catch (function (error) {
1715 console.error(error);
1716 });
1717}
1718
1719// Show extra rows for wait and loop
1720function extraRows(e) {
1721 /* Arguments:
1722 e - triggering element to identify wait or loop
1723 */
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)
1729 if (e.value) {
1730 showProgress();
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());
1736 // Insert a new row
1737 let tr = table.insertRow(rIndex+1);
1738 tr.className = "waitTR";
1739 tr.innerHTML = `<td style="position: relative;width:80%;" colspan="2">&nbsp;
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>
1743 </span>
1744 </td>`;
1745 windowResize();
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">&nbsp;
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>]
1757 </span>
1758 </td>`;
1759 windowResize();
1760 }
1761 }).catch (function (error) {
1762 console.error(error);
1763 });
1764 }
1765 } else {
1766 // remove rows
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);
1771 windowResize();
1772 }
1773}
1774
1775// Helper function to identify browser, 1 FF, 2 Chrome, 3, other
1776function browserType() {
1777 if (navigator.userAgent.indexOf("Chrome") !== -1) {
1778 return 1;
1779 } else if (navigator.userAgent.indexOf("Firefox") !== -1) {
1780 return 2;
1781 }
1782
1783 return 1;
1784}
1785
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);
1791 if (filechanged) {
1792 if (flag) {
1793 filechanged.innerHTML = "&nbsp;&#9998;";
1794 } else if (flag === undefined) {
1795 // true if text has changed false if not
1796 return (filechanged.innerHTML !== "");
1797 } else {
1798 filechanged.innerHTML = "";
1799 }
1800 }
1801 // also change validation icon to defaule
1802 let vldBtn = document.getElementById("vldBtn");
1803 vldBtn.src = "icons/validate-syntax.svg";
1804 vldBtn.style.backgroundColor = "";
1805}
1806
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) {
1812 return;
1813 } else if (saveRevision[editorId] === undefined){
1814 saveRevision[editorId] = true;
1815 }
1816
1817 if (!previousRevisions[editorId]) {
1818 previousRevisions[editorId] = [];
1819 revisionIndex[editorId] = -1;
1820 }
1821
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);
1826 }
1827 // Push new revision and keep only nRevisions revisions
1828 previousRevisions[editorId].push(mslText)
1829 if (previousRevisions[editorId].length > nRevisions) {
1830 previousRevisions[editorId].shift();
1831 }
1832 revisionIndex[editorId] = previousRevisions[editorId].length - 1;
1833}
1834
1835// undo last edit
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);
1843 return;
1844 } else {
1845 // enable menu item
1846 disableMenuItems("undoMenu",false);
1847 revisionIndex[editorId]--;
1848 }
1849 // reset redo
1850 disableMenuItems("redoMenu",false);
1851
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);
1861}
1862
1863// redo the undo
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);
1870 return;
1871 } else {
1872 // enable menu item
1873 disableMenuItems("redoMenu",false);
1874 revisionIndex[editorId]++;
1875 }
1876 // reset undo
1877 disableMenuItems("undoMenu",false);
1878 seqIsChanged(true);
1879
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);
1889}
1890
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) {
1904 startLine++;
1905 startElementId = '#sline' + startLine;
1906 startElement = e.querySelector(startElementId).firstChild;
1907 }
1908 while (endElement === null && endLine > 0) {
1909 endLine--;
1910 endElementId = '#sline' + endLine;
1911 endElement = e.querySelector(endElementId).lastChild;
1912 }
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);
1921 }
1922}
1923
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";
1933 });
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");
1942 } else {
1943 localStorage.removeItem("darkMode");
1944 edt_areas.forEach(area => {
1945 area.style.backgroundColor = "white";
1946 area.style.color = "black";
1947 });
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");
1956 }
1957}
1958
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;
1968 return;
1969 }
1970 }
1971 }
1972}
1973
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");
1979 if (e.checked) {
1980 localStorage.setItem("showProgress",true);
1981 progressDiv.style.display = "block";
1982 //dlgShow(progressDiv);
1983 } else {
1984 localStorage.removeItem("showProgress");
1985 progressDiv.style.display = "none";
1986 //dlgHide(progressDiv);
1987 }
1988}
1989
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);
2000 if (lineNum)
2001 lineNum.className = "edt_linenum_curr";
2002 }
2003}
2004
2005
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);
2016 let message = "";
2017 if (isDefined) {
2018 message = `${progName} program is not running.<br>Should I start it?`
2019 } else {
2020 message = `${progName} program is not configured and not running.<br>Should I try to start it anyway?`
2021 }
2022 dlgConfirm(message,function(resp) {
2023 // Guess the name of the actual binary
2024 let binName = (progName === "Sequencer") ? "msequencer" : progName.toLowerCase();
2025 if (resp) {
2026 if (!isDefined) {
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);
2034 }
2035 }).catch(function (error) {
2036 console.error(error);
2037 });
2038 }, 2000);
2039 }).catch(function (error) {
2040 console.error(error);
2041 });
2042 } else {
2043 mjsonrpc_start_program(progName);
2044 }
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.`);
2050 } else {
2051 dlgAlert(`Failed to start ${progName}!<br>Try to start it manually (${binName} -D)`);
2052 }
2053 });
2054 }, 3000);
2055 }
2056 });
2057 }).catch (function (error) {
2058 console.error(error);
2059 });
2060 }).catch(function (error) {
2061 console.error(error);
2062 });
2063}
2064
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) {
2071 const rangeData = {
2072 startPath: range.startContainer,
2073 startOffset: range.startOffset,
2074 endPath: range.endContainer,
2075 endOffset: range.endOffset
2076 };
2077 sessionStorage.setItem("tempSelText", text);
2078 sessionStorage.setItem("tempSelRange", JSON.stringify(rangeData));
2079 }
2080 }
2081}
2082
2083function editMenu(action) {
2084 let text = sessionStorage.getItem("tempSelText") ?? "";
2085 let storedRange = sessionStorage.getItem("tempSelRange") ?? "";
2086 /*
2087 if (storedRange) {
2088 let rangeData = JSON.parse(storedRange);
2089 let startContainer = nodeFromPath(rangeData.startPath);
2090 let endContainer = nodeFromPath(rangeData.endPath);
2091
2092 // Create a new range
2093 let newRange = new Range();
2094 newRange.setStart(startContainer, rangeData.startOffset);
2095 newRange.setEnd(endContainer, rangeData.endOffset);
2096
2097 // Select the new range
2098 let selection = window.getSelection();
2099 selection.removeAllRanges();
2100 selection.addRange(newRange);
2101 }
2102 */
2103 if (action === "Copy") {
2104 if (text) {
2105 sessionStorage.setItem("copiedText",text);
2106 }
2107 } else if (action === "Paste") {
2108 const copiedText = sessionStorage.getItem("copiedText");
2109 if (copiedText) {
2110 newRange.deleteContents();
2111 newRange.insertNode(document.createTextNode(copiedText));
2112 }
2113 } else if (action === "Cut") {
2114 if (text) {
2115 sessionStorage.setItem("copyText",text);
2116 //document.execCommand("cut");
2117 newRange.deleteContents();
2118 }
2119 } else if (action === "Undo") {
2120 undoEdit();
2121 } else if (action === "Redo") {
2122 redoEdit();
2123 }
2124}
2125
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";
2134 });
2135 tablinks.forEach(link => {
2136 link.classList.remove("edt_active");
2137 });
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);
2144 } else {
2145 disableMenuItems("noteditor",false);
2146 }
2147 // Change validation icon to defaule
2148 let vldBtn = document.getElementById("vldBtn");
2149 vldBtn.src = "icons/validate-syntax.svg";
2150 vldBtn.style.backgroundColor = "";
2151
2152 // Adjust height of active editor
2153 windowResize();
2154}
2155
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) {
2165 if (resp) {
2166 tabBtn.remove();
2167 tabContent.remove();
2168 // switch to previous tab
2169 openETab(document.getElementById(`etab${(tabCount-1)}-btn`));
2170 }
2171 });
2172 } else {
2173 tabBtn.remove();
2174 tabContent.remove();
2175 // switch to previous tab
2176 openETab(document.getElementById(`etab${(tabCount-1)}-btn`));
2177 }
2178 // need this since the close button is inside the tab button
2179 event.stopPropagation();
2180}
2181
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">&times;</span>`;
2190 btn.parentNode.insertBefore(tabBtn,btn);
2191 tabBtn.onclick = function () { openETab(this);};
2192
2193 // Create editor area
2194 const tabContent = document.createElement("div");
2195 tabContent.id = "etab" + tabCount;
2196 tabContent.className = "etabcontent";
2197 let makeDark = "";
2198 if (localStorage.getItem("darkMode")) makeDark = "style='background-color: black; color: white;'";
2199 const html =
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);
2204 tabBtn.click();
2205 windowResize();
2206 // Add event listeners
2207 editorEventListeners();
2208}
2209
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];
2217}
2218
2219// disable and enable clicking on menu item
2220function disableMenuItems(className,flag) {
2221 /* Arguments:
2222 className - the class name of the item
2223 flag - true/false to enable/disable item
2224 */
2225 const els = document.querySelectorAll(`.${className}`);
2226 els.forEach(e => {
2227 if (flag) {
2228 e.style.opacity = 0.5;
2229 e.style.pointerEvents = "none";
2230 } else {
2231 e.style.opacity = "";
2232 e.style.pointerEvents = "";
2233 }
2234 });
2235}
2236
2237// Function to replace some special characters in the text
2238function escapeSpecialCharacters(text) {
2239 return text.replace(/&/g, "&amp;")
2240 .replace(/</g, "&lt;")
2241 .replace(/>/g, "&gt;")
2242 .replace(/\r\n|\n\r/g, '\n')
2243 //.replace(/"/g, "&quot;")
2244 //.replace(/'/g, "&#39;")
2245 .replace(/\t/g, " ");
2246}
2247
2248// Produce a help window
2249function dlgHelp() {
2250 let help = `
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>
2263</ul>
2264</span>
2265`;
2266 const d = dlgMessage("Editor help",help, false, false);
2267 const btn = d.querySelectorAll('.dlgButton')[0];
2268 btn.className = "dlgButtonDefault";
2269 btn.focus();
2270
2271}
2272
2273// Activate drag and drop events on next files table
2274function activateDragDrop(table) {
2275 /* Arguments:
2276 table - The table element containing the list of next files.
2277 */
2278 // collect all rows with class dragrow
2279 const rows = table.querySelectorAll('.dragrow');
2280 let dragStartIndex,dragEndIndex;
2281 let emptyRow;
2282 // add event listeners
2283 rows.forEach(row => {
2284 row.addEventListener('dragstart', dragStart);
2285 row.addEventListener('dragover', dragOver);
2286 row.addEventListener('dragend', dragEnd);
2287 });
2288
2289 function dragStart(e) {
2290 dragStartIndex = Array.from(rows).indexOf(this);
2291 rows.forEach(row => row.classList.remove('dragstart'));
2292 this.classList.add('dragstart');
2293 }
2294 function dragOver(e) {
2295 e.preventDefault();
2296 dragEndIndex = Array.from(rows).indexOf(this);
2297 // Create or update the empty row element
2298 if (!emptyRow) {
2299 emptyRow = document.createElement('tr');
2300 emptyRow.innerHTML = "<td colspan=4>&nbsp;</td>";
2301 emptyRow.classList.add('empty-dragrow');
2302 }
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);
2308 }
2309 }
2310 function dragEnd(e) {
2311 if (emptyRow) {
2312 emptyRow.remove();
2313 emptyRow = null;
2314 }
2315 reorderNextFilenames(dragStartIndex,dragEndIndex);
2316 rows.forEach(row => {
2317 row.classList.remove('dragstart');
2318 });
2319 }
2320}
2321
2322// Move next filename from dragStarIndex to dragEndIndex position
2323function reorderNextFilenames(dragStarIndex,dragEndIndex) {
2324 /* Arguments:
2325 dragStarIndex - move file from this indext.
2326 dragEndIndex - destination index of the file.
2327 */
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];
2333
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!.");
2341 return;
2342 }
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');
2349 }
2350 }).catch(function (error) {
2351 console.error(error);
2352 });
2353 }).catch (function (error) {
2354 console.error(error);
2355 });
2356}
2357
2358// Change the next file name in the clicked row
2359function chngNextFilename(e,filename) {
2360 /* Arguments:
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.
2364 */
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
2369 if (index > 9) {
2370 dlgAlert("Maximum number (10) of next files reached!");
2371 return -1;
2372 }
2373 index++;
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);
2378 if (filename) {
2379 odbpath = odbpath + "[" + index.toString() + "]";
2380 modbset(odbpath,filename);
2381 } else {
2382 file_picker(path,mslDefs.filename.ext,function(filename) {
2383 filename = filename.replace(/^sequencer\//,'').replace(/^\//,'');
2384 odbpath = odbpath + "[" + index.toString() + "]";
2385 if (filename) {
2386 modbset(odbpath,filename);
2387 }
2388 });
2389 }
2390 return index+1;
2391 }).catch(function (error) {
2392 mjsonrpc_error_alert(error);
2393 });
2394}
2395
2396// Reomve the next file name from the queue
2397function remNextFilename(e) {
2398 /* Arguments:
2399 e - cell element on the same row of "next file" to be removed
2400 */
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
2408 fList.push("");
2409 let paths = [];
2410 let values = [];
2411 let j = 0;
2412 for (let i = index+1; i < fList.length; i++) {
2413 paths[j] = odbpath + "[" + (i-1).toString() + "]";
2414 values[j] = fList[i];
2415 j++;
2416 }
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');
2422 }
2423 }).catch(function (error) {
2424 console.error(error);
2425 });
2426 }).catch (function (error) {
2427 console.error(error);
2428 });
2429}
2430
2431var NcheckValue = 0;
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) {
2435 /* Arguments:
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
2440 */
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);
2445 NcheckValue++;
2446 if (NcheckValue < 100) {
2447 // Wait 0.1 second and then call checkODBValue again
2448 // Time out after 10 s
2449 setTimeout(() => {
2450 checkODBValue(path,value,funcCall,args);
2451 }, 100);
2452 }
2453 } else {
2454 if (funcCall) funcCall(args);
2455 console.log("Value reached, proceeding...");
2456 // reset counter
2457 NcheckValue = 0;
2458 }
2459 }).catch(function(error) {
2460 console.error(error);
2461 });
2462}
2463
2464function isTextSelected(element) {
2465 const selection = window.getSelection();
2466 if (!selection.rangeCount) return false;
2467
2468 const range = selection.getRangeAt(0);
2469 const selectedText = selection.toString();
2470
2471 // Check if the selection is within the specified element
2472 if (!element.contains(range.commonAncestorContainer)) return false;
2473
2474 // Check if there is any selected text
2475 return selectedText.length > 0;
2476}
2477
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();
2486}
2487
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;
2498
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;
2502 }
2503
2504 // If the caret is near the top, scroll the container up
2505 if (caretTop < 0) {
2506 editor.scrollTop -= Math.abs(caretTop);
2507 }
2508}
2509
2510
2511// commands object for syntax validation
2512const mslCommands = {
2513 INCLUDE: {
2514 patterns: [/^INCLUDE\s+.+(\.msl)?$/i],
2515 description: "Include another MSL file.",
2516 args: 1,
2517 },
2518 BREAK: {
2519 patterns: [/^BREAK$/i],
2520 description: "Break (finish prematurely) a loop.",
2521 args: 0,
2522 },
2523 CALL: {
2524 patterns: [/^CALL\s+\w+(\s*,\s*.+)*$/i],
2525 description: "Call a subroutine with optional parameters.",
2526 args: ">=1",
2527 },
2528 CAT: {
2529 patterns: [/^CAT\s+\w+(\s*,\s*.+)*$/i],
2530 description: "Concatenate strings into a single variable.",
2531 args: ">=1",
2532 },
2533 COMMENT: {
2534 patterns: [/^(?:COMMENT\s+.*|#.*)$/i],
2535 description: "A comment line starting with COMMENT or #.",
2536 args: ">=1",
2537 },
2538 EXIT: {
2539 patterns: [/^EXIT$/i],
2540 description: "Exit the script immediately.",
2541 args: 0,
2542 },
2543 GOTO: {
2544 patterns: [/^GOTO\s+\d+$/i],
2545 description: "Jump to a specific line in the script.",
2546 args: 1,
2547 },
2548 IF: {
2549 patterns: [/^IF\s*\‍(.+\‍)$/i],
2550 description: "Conditional execution of code blocks.",
2551 blockStart: true, // Marks this as a block start command
2552 args: ">=1",
2553 },
2554 ELSE: {
2555 patterns: [/^ELSE$/i],
2556 description: "Alternative block for an IF statement.",
2557 args: 0,
2558 },
2559 ENDIF: {
2560 patterns: [/^ENDIF$/i],
2561 description: "Ends an IF block.",
2562 blockEnd: "IF", // Marks this as a block end command for IF
2563 args: 0,
2564 },
2565 LIBRARY: {
2566 patterns: [/^LIBRARY\s+.+$/i],
2567 description: "Declare the file as a library.",
2568 args: 1,
2569 },
2570 LOOP: {
2571 patterns: [
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
2575 ],
2576 description: "Execute a loop a fixed number of times or with values.",
2577 blockStart: true, // Marks this as a block start command
2578 args: ">=1",
2579 //argcond1: /^(\d+|INFINITE|\$\w+)$/, // First argument can be a number, INFINITE, or a variable
2580 },
2581 ENDLOOP: {
2582 patterns: [/^ENDLOOP$/i],
2583 description: "Ends a LOOP block.",
2584 blockEnd: "LOOP", // Marks this as a block end command for LOOP
2585 args: 0,
2586 },
2587 MESSAGE: {
2588 patterns: [/^MESSAGE\s+.+(\s*,\s*1)?$/i],
2589 description: "Display a message in the browser.",
2590 args: ">=1",
2591 },
2592 MSG: {
2593 patterns: [/^MSG\s+.+(\s*,\s*(ERROR|INFO|DEBUG|LOG|TALK))?$/i],
2594 description: "Send a message to the MIDAS buffer.",
2595 args: ">=1",
2596 },
2597 ODBCREATE: {
2598 patterns: [/^ODBCREATE\s+.+,\s+\w+(\s*,\s*\d+)?$/i],
2599 description: "Create an ODB key.",
2600 args: ">=2",
2601 },
2602 ODBDELETE: {
2603 patterns: [/^ODBDELETE\s+.+$/i],
2604 description: "Delete an ODB key or subdirectory.",
2605 args: 1,
2606 },
2607 ODBGET: {
2608 patterns: [/^ODBGET\s+".+",\s+\w+$/i],
2609 description: "Retrieve a value from the ODB.",
2610 args: 2,
2611 argcond: [/^\/.+$/, // First argument must start with /
2612 /^.+$/], // Second argument can be anything
2613 },
2614 ODBINC: {
2615 patterns: [/^ODBINC\s+.+(\s*,\s*[+-]?\d+)?$/i],
2616 description: "Increment an ODB value.",
2617 args: ">=1",
2618 },
2619 ODBLOOKUP: {
2620 patterns: [/^ODBLOOKUP\s+".+",\s+".+",\s+\w+$/i],
2621 description: "Lookup a string in the ODB array.",
2622 args: 3,
2623 },
2624 ODBSET: {
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.",
2628 args: ">=2",
2629 argcond: [
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
2633 ],
2634 },
2635 ODBLOAD: {
2636 patterns: [/^ODBLOAD\s+.+(\s*,\s*.+)?$/i],
2637 description: "Load an external file into the ODB.",
2638 args: ">=1",
2639 },
2640 ODBSAVE: {
2641 patterns: [/^ODBSAVE\s+.+,\s+.+$/i],
2642 description: "Save part of the ODB to a file.",
2643 args: 2,
2644 },
2645 ODBSUBDIR: {
2646 patterns: [/^ODBSUBDIR\s+("\/[^"]*"|\/[^,]+)$/i],
2647 description: "Set a base path for ODB commands.",
2648 blockStart: true, // Marks this as a block start command
2649 args: 1,
2650 },
2651 ENDODBSUBDIR: {
2652 patterns: [/^ENDODBSUBDIR$/i],
2653 description: "Ends an ODBSUBDIR block.",
2654 blockEnd: "ODBSUBDIR", // Marks this as a block end command for ODBSUBDIR
2655 args: 1,
2656 },
2657 PARAM: {
2658 patterns: [/^PARAM\s+\w+(\s*,\s*.+)*$/i],
2659 description: "Define script parameters.",
2660 args: ">=1",
2661 },
2662 RUNDESCRIPTION: {
2663 patterns: [/^RUNDESCRIPTION\s+.+$/i],
2664 description: "Set a run description.",
2665 args: 1,
2666 },
2667 SCRIPT: {
2668 patterns: [/^SCRIPT\s+.+(\s*,\s*.+)*$/i],
2669 description: "Call a server-side script with optional parameters.",
2670 args: ">=1",
2671 },
2672 SET: {
2673 patterns: [/^SET\s+\w+\s*,\s*.+$/i],
2674 description: "Set a variable to a value.",
2675 args: 2,
2676 },
2677 SUBROUTINE: {
2678 patterns: [/^SUBROUTINE\s+\w+$/i],
2679 description: "Start a subroutine block.",
2680 blockStart: true, // Marks this as a block start command
2681 args: 1,
2682 },
2683 ENDSUBROUTINE: {
2684 patterns: [/^ENDSUBROUTINE$/i],
2685 description: "Ends a subroutine block.",
2686 blockEnd: "SUBROUTINE", // Marks this as a block end command for SUBROUTINE
2687 args: 0,
2688 },
2689 TRANSITION: {
2690 patterns: [/^TRANSITION\s+(start|stop|pause|resume)$/i],
2691 description: "Trigger a run state transition.",
2692 args: 1,
2693 },
2694 WAIT: {
2695 patterns: [
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
2699 ],
2700 description: "Wait for a condition, time, or event.",
2701 args: ">=1",
2702 },
2703 EQUATION: {
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.",
2706 args: ">=1",
2707 },
2708};
2709
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;
2715
2716 // Validate the script content
2717 const errors = validateScript(editorContent,commands);
2718
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
2726 } else {
2727 return "Unknown error"; // Fallback for unexpected error formats
2728 }
2729 }).join("\n");
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));
2733 editor.focus();
2734
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)";
2740 } else {
2741 //dlgAlert("The syntax of the sequence is valid.");
2742 e.src = "icons/validate-syntax-green.svg";
2743 e.style.backgroundColor = "var(--mgreen)";
2744 }
2745}
2746
2747// Function to validate the script against commands object
2748function validateScript(script, commands = mslCommands) {
2749 const errors = [];
2750
2751 // Stack to track nested blocks
2752 const blockStack = [];
2753 const lineNums = [];
2754 // Stack to track delimiters
2755 const delimiterStack = [];
2756 const delimiterLineNums = [];
2757
2758 // Delimiters and their matching pairs
2759 const delimiters = {
2760 '{': '}',
2761 '[': ']',
2762 '(': ')',
2763 '"': '"',
2764 "'": "'",
2765 };
2766
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");
2770
2771 lines.forEach((line, index) => {
2772 const trimmedLine = line.trim();
2773 if (!trimmedLine) return; // Skip empty lines
2774
2775 let isValid = false;
2776
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));
2783
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);
2788 }
2789
2790 // Handle block end commands
2791 if (isValid && cmdData.blockEnd) {
2792 const expectedBlock = cmdData.blockEnd;
2793 const lastBlock = blockStack.pop();
2794 lineNums.pop();
2795
2796 if (lastBlock !== expectedBlock) {
2797 errors.push({ line: index + 1, message: `"${cmd}" without matching "${expectedBlock}".` });
2798 }
2799 }
2800
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.` });
2810 }
2811 }
2812 }
2813
2814 return isValid;
2815 }
2816 return cmdPattern && cmdPattern.test(trimmedLine);
2817 });
2818
2819 if (cmdKey) {
2820 // Valid command or comment, no error
2821 return;
2822 } else {
2823 errors.push({ line: index + 1, message: `Syntax error or unknown command.` });
2824 }
2825
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}.` });
2836 } else {
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]}".` });
2840 }
2841 }
2842 }
2843 }
2844 });
2845
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]}.` });
2849 }
2850
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}.` });
2854 }
2855
2856 return errors;
2857}
2858
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;
2863 const args = [];
2864 let lastIndex = 0;
2865
2866 // Step 1: Find all quoted strings and split the input around them
2867 let match;
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();
2871 if (beforeQuoted) {
2872 args.push(...beforeQuoted.split(",").map(arg => arg.trim()).filter(arg => arg.length > 0));
2873 }
2874
2875 // Add the quoted argument
2876 args.push(match[2]); // Capture the content inside the quotes
2877
2878 // Update lastIndex to the end of the current match
2879 lastIndex = regex.lastIndex;
2880 }
2881
2882 // Step 2: Add any remaining unquoted arguments after the last quoted string
2883 const remaining = input.slice(lastIndex).trim();
2884 if (remaining) {
2885 args.push(...remaining.split(",").map(arg => arg.trim()).filter(arg => arg.length > 0));
2886 }
2887
2888 return args;
2889}