MIDAS
Loading...
Searching...
No Matches
sequencer_v1.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)
26 - lineNum - the current line number.
27 - color - (optional) the bg color to use
28*/
29
30// Using Solarized color scheme - https://ethanschoonover.com/solarized/
31
32// Default msl syntax
33const defKeywordGroups = {
34 strings: {
35 keywords: [/(["'])(.*?)\1/g],
36 color: "#008000",
37 fontWeight: "",
38 mslclass: "msl_string",
39 },
40 variables: {
41 keywords: [/(\$[\w]+|^\s*[\w]+(?=\s*=))/gm],
42 color: "#8B0000",
43 fontWeight: "",
44 mslclass: "msl_variable",
45 },
46 controlFlow: {
47 keywords: ["GOTO", "CALL", "SCRIPT", "SUBROUTINE", "ENDSUBROUTINE", "TRANSITION", "INCLUDE", "EXIT"],
48 color: "#268bd2", // blue
49 fontWeight: "bold",
50 mslclass: "msl_control_flow",
51 },
52 dataManagement: {
53 keywords: ["ODBSET", "ODBGET", "ODBCREATE", "ODBDELETE", "ODBINC", "ODBLOAD", "ODBSAVE", "ODBSUBDIR", "PARAM", "SET", "CAT"],
54 color: "#2aa198", // cyan
55 fontWeight: "bold",
56 mslclass: "msl_data_management",
57 },
58 info: {
59 keywords: ["RUNDESCRIPTION", "LIBRARY", "MESSAGE", "MSG"],
60 color: "#6c71c4", // violet
61 fontWeight: "bold",
62 mslclass: "msl_info",
63 },
64 cond: {
65 keywords: ["IF", "ELSE", "ENDIF", "WAIT"],
66 color: "#c577f6", // pink (not solarized)
67 fontWeight: "bold",
68 mslclass: "msl_cond",
69 },
70 loops: {
71 keywords: ["BREAK", "LOOP", "ENDLOOP"],
72 color: "#d33682", // magenta
73 fontWeight: "bold",
74 mslclass: "msl_loops",
75 },
76 dataTypes: {
77 keywords: ["UNIT8", "INT8", "UNIT16", "INT16", "UNIT32", "INT32", "BOOL", "FLOAT", "DOUBLE", "STRING"],
78 color: "#859900", // green
79 fontWeight: "bold",
80 mslclass: "msl_data_types",
81 },
82 units: {
83 keywords: ["SECONDS", "EVENTS", "ODBVALUE"],
84 color: "#cb4b16", // orange
85 fontWeight: "bold",
86 mslclass: "msl_units",
87 },
88 actions: {
89 keywords: ["start", "stop", "pause", "resume"],
90 color: "#dc322f", // red
91 fontWeight: "bold",
92 mslclass: "msl_actions",
93 },
94 bool: {
95 keywords: ["true","false"],
96 color: "#61b7b7",
97 fontWeight: "",
98 mslclass: "msl_bool",
99 },
100 numbers: {
101 keywords: [/\b(?<![0-9a-fA-F#])\d+(\.\d+)?([eE][-+]?\d+)?\b/g],
102 color: "#b58900", // yellow
103 fontWeight: "",
104 mslclass: "msl_number",
105 },
106 comments: {
107 keywords: ["#","COMMENT"],
108 color: "#839496", // base0
109 fontWeight: "italic",
110 mslclass: "msl_comment",
111 },
112};
113
114// Iindentation keywords
115const defIndent = {
116 indentplus: ["IF", "LOOP", "ELSE", "SUBROUTINE"],
117 indentminos: ["ENDIF", "ENDLOOP", "ELSE", "ENDSUBROUTINE"],
118};
119
120var seq_css = `
121/* For buttons */
122.seqbtn {
123 display: none;
124 width: 100px;
125}
126
127.msl_linenum {
128 background-color: #f0f0f0;
129 user-select: none;
130 width: 3em;
131 overflow: hidden;
132 color: gray;
133 text-align: right;
134 padding: 5px;
135 box-sizing: border-box;
136 vertical-align: top;
137 display: inline-block;
138 margin: 0 0 0 0;
139 font-family: monospace;
140 white-space: pre;
141 -moz-user-select: text
142}
143
144.msl_linenum span {
145 display: block;
146}
147
148.msl_linenum_curr {
149 color: #000000;
150 font-weight: bold;
151}
152
153.msl_area {
154 width: calc(100% - 3.5em);
155 overflow:auto;
156 resize: vertical;
157 background-color:white;
158 color: black;
159/* for black bg */
160/* background-color:black;
161 color: white;*/
162 padding: 5px;
163 box-sizing: border-box;
164 vertical-align: top;
165 display: inline-block;
166 margin: 0 0 0 0;
167 font-family: monospace;
168 white-space: pre;
169 -moz-user-select: text;
170}
171
172/*span[id^="sline"] {*/
173.sline {
174
175 display: inline-block;
176}
177.esline {
178 display: block;
179}
180
181.msl_current_line {
182 background-color: #FFFF00;
183 font-weight: bold;
184}
185.msl_fatal {
186 background-color: #FF0000;
187 font-weight: bold;
188}
189.msl_err {
190 background-color: #FF8800;
191 font-weight: bold;
192}
193.msl_msg {
194 background-color: #11FF11;
195 font-weight: bold;
196}
197.msl_current_cmd {
198 background-color: #FFFF00;
199 font-weight: bold;
200}
201
202/* Dropdown button styles*/
203.dropbtn {
204 background-color: Transparent;
205 font-size: 120%;
206 font-family: verdana,tahoma,sans-serif;
207 border: none;
208 cursor: pointer;
209 text-align: center;
210 width: 2.9em;
211 height: 25px;
212}
213
214.dropbtn:hover {
215 background-color: #C0D0D0;
216}
217
218/* Style the dropdown content (hidden by default) */
219.dropdown-content {
220 display: none;
221 position: absolute;
222 background-color: #f9f9f9;
223 min-width: 100px;
224 box-shadow: 0 8px 16px rgba(0,0,0,0.2);
225 z-index: 10;
226}
227
228/* Style the dropdown links */
229.dropdown-content a{
230 color: black;
231 padding: 12px 16px;
232 text-decoration: none;
233 display: block;
234 white-space: nowrap;
235}
236
237.dropdown-content div{
238 color: black;
239 padding: 12px 16px;
240 text-decoration: none;
241 display: block;
242 white-space: nowrap;
243}
244
245/* Change color on hover */
246.dropdown-content a:hover {
247 background-color: #ddd;
248}
249.dropdown-content div:hover {
250 background-color: #ddd;
251}
252
253/* Show the dropdown menu when the button is hovered over */
254.dropdown:hover .dropdown-content {
255 display: block;
256}
257`;
258
259// Implement colors and styles from KeywordsGroups in CSS
260for (const group in defKeywordGroups) {
261 const { mslclass, color, fontWeight } = defKeywordGroups[group];
262
263 if (mslclass) {
264 seq_css += `.${mslclass} { color: ${color}; font-weight: ${fontWeight}; }\n`;
265 }
266}
267
268const seqStyle = document.createElement('style');
269seqStyle.textContent = seq_css;
270document.head.appendChild(seqStyle);
271
272// line connector string
273const lc = "\n";
274// revisions array, maximum nRevisions
275const previousRevisions = [];
276const nRevisions = 20;
277var revisionIndex = -1;
278var saveRevision = true;
279// Meta combo keydown flag
280var MetaCombo = false;
281
282// -- Sequencer specific functions --
283// Setup the correct sequencer state visually
284function seqState(funcCall) {
285 /* Arguments:
286 funcCall - (optional) a function to be called when the state is set (with the state text)
287 */
288 let stateText = "Stopped";
289 // Check sequence state
290 mjsonrpc_db_get_values(["/Sequencer/State/Running","/Sequencer/State/Paused","/Sequencer/State/Finished","/Sequencer/State/Debug"]).then(function(rpc) {
291 if (rpc.result.data[0] && !rpc.result.data[1] && !rpc.result.data[2]) {
292 stateText = "Running";
293 } else if (rpc.result.data[1] && rpc.result.data[0] && !rpc.result.data[2]) {
294 stateText = "Paused";
295 } else {
296 stateText = "Stopped";
297 }
298 if (funcCall) {
299 funcCall(stateText);
300 }
301 return stateText;
302 }).catch (function (error) {
303 console.error(error);
304 return "Unknown";
305 });
306}
307
308// Enable editing of sequence
309function editCurrentSeq(divID) {
310 /* Arguments:
311 divID - ID of <pre> of the editor area
312 */
313 if (!divID) return;
314 let editor = document.getElementById(divID);
315 editor.contentEditable = true;
316 // Attached syntax highlight event editor
317 editor.addEventListener("keyup",checkSyntaxEventUp);
318 editor.addEventListener("keydown",checkSyntaxEventDown);
319 editor.addEventListener("paste", checkSyntaxEventPaste);
320 // Change in text
321 editor.addEventListener("input", function() {
322 seqIsChanged(true);
323 });
324 document.addEventListener("selectionchange", function(event) {
325 if (event.target.activeElement === editor) markCurrLineNum(editor);
326 });
327
328 // Short cuts have to be attached to window
329 window.addEventListener("keydown",shortCutEvent);
330}
331
332// apply changes of filename in the ODB (triggers reload)
333function seqChange(filename) {
334 /* Arguments:
335 filename - full file name with path to change
336 */
337 if (!filename) return;
338 const lastIndex = filename.lastIndexOf('/');
339 const path = filename.substring(0, lastIndex).replace(/^sequencer/,"").replace(/\/+/g, '/').replace(/^\/+|\/+$/g, '');
340 const name = filename.substring(lastIndex + 1);
341 mjsonrpc_db_paste(["/Sequencer/Command/Load filename","/Sequencer/State/Path","/Sequencer/Command/Load new file"], [name,path,true]).then(function (rpc) {
342 sessionStorage.removeItem("depthDir");
343 return;
344 }).catch(function (error) {
345 console.error(error);
346 });
347}
348
349// Load the sequence text from the file name in the ODB
350function seqLoad() {
351 mjsonrpc_db_get_values(["/Sequencer/State/Path","/Sequencer/State/Filename"]).then(function(rpc) {
352 let path = rpc.result.data[0].replace(/\/+/g, '/');
353 let filenameText = rpc.result.data[1];
354 sessionStorage.setItem("fileName", filenameText);
355 file_picker('sequencer/' + path ,'*.msl',seqChange,false,{},true);
356 }).catch(function (error) {
357 mjsonrpc_error_alert(error);
358 });
359}
360
361// Save sequence text in filename.
362function seqSave(filename) {
363 /* Arguments:
364 filename (opt) - undeined save to file in ODB
365 - empty trigger file_picker
366 - save to provided filename with path
367 */
368 let editor = document.getElementById("mslCurrent");
369 let text = editor.innerText.replaceAll("\u200b","");
370 // if a full filename is provided, save text and return
371 if (filename && filename !== "") {
372 file_save_ascii(filename,text,seqChange);
373 updateBtns('Stopped');
374 return;
375 }
376 // empty or undefined file name
377 mjsonrpc_db_get_values(["/Sequencer/State/Path","/Sequencer/State/Filename"]).then(function(rpc) {
378 let path = rpc.result.data[0].replace(/\/+/g, '/').replace(/^\/+|\/+$/g, '');
379 let filenameText = rpc.result.data[1];
380 sessionStorage.setItem("fileName", filenameText);
381 if (filenameText === "(empty)" || filenameText.trim() === "" || filename === "") {
382 file_picker('sequencer/' + path ,'*.msl',seqSave,true,{},true);
383 } else {
384 filename = (path === "") ? "sequencer/" + filenameText : "sequencer/" + path + "/" + filenameText;
385 }
386 if (filename) {
387 file_save_ascii_overwrite(filename,text,seqChange);
388 updateBtns('Stopped');
389 }
390 }).catch(function (error) {
391 mjsonrpc_error_alert(error);
392 });
393}
394
395// Show/hide buttons according to sequencer state
396function updateBtns(state) {
397 /* Arguments:
398 state - the state of the sequencer
399 */
400 const seqState = {
401 Running: {
402 color: "var(--mgreen)",
403 },
404 Stopped: {
405 color: "var(--mred)",
406 },
407 Editing: {
408 color: "var(--mred)",
409 },
410 Paused: {
411 color: "var(--myellow)",
412 },
413 }
414 const color = seqState[state].color;
415 const seqStateSpan = document.getElementById("seqState");
416 seqStateSpan.innerHTML = state;
417 seqStateSpan.style.backgroundColor = color;
418 // hide all buttons
419 const hideBtns = document.querySelectorAll('.seqbtn');
420 hideBtns.forEach(button => {
421 button.style.display = "none";
422 });
423 // then show only those belonging to the current state
424 const showBtns = document.querySelectorAll('.seqbtn.' + state);
425 showBtns.forEach(button => {
426 button.style.display = "inline-block";
427 });
428}
429
430// Show sequencer messages if present
431function mslMessage(message) {
432 // Empty message, return
433 if (!message) return;
434 // Check message and message wait
435 mjsonrpc_db_get_values(["/Sequencer/State/Message","/Sequencer/State/Message Wait"]).then(function(rpc) {
436 const message = rpc.result.data[0];
437 const hold = rpc.result.data[1];
438 if (hold) {
439 dlgMessage("Message", message, true, false,clrMessage);
440 } else {
441 dlgAlert(message);
442 }
443 }).catch (function (error) {
444 console.error(error);
445 });
446}
447
448// Clear sequencer messages
449function clrMessage() {
450 mjsonrpc_db_paste(["Sequencer/State/Message"], [""]).then(function (rpc) {
451 return;
452 }).catch(function (error) {
453 console.error(error);
454 });
455}
456
457// Adjust size of sequencer editor according to browser window size
458function windowResize() {
459 const m = document.getElementById("mmain");
460 const mslCurrent = document.getElementById("mslCurrent");
461 const lineNumbers = document.getElementById('lineNumbers');
462 const seqTable = document.getElementById("seqTable");
463 mslCurrent.style.height = document.documentElement.clientHeight - mslCurrent.getBoundingClientRect().top - 15 + "px";
464 // Sync line number height
465 lineNumbers.style.height = mslCurrent.style.height;
466 seqTable.style.width = m.getBoundingClientRect().width - 10 + "px";
467}
468
469// Load the current sequence from ODB
470function load_msl(divID) {
471 /* Arguments:
472 divID - (optional) div id of editor
473 */
474 if (divID === undefined || divID == "")
475 divID = "mslCurrent";
476 const editor = document.getElementById(divID);
477 mjsonrpc_db_get_values(["/Sequencer/Script/Lines","/Sequencer/State/Running","/Sequencer/State/SCurrent line number"]).then(function(rpc) {
478 let seqLines = rpc.result.data[0];
479 let seqState = rpc.result.data[1];
480 let currLine = rpc.result.data[2];
481 // syntax highlight
482 if (seqLines) {
483 seqLines = syntax_msl(seqLines);
484 let seqHTML = seqLines.join(lc);
485 editor.innerHTML = seqHTML;
486 if (seqState) hlLine(currLine);
487 }
488 // Make not editable until edit button is pressed
489 editor.contentEditable = false;
490 seqIsChanged(false);
491 window.removeEventListener("keydown",shortCutEvent);
492 }).catch (function (error) {
493 console.error(error);
494 });
495}
496
497// Highlight (background color) and scroll to current line
498function hlLine(lineNum,color) {
499 /* Arguments:
500 lineNum - the line number to be highlighted
501 color - (optional) background color
502 */
503 const lineId = "sline" + lineNum;
504 const lineHTML = document.getElementById(lineId);
505
506 // Remove highlight from all lines with the class "msl_current_line"
507 const highlightedLines = document.querySelectorAll(".msl_current_line");
508 highlightedLines.forEach((line) => line.classList.remove("msl_current_line"));
509
510 if (lineHTML) {
511 lineHTML.classList.add("msl_current_line");
512 if (color) lineHTML.style.backgroundColor = color;
513 // Scroll to the highlighted line if the checkbox is checked
514 const scrollToCurrCheckbox = document.getElementById("scrollToCurr");
515 if (scrollToCurrCheckbox && scrollToCurrCheckbox.checked) {
516 lineHTML.scrollIntoView({ block: "center" });
517 }
518 }
519}
520
521// Scroll to make line appear in the center of editor
522function scrollToCurr(scrToCur) {
523 if (scrToCur.checked) {
524 localStorage.setItem("scrollToCurr",true);
525 const currLine = document.querySelector(".msl_current_line");
526 if (currLine) {
527 currLine.scrollIntoView({ block: "center" });
528 }
529 } else {
530 localStorage.removeItem("scrollToCurr",true);
531 }
532}
533
534// shortcut event handling to overtake default behaviour
535function shortCutEvent(event) {
536 if (event.altKey && event.key === 's') {
537 event.preventDefault();
538 seqSave('');
539 event.preventDefault();
540 } else if ((event.ctrlKey || event.metaKey) && event.key === 's') {
541 event.preventDefault();
542 seqSave();
543 event.preventDefault();
544 } else if ((event.ctrlKey || event.metaKey) && event.key === 'z') {
545 event.preventDefault();
546 undoEdit(event.target);
547 event.preventDefault();
548 } else if ((event.ctrlKey || event.metaKey) && event.key === 'r') {
549 event.preventDefault();
550 redoEdit(event.target);
551 event.preventDefault();
552 }
553
554}
555
556// Trigger syntax highlighting on keyup events
557function checkSyntaxEventUp(event) {
558 if (event.ctrlKey || event.altKey || event.metaKey || MetaCombo) return;
559 if (event.keyCode >= 0x30 || event.key === ' '
560 || event.key === 'Backspace' || event.key === 'Delete'
561 || event.key === 'Enter'
562 ) {
563 const e = event.target;
564 let caretPos = getCurrentCursorPosition(e);
565 // Indent according to previous line
566 if (event.key === 'Enter') {
567 // get previous and current line elements (before and after enter)
568 let pline = whichLine(e,-1);
569 let cline = whichLine(e);
570 let plineText = (pline) ? pline.innerText : null;
571 let clineText = (cline) ? cline.innerText : null;
572 let indentLevel = 0;
573 let preSpace = 0;
574
575 if (plineText) {
576 // indent line according to the previous line text
577 // if, loop, else, subroutine - increase indentation
578 const indentPlus = defIndent.indentplus.some(keyword => plineText.trim().toLowerCase().startsWith(keyword.toLowerCase()));
579 // else, endif, endloop, endsubroutine - decrease indentation
580 const indentMinos = defIndent.indentminos.some(keyword => plineText.trim().toLowerCase().startsWith(keyword.toLowerCase()));
581 if (indentMinos) {
582 indentLevel = -1;
583 } else if (indentPlus) {
584 indentLevel = 1;
585 }
586 // Count number of white spaces at begenning of pline and add indentation
587 preSpace = plineText.replace("\n","").search(/\S|$/) + (indentLevel * 3);
588 if (preSpace < 0) preSpace = 0;
589 // Adjust and insert indentation
590 const indentString = " ".repeat(preSpace);
591 let range = window.getSelection().getRangeAt(0);
592 range.deleteContents();
593 range.insertNode(document.createTextNode(indentString));
594 caretPos += preSpace;
595 if (indentMinos) {
596 // remove extra space before line starting with indentMinos keyword
597 let orgLine = plineText;
598 let newLine = indentString + orgLine.trimStart();
599 pline.innerText = newLine;
600 // Adjust caret position accordingly
601 caretPos = caretPos + newLine.length - orgLine.length;
602 }
603 // still needs to handle else which gives indentPlus=indentMinos=true
604 }
605 }
606 e.innerHTML = syntax_msl(e.innerText).join(lc);
607 setCurrentCursorPosition(e, caretPos);
608 e.focus();
609 }
610 return;
611}
612
613
614// Trigger syntax highlighting on keydown events
615function checkSyntaxEventDown(event) {
616 // take care of Mac odd keyup behaviour
617 if (event.metaKey && (/^[a-z]$/.test(event.key) || event.shiftKey || event.altKey)) {
618 MetaCombo = true;
619 } else {
620 MetaCombo = false;
621 }
622
623 if (event.ctrlKey || event.altKey || event.metaKey) return;
624 // Only pass indentation related keys
625 if (event.key !== 'Tab' && event.key !== 'Escape') return;
626 event.preventDefault();
627 let e = event.target;
628 let caretPos = getCurrentCursorPosition(e);
629 let currText = e.innerText;
630 let lines = getLinesInSelection(e);
631 if (event.shiftKey && event.key === 'Tab') {
632 indent_msl(lines,-1);
633 } else if (event.key === 'Tab') {
634 indent_msl(lines,+1);
635 } else if (event.key === 'Escape') {
636 indent_msl(lines);
637 }
638 e.innerHTML = syntax_msl(e.innerText).join(lc);
639 let newText = e.innerText;
640 setCurrentCursorPosition(e, caretPos + newText.length - currText.length);
641 if (lines[0] !== lines[1]) selectLines(lines);
642 event.preventDefault();
643 return;
644}
645
646// Trigger syntax highlighting when you paste text
647function checkSyntaxEventPaste(event) {
648 // set time out to allow default to go first
649 setTimeout(() => {
650 let e = event.target;
651 let caretPos = getCurrentCursorPosition(e);
652 e.innerHTML = syntax_msl(e.innerText).join(lc);
653 setCurrentCursorPosition(e, caretPos);
654 e.focus();
655 }, 0);
656 return;
657}
658
659// Find on which line is the current carret position in e
660// This assumes each line has an id="sline#" where # is the line number.
661function whichLine(e,offset = 0) {
662 // offset allows to pick previous line (after enter)
663 let pos = getCurrentCursorPosition(e);
664 if (!pos) return;
665 let lineNum = e.innerText.substring(0,pos).split("\n").length + offset;
666 let sline = document.getElementById("sline" + lineNum);
667 return sline;
668}
669
670// Return an array with the first and last line numbers of the selected region
671/* This assumes that the lines are in a <pre> element and that
672 each line has an id="sline#" where # is the line number.
673 When the caret in in an empty line, the anchorNode is the <pre> element.
674*/
675function getLinesInSelection(e) {
676 const selection = window.getSelection();
677 //console.trace();
678 if (selection.rangeCount === 0) return [0,0];
679 // is it a single line?
680 const singleLine = selection.isCollapsed;
681 if (singleLine) {
682 const line = whichLine(e);
683 if (line) {
684 const startLine = parseInt(line.id.replace("sline",""));
685 return [startLine,startLine];
686 }
687 }
688 const anchorNode = selection.anchorNode;
689 const range = selection.getRangeAt(0);
690 let startNode,endNode;
691 if (anchorNode.tagName === 'PRE') {
692 let startOffset = range.startOffset;
693 let endOffset = range.endOffset;
694 startNode = range.startContainer.childNodes[startOffset];
695 endNode = range.startContainer.childNodes[endOffset-1];
696 } else {
697 startNode = (range.startContainer && range.startContainer.parentElement.tagName !== 'PRE') ? range.startContainer : range.startContainer.nextSibling;
698 if (startNode && startNode.tagName === 'PRE') startNode = startNode.firstChild;
699 endNode = (range.endContainer && range.endContainer.parentElement.tagName !== 'PRE') ? range.endContainer : range.endContainer.previousSibling;
700 if (endNode && endNode.tagName === 'PRE') endNode = endNode.lastChild;
701 }
702 let startID = (startNode && startNode.id) ? startNode.id : "";
703 let endID = (endNode && endNode.id) ? endNode.id : "";
704 // get first line
705 while (startNode && !startID.startsWith("sline") && startNode.tagName !== 'PRE') {
706 startNode = (startNode.parentNode.tagName !== 'PRE') ? startNode.parentNode : startNode.nextSibling;
707 startID = (startNode && startNode.id) ? startNode.id : "";
708 }
709 // get last line
710 while (endNode && !endID.startsWith("sline") && endNode.tagName !== 'PRE') {
711 endNode = (endNode.parentNode.tagName !== 'PRE') ? endNode.parentNode : endNode.previousSibling;
712 endID = (endNode && endNode.id) ? endNode.id : "";
713 }
714 let startLine = (startNode && startNode.id) ? parseInt(startNode.id.replace("sline","")) : 0;
715 let endLine = (endNode && endNode.id) ? parseInt(endNode.id.replace("sline","")) : 0;
716 if (singleLine) {
717 startLine = endLine = Math.min(startLine, endLine);
718 }
719 return [startLine,endLine];
720}
721
722// get current caret position in chars within element parent
723function getCurrentCursorPosition(parent) {
724 let sel = window.getSelection();
725 if (!sel.focusNode || !parent) return;
726 const range = sel.getRangeAt(0);
727 const prefix = range.cloneRange();
728 prefix.selectNodeContents(parent);
729 prefix.setEnd(range.endContainer, range.endOffset);
730 return prefix.toString().length;
731}
732
733// set current caret position at pos within element parent
734function setCurrentCursorPosition(parent,pos) {
735 for (const node of parent.childNodes) {
736 if (node.nodeType == Node.TEXT_NODE) {
737 if (node.length >= pos) {
738 const range = document.createRange();
739 const sel = window.getSelection();
740 range.setStart(node, pos);
741 range.collapse(true);
742 sel.removeAllRanges();
743 sel.addRange(range);
744 return -1;
745 } else {
746 pos = pos - node.length;
747 }
748 } else {
749 pos = setCurrentCursorPosition(node, pos);
750 if (pos < 0) {
751 return pos;
752 }
753 }
754 }
755 return pos;
756}
757
758// Update line numbers in lineNumbers div
759function updateLineNumbers(lineCount) {
760 const lineNumbers = document.getElementById("lineNumbers");
761 // Clear existing line numbers
762 lineNumbers.innerHTML = "";
763 // Add line numbers to lineNumbers
764 for (let i = 1; i <= lineCount; i++) {
765 const lineNumber = document.createElement('span');
766 lineNumber.id = "lNum" + i.toString();
767 lineNumber.textContent = i;
768 lineNumbers.appendChild(lineNumber);
769 }
770 windowResize();
771}
772
773// Utility function to escape special characters in a string for use in a regular expression
774function escapeRegExp(s) {
775 if (!s) return "";
776 return s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
777}
778
779// Syntax highlight any text according to provided rules
780function syntax_msl(seqLines,keywordGroups) {
781 // If not provided use the default msl keywords and groups
782 if (!keywordGroups) {
783 // take msl default
784 keywordGroups = defKeywordGroups;
785 }
786
787 // Keep original sequence lines (as array)
788 let seqLines_org = Array.isArray(seqLines) ? seqLines : seqLines.split(/\r\n|\r|\n/);
789 // Make full text if you get an array of lines
790 let mslText = Array.isArray(seqLines) ? seqLines.join("\n") : seqLines;
791 // Make some cleanup of illegal characters
792 mslText = mslText.replace(/\t/g, " ");
793
794 let nLines = (mslText.match(/\n/g) || []).length;
795 // save current revision for undo
796 saveState(mslText);
797 // These can be done on the text in one go
798 // Strings
799 let reg = /(["'])(.*?)\1/g;
800 mslText = mslText.replace(reg,'<span class="msl_string">$1$2$1</span>');
801
802 // Comments
803 //reg = /^(COMMENT|#.*?)(.*)$/gim;
804 //mslText = mslText.replace(reg,'<span class="msl_comment">$&</span>');
805
806 // Variables
807 reg = /(\$[\w]+|^\s*[\w]+(?=\s*=))/gm; // starting with $
808 //reg = /^(?!COMMENT|#)(\$[\w]+|^\s*[\w]+(?=\s*=))/gm; // starting with $
809 mslText = mslText.replace(reg,'<span class="msl_variable">$&</span>');
810 reg = new RegExp("(^(?:\\s*)\\b(PARAM|CAT|SET)\\s+)(\\w+)\\b", "gim"); // after PARAM, CAT and SET
811 mslText = mslText.replace(reg,'$1<span class="msl_variable">$3</span>');
812
813 // Data Management group excluding variables (must be after variables)
814 reg = new RegExp("^(\\s*)\\b(" + keywordGroups.dataManagement.keywords.join("|") + ")\\b", "gim");
815 mslText = mslText.replace(reg, "$1<span class='msl_data_management'>$2</span>");
816
817 // Data Type group (must have comma before the keyword)
818 reg = new RegExp("(?<=,\\s*)\\b(" + keywordGroups.dataTypes.keywords.join("|") + ")\\b", "gim");
819 mslText = mslText.replace(reg, "<span class='msl_data_types'>$1</span>");
820
821 // Loops group (must be at the begenning of the line)
822 reg = new RegExp("^(\\s*)\\b(" + keywordGroups.loops.keywords.join("|") + ")\\b", "gim");
823 mslText = mslText.replace(reg, "$1<span class='msl_loops'>$2</span>");
824
825 // Control Flow group
826 reg = new RegExp("^(\\s*)\\b(" + keywordGroups.controlFlow.keywords.join("|") + ")\\b", "gim");
827 mslText = mslText.replace(reg, "$1<span class='msl_control_flow'>$2</span>");
828
829 // Data Management group
830 reg = new RegExp("^(\\s*)\\b(" + keywordGroups.dataManagement.keywords.join("|") + ")\\b", "gim");
831 mslText = mslText.replace(reg, "$1<span class='msl_data_managemen'>$2</span>");
832
833 // Information group
834 reg = new RegExp("^(\\s*)\\b(" + keywordGroups.info.keywords.join("|") + ")\\b", "gim");
835 mslText = mslText.replace(reg, "$1<span class='msl_info'>$2</span>");
836
837 // Conditional group
838 reg = new RegExp("^(\\s*)\\b(" + keywordGroups.cond.keywords.join("|") + ")\\b", "gim");
839 mslText = mslText.replace(reg,"$1<span class='msl_cond'>$2</span>");
840
841 // Units group
842 reg = new RegExp("\\b(" + keywordGroups.units.keywords.join("|") + ")\\b", "gi");
843 mslText = mslText.replace(reg, "<span class='msl_units'>$1</span>");
844
845 // Action group
846 reg = new RegExp("\\b(" + keywordGroups.actions.keywords.join("|") + ")\\b(\\s*)$", "gim");
847 mslText = mslText.replace(reg, "<span class='msl_actions'>$1</span>$2");
848
849 // Numbers/boolean group
850 reg = /\b(\d+(\.\d+)?([eE][-+]?\d+)?)\b/g;
851 mslText = mslText.replace(reg, '<span class="msl_number">$1</span>');
852 reg = /\b(true|false)\b/gi;
853 mslText = mslText.replace(reg, '<span class="msl_bool">$1</span>');
854
855 // Break lines and handle one by one
856 seqLines = mslText.split("\n");
857
858 // This is important for Firefox
859 let emptyClass = "";
860 if (browserType() === 1) emptyClass = "sline";
861 // Loop and restore comment lines and empty lines
862 for (let j = 0; j < seqLines_org.length ; j++) {
863 let line = seqLines_org[j];
864 commentIndex = line.indexOf("#");
865 if (line.trim().startsWith("#") || line.trim().toLowerCase().startsWith("comment")) {
866 // Restore comment lines without highlighting
867 seqLines[j] = `<span class='msl_comment'>${line}</span>`;
868 } else if (commentIndex > 0) {
869 // Restore comment section at end of line
870 const comment = line.slice(commentIndex);
871 seqLines[j] = seqLines[j].slice(0, seqLines[j].indexOf("#")) + `</span><span class='msl_comment'>${comment}</span>`;
872 }
873
874 // empty class is needed for cursor movement in Firefox
875 if (line === "") {
876 //if (j === seqLines_org.length - 1) {
877 seqLines[j] = "<span class='" + emptyClass + "' id='sline" + (j+1).toString() + "'></span>";
878 } else {
879 seqLines[j] = "<span class='sline' id='sline" + (j+1).toString() + "'>" + seqLines[j] + "</span>";
880 }
881 }
882 //seqLines = seqLines.slice(0, nLines + 1);
883 updateLineNumbers(seqLines.length);
884 return seqLines;
885}
886
887// Adjust indentation of a selection of lines
888function indent_msl(lines,addTab) {
889 /* Arguments:
890 lines - an array of two elements, first and last line numbers
891 addTab - (opt) +/-1 to add/subtract three spaces to selected lines
892 */
893 let indentLevel = 0;
894 let singleLine = false;
895 // Avoid issues of begenning of single line
896 if (lines[0] > lines[1] || lines[0] == lines[1]) {
897 lines[1] = lines[0];
898 singleLine = true;
899 }
900 for (let j = lines[0]; j <= lines[1] ; j++) {
901 let lineId = "sline" + j.toString();
902 let prevLineId = "sline" + (j-1).toString();
903 let lineEl = document.getElementById(lineId);
904 let line = "";
905 if (lineEl) line = lineEl.innerText;
906 if (addTab === 1) {
907 let indentString = " ".repeat(3);
908 lineEl.innerText = indentString + line;
909 } else if (addTab === -1) {
910 lineEl.innerText = line.replace(/^\s{1,3}/, '');
911 } else if (singleLine && document.getElementById(prevLineId)) {
912 let prevLineEl = document.getElementById(prevLineId);
913 let prevLine = prevLineEl.innerText;
914 const indentMinos = defIndent.indentminos.some(keyword => prevLine.trim().toLowerCase().startsWith(keyword.toLowerCase()));
915 const indentPlus = defIndent.indentplus.some(keyword => prevLine.trim().toLowerCase().startsWith(keyword.toLowerCase()));
916 if (indentMinos) {
917 indentLevel = -1;
918 } else if (indentPlus) {
919 indentLevel = 1;
920 }
921 let preSpace = prevLine.search(/\S|$/) + (indentLevel * 3);
922 if (preSpace < 0) preSpace = 0;
923 let indentString = " ".repeat(preSpace);
924 lineEl.innerText = indentString + line.trimStart();
925 } else {
926 const indentMinos = defIndent.indentminos.some(keyword => line.trim().toLowerCase().startsWith(keyword.toLowerCase()));
927 if (indentMinos && indentLevel > 0) indentLevel--;
928 let indentString = " ".repeat(indentLevel * 3);
929 if (line !== "" || indentString !== "") {
930 lineEl.innerText = indentString + line.trimStart();
931 }
932 const indentPlus = defIndent.indentplus.some(keyword => line.trim().toLowerCase().startsWith(keyword.toLowerCase()));
933 if (indentPlus) indentLevel++;
934 }
935 }
936}
937
938// Prepare the parameters/variables (if present) from the ODB as a table
939function seqParVarTable(odbTreeV,odbTreeC,odbTreeD,odbTreeO,odbTreeVar,varFlag) {
940 /* Arguments:
941 odbTree... - Objects of ODB values
942 varFlag - (opt) true/false return variable/param table
943 */
944
945 let html = `<span class="modb" data-odb-path="/Sequencer/Variables" onchange="seqParVar('parContainer');"></span>`;
946
947 // If /Sequencer/Param/Value and /Sequencer/Variables are empty return empty
948 if (!odbTreeV && !odbTreeC && !odbTreeVar) {
949 // Clear container row
950 html = `<td colspan="4">${html}</td>`;
951 return html;
952 }
953
954 html += "<table id='paramTable' class='mtable partable' style='width:100%; border-spacing:0px; text-align:left; padding:5px;'>";
955 html += varFlag ? "<tr><th style='width: 120px'>Variable&nbsp;&nbsp;</th><th style='width: 200px'>Current value&nbsp;&nbsp;</th><th>Comment</th></tr>" : "<tr><th>Parameter&nbsp;&nbsp;</th><th>Initial value&nbsp;&nbsp;</th><th>Comment</th></tr>";
956
957 const processParam = (name, value, isBool, defValue, optValue, comment) => {
958 let addString = "";
959 let parLine = `<tr><td>${name}</td>`;
960 let inParLine = "";
961 if (defValue) {
962 // set default value in ODB
963 addString = `value="${defValue}"`;
964 modbset(`/Sequencer/Param/Value/${name}`, defValue);
965 }
966 if (optValue) {
967 if (!defValue) defValue = optValue[0];
968 const optionsHtml = optValue.map(option => `<option value="${option}" ${option === defValue ? 'selected' : ''}>${option}</option>`).join('');
969 inParLine += `<select onchange="modbset('/Sequencer/Param/Value/${name}', this.value)">${optionsHtml}</select>`;
970 } else if (isBool) {
971 let initState = defValue ? "checked" : "";
972 inParLine += `<input type="checkbox" ${initState} class="modbcheckbox" data-odb-path="/Sequencer/Param/Value/${name}"></input>`;
973 } else {
974 inParLine += `<input ${addString} onchange="modbset('/Sequencer/Param/Value/${name}', this.value);"></input>`;
975 }
976
977 parLine += `<td>${inParLine}<span class="modb" data-odb-path="/Sequencer/Param/Value/${name}"></span></td>`;
978 parLine += `<td>${comment}</td></tr>`;
979 html += parLine;
980 };
981
982 if (varFlag) {
983 // Go over all variables in ODB
984 for (let key in odbTreeVar) {
985 const match = key.match(/([^/]+)\/key$/);
986 if (match) {
987 const name = match[1];
988 const value = odbTreeVar[name];
989 let comment = (odbTreeC && odbTreeC[name]) ? odbTreeC[name] : '';
990 html += `<tr><td>${name}</td><td><span class="modbvalue" data-odb-path="/Sequencer/Variables/${name}">${value}</span></td><td>${comment}</td></tr>\n`;
991 }
992 }
993 } else {
994 // Go over all parameters in ODB
995 for (let key in odbTreeV) {
996 const match = key.match(/([^/]+)\/key$/);
997 if (match) {
998 const name = match[1];
999 // if variable is found use its value
1000 let value = (odbTreeVar && odbTreeVar[name]) ? odbTreeVar[name] : odbTreeV[name];
1001 let isBool = (odbTreeV[key].type == 8);
1002 let defValue = (value !== null && value !== undefined && value !== '') ? value : (odbTreeD && odbTreeD[name]) || value;
1003 let optValue = odbTreeO ? odbTreeO[name] : undefined;
1004 let comment = odbTreeC[name] || '';
1005 let addString = "";
1006 if (typeof value !== "object") {
1007 processParam(name, value, isBool, defValue, optValue, comment);
1008 }
1009 }
1010 }
1011 }
1012
1013 html += "</table>";
1014 html = `<td colspan="4">${html}</td>`;
1015 return html;
1016}
1017
1018// Prepare the parameters/variables (if present) from the ODB as a table
1019function seqParVar(parContainer,debugFlag) {
1020 /* Arguments:
1021 parContainer - (opt) id of element to be filled with html param table
1022 debugFlag - (opt) true/false run in debug/normal mode
1023 */
1024
1025 let html = "";
1026 mjsonrpc_db_ls(["/Sequencer/Param/Value","/Sequencer/Param/Comment","/Sequencer/Param/Defaults","/Sequencer/Param/Options","/Sequencer/Variables"]).then(function(rpc) {
1027 const odbTreeV = rpc.result.data[0]; // value
1028 const odbTreeC = rpc.result.data[1]; // comment
1029 const odbTreeD = rpc.result.data[2]; // defaults
1030 const odbTreeO = rpc.result.data[3]; // options
1031 const odbTreeVar = rpc.result.data[4]; // Variables
1032 // dialog is created if parContainer is undefined and variables are filled otherwise
1033 const dlgTable = !parContainer;
1034 // If /Sequencer/Param/Value and /Sequencer/Variables are empty start and return immediately
1035 if (!odbTreeV || !odbTreeC) {
1036 if (dlgTable) {
1037 parContainer = "parContainer";
1038 if (debugFlag) {
1039 modbset('/Sequencer/Command/Debug script',true);
1040 } else {
1041 modbset('/Sequencer/Command/Start script',true);
1042 }
1043 }
1044 html = seqParVarTable(odbTreeV,odbTreeC,odbTreeD,odbTreeO,odbTreeVar,!dlgTable);
1045 if (document.getElementById(parContainer))
1046 document.getElementById(parContainer).innerHTML = html;
1047 return;
1048 }
1049
1050 // For dialog use parameters
1051 if (dlgTable) {
1052 // Go over all parameters in ODB
1053 html = seqParVarTable(odbTreeV,odbTreeC,odbTreeD,odbTreeO,odbTreeVar,!dlgTable);
1054 // if parContainer not given produce a dialog
1055 let htmlDlg = `${html}<br><button class="dlgButtonDefault" id="seqParamStart" type="button">Start</button><button class="dlgButton" id="seqParamCancel" type="button">Cancel</button><br>`;
1056 let d = general_dialog(htmlDlg,"Variables");
1057 let e = document.getElementById("parContainer");
1058 // Append the table to a container
1059 let startBtn = document.getElementById("seqParamStart");
1060 let cancelBtn = document.getElementById("seqParamCancel");
1061
1062 cancelBtn.addEventListener("click", function () {d.remove();});
1063 startBtn.addEventListener("click", function () {
1064 e.innerHTML = html;
1065 d.remove();
1066 if (debugFlag) {
1067 modbset('/Sequencer/Command/Debug script',true);
1068 } else {
1069 modbset('/Sequencer/Command/Start script',true);
1070 }
1071 });
1072 } else {
1073 // Go over all variables in ODB
1074 html = seqParVarTable(odbTreeV,odbTreeC,odbTreeD,odbTreeO,odbTreeVar,!dlgTable);
1075 if (document.getElementById(parContainer))
1076 document.getElementById(parContainer).innerHTML = html;
1077 windowResize();
1078 }
1079 }).catch (function (error) {
1080 console.error(error);
1081 });
1082}
1083
1084function seqParamsDlg(html) {
1085 let htmlDlg = html + "<br />" +
1086 "<button class=\"dlgButtonDefault\" id=\"seqParamStart\" type=\"button\">Start</button>" +
1087 "<button class=\"dlgButton\" id=\"seqParamCancel\" type=\"button\">Cancel</button><br>";
1088
1089 let d = general_dialog(htmlDlg,"Variables");
1090 let e = document.getElementById("parContainer");
1091 // Append the table to a container
1092 let startBtn = document.getElementById("seqParamStart");
1093 let cancelBtn = document.getElementById("seqParamCancel");
1094
1095 cancelBtn.addEventListener("click", function () {
1096 d.remove();
1097 });
1098 startBtn.addEventListener("click", function () {
1099 e.innerHTML = "<td colspan='4'>" + html + "</td>";
1100 d.remove();
1101 if (debugFlag) {
1102 modbset('/Sequencer/Command/Debug script',true);
1103 } else {
1104 modbset('/Sequencer/Command/Start script',true);
1105 }
1106 });
1107}
1108
1109// helper debug function
1110function debugSeq(parContainer) {
1111 seqParVar(parContainer,true);
1112}
1113
1114// helper start function
1115function startSeq(parContainer) {
1116 seqParVar(parContainer,false);
1117}
1118
1119// helper stop function
1120function stopSeq(flag) {
1121 modbset('/Sequencer/Command/Stop immediately',flag);
1122}
1123
1124// Show or hide parameters table
1125function showParTable(parContainer) {
1126 let e = document.getElementById(parContainer);
1127 // update embedded table to make sure values are synced with ODB
1128 seqParVar(parContainer);
1129 let vis = document.getElementById("showParTable").checked;
1130 if (e.style.display == "none" && vis) {
1131 e.style.display = "table-row";
1132 } else {
1133 e.style.display = "none";
1134 }
1135}
1136
1137// Show error state of sequencer
1138function checkError(element) {
1139 let e = element.parentElement.parentElement;
1140 if (element.value === "") {
1141 e.style.display = "none";
1142 } else {
1143 e.style.display = "table-row";
1144 }
1145}
1146
1147// Show extra rows for wait and loop
1148// ToDo: the size of the editor should be adjusted to fill the screen
1149function extraRows(e) {
1150 /* Arguments:
1151 e - triggering element to identify wait or loop
1152 */
1153 // get current row
1154 let rIndex = e.parentElement.parentElement.rowIndex;
1155 let table = e.parentElement.parentElement.parentElement;
1156 // check if there is a wait or loop commands (if non-zero)
1157 if (e.value) {
1158 if (e.id === "waitTrig") {
1159 // Make sure there is only one wait row
1160 document.querySelectorAll('.waitTR').forEach(element => element.remove());
1161 // Insert a new row
1162 let tr = table.insertRow(rIndex+1);
1163 tr.className = "waitTR";
1164 tr.innerHTML = `<td></td><td>Wait:</td>
1165 <td style="position: relative;" colspan="2">
1166 <span class="modbhbar" style="z-index: 1; position: absolute; top: 0; left: 0; width: calc(100% - 2px); height: 100%; color: lightgreen;" data-odb-path="/Sequencer/State/Wait value" id="mwaitProgress"></span>
1167 <span style="z-index: 2; position: absolute; top: 1px; left: 5px; display: inline-block;">
1168 [<span class="modbvalue" data-odb-path="/Sequencer/State/Wait value"></span>/<span class="modbvalue" data-odb-path="/Sequencer/State/Wait limit" onchange="if (document.getElementById('mwaitProgress')) document.getElementById('mwaitProgress').dataset.maxValue = this.value;"></span>] <span class="modbvalue" data-odb-path="/Sequencer/State/Wait type"></span>
1169 </span>
1170 </td>`;
1171 windowResize();
1172 } else if (e.id === "loopTrig") {
1173 mjsonrpc_db_get_values(["/Sequencer/State/Loop n"]).then(function(rpc) {
1174 let loopArray = rpc.result.data[0];
1175 for (let i = 0; i < loopArray.length; i++) {
1176 if (loopArray[i] === 0) break;
1177 let tr = table.insertRow(rIndex+1);
1178 tr.className = "loopTR";
1179 tr.innerHTML = `<td></td><td>Loop ${i}:</td>
1180 <td style="position: relative;" colspan="2">
1181 <span class="modbhbar" style="z-index: 1; position: absolute; top: 0; left: 0; width: calc(100% - 2px); height: 100%; color: #CBC3E3;" data-odb-path="/Sequencer/State/Loop counter[${i}]" id="mloopProgress${i}"></span>
1182 <span style="z-index: 2; position: absolute; top: 1px; left: 5px; display: inline-block;">
1183 [<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>]
1184 </span>
1185 </td>`;
1186 windowResize();
1187 }
1188 }).catch (function (error) {
1189 console.error(error);
1190 });
1191 }
1192 } else {
1193 // remove rows
1194 if (e.id === "waitTrig") document.querySelectorAll('.waitTR').forEach(element => element.remove());
1195 if (e.id === "loopTrig") document.querySelectorAll('.loopTR').forEach(element => element.remove());
1196 windowResize();
1197 }
1198}
1199
1200function extraCell(e) {
1201 // get current row
1202 // check if there is a wait or loop commands (if non-zero)
1203 if (e.value) {
1204 if (e.id === "waitTrig") {
1205 // Make sure there is only one wait row
1206 document.querySelectorAll('.waitTR').forEach(element => element.remove());
1207 // Insert a new row
1208 let waitDiv = document.createElement('div');
1209 waitDiv.style.position = "relative";
1210 waitDiv.style.width = "calc(100% - 2px)";
1211 waitDiv.className = "waitTR";
1212 waitDiv.innerHTML = `<span class="modbhbar" style="display: inline-block;z-index: 1; position: relative; top: 0; left: 0; width: 100%; height: 1.5em; color: lightgreen;" data-odb-path="/Sequencer/State/Wait value" id="mwaitProgress">&nbsp;</span>
1213 <span style="z-index: 2; position: absolute; top: 1px; left: 5px; display: inline-block;">
1214 Wait:[<span style="display: inline-block;" class="modbvalue" data-odb-path="/Sequencer/State/Wait value"></span>/<span style="display: inline-block;" class="modbvalue" data-odb-path="/Sequencer/State/Wait limit" onchange="if (document.getElementById('mwaitProgress')) document.getElementById('mwaitProgress').dataset.maxValue = this.value;"></span>] <span style="display: inline-block;" class="modbvalue" data-odb-path="/Sequencer/State/Wait type"></span>
1215 </span>`;
1216 e.insertAdjacentElement('beforebegin', waitDiv);
1217 } else if (e.id === "loopTrig") {
1218 mjsonrpc_db_get_values(["/Sequencer/State/Loop n"]).then(function(rpc) {
1219 let loopArray = rpc.result.data[0];
1220 for (let i = 0; i < loopArray.length; i++) {
1221 if (loopArray[i] === 0) break;
1222 let tr = table.insertRow(rIndex+1);
1223 tr.className = "loopTR";
1224 tr.innerHTML = `<td></td>
1225 <td style="position: relative;" colspan="3">&nbsp;
1226 <span class="modbhbar" style="z-index: 1; position: absolute; top: 0; left: 0; width: 100%; height: 100%; color: #CBC3E3;" data-odb-path="/Sequencer/State/Loop counter[${i}]" id="mloopProgress${i}"></span>
1227 <span style="z-index: 2; position: absolute; top: 1px; left: 5px; display: inline-block;">
1228 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>]
1229 </span>
1230 </td>`;
1231 }
1232 }).catch (function (error) {
1233 console.error(error);
1234 });
1235 }
1236 } else {
1237 // remove rows
1238 if (e.id === "waitTrig") document.querySelectorAll('.waitTR').forEach(element => element.remove());
1239 if (e.id === "loopTrig") document.querySelectorAll('.loopTR').forEach(element => element.remove());
1240 }
1241}
1242
1243// Helper function to identify browser, 1 FF, 2 Chrome, 3, other
1244function browserType() {
1245 if (navigator.userAgent.indexOf("Chrome") !== -1) {
1246 return 1;
1247 } else if (navigator.userAgent.indexOf("Firefox") !== -1) {
1248 return 2;
1249 }
1250
1251 return 1;
1252}
1253
1254// make visual hint that file is changes
1255function seqIsChanged(flag) {
1256 // flag - true is change, false is saved
1257 let filechanged = document.getElementById("filechanged");
1258 filechanged.innerHTML = "";
1259 if (flag) {
1260 filechanged.innerHTML = "&nbsp;&#9998;"; // 2022 too small
1261 }
1262}
1263
1264// save history of edits, called from syntax_msl()
1265function saveState(mslText) {
1266 if (!saveRevision) return;
1267 previousRevisions.push(mslText)
1268 // keep only nRevisions revisions
1269 if (previousRevisions.length > nRevisions) {
1270 previousRevisions.pop();
1271 }
1272 revisionIndex = previousRevisions.length - 1;
1273}
1274
1275// undo last edit
1276function undoEdit(editor) {
1277 if (revisionIndex < 1) {
1278 return;
1279 } else {
1280 revisionIndex--;
1281 }
1282 let caretPos = getCurrentCursorPosition(editor);
1283 let currText = editor.innerText;
1284 saveRevision = false;
1285 editor.innerHTML = syntax_msl(previousRevisions[revisionIndex]).join(lc);
1286 saveRevision = true;
1287 // calculate change in caret position based on length
1288 caretPos = caretPos + previousRevisions[revisionIndex].length - currText.length;
1289 setCurrentCursorPosition(editor, caretPos);
1290}
1291
1292// redo the undo
1293function redoEdit(editor) {
1294 if (revisionIndex >= previousRevisions.length - 1) {
1295 return;
1296 } else {
1297 revisionIndex++;
1298 }
1299 let caretPos = getCurrentCursorPosition(editor);
1300 let currText = editor.innerText;
1301 saveRevision = false;
1302 editor.innerHTML = syntax_msl(previousRevisions[revisionIndex]).join(lc);
1303 saveRevision = true;
1304 // calculate change in caret position based on length
1305 caretPos = caretPos + previousRevisions[revisionIndex].length - currText.length;
1306 setCurrentCursorPosition(editor, caretPos);
1307}
1308
1309// Select slines from startLine to endLine
1310function selectLines([startLine, endLine]) {
1311 const selection = window.getSelection();
1312 // Remove existing selections
1313 selection.removeAllRanges();
1314 let startElementId = 'sline' + startLine;
1315 let endElementId = 'sline' + endLine;
1316 let startElement = null, endElement = null;
1317 if (document.getElementById(startElementId)) startElement = document.getElementById(startElementId).firstChild;
1318 if (document.getElementById(endElementId)) endElement = document.getElementById(endElementId).lastChild;
1319 // we need startElement and endElement with first/lastChild
1320 // the following prevents loosing selection but not ideal
1321 while (startElement === null && startLine <= endLine) {
1322 startLine++;
1323 startElementId = 'sline' + startLine;
1324 startElement = document.getElementById(startElementId).firstChild;
1325 }
1326 while (endElement === null && endLine > 0) {
1327 endLine--;
1328 endElementId = 'sline' + endLine;
1329 endElement = document.getElementById(endElementId).lastChild;
1330 }
1331 if (startElement && endElement) {
1332 const range = document.createRange();
1333 // Set the start of the range to the startElement at offset 0
1334 range.setStart(startElement, 0);
1335 // Set the end of the range to the endElement at its length
1336 range.setEnd(endElement, endElement.childNodes.length);
1337 // Add the range to the selection
1338 selection.addRange(range);
1339 }
1340}
1341
1342// switch between dark and light modes on request
1343function lightToDark(lToDcheck) {
1344 const msl_area = document.querySelector('.msl_area');
1345 if (lToDcheck.checked) {
1346 localStorage.setItem("darkMode", true);
1347 msl_area.style.backgroundColor = "black";
1348 msl_area.style.color = "white";
1349 } else {
1350 localStorage.removeItem("darkMode");
1351 msl_area.style.backgroundColor = "white";
1352 msl_area.style.color = "black";
1353 }
1354}
1355
1356// Mark the current line number
1357function markCurrLineNum(editor) {
1358 const currLines = document.querySelectorAll(".msl_linenum_curr");
1359 currLines.forEach((line) => line.classList.remove("msl_linenum_curr"));
1360 const [startLine,endLine] = getLinesInSelection(editor);
1361 if (startLine === 0 && endLine === 0) return;
1362 for (let i = startLine; i <= endLine; i++) {
1363 let lineNumId = "#lNum" + i.toString();
1364 let lineNum = lineNumbers.querySelector(lineNumId);
1365 if (lineNum)
1366 lineNum.className = "msl_linenum_curr";
1367 }
1368}
1369
1370// Check if sequencer program is in ODB and running.
1371// If not, try to get it going
1372function checkSequencer() {
1373 mjsonrpc_call('cm_exist', {"name":"Sequencer","unique":true}).then(function (rpc1) {
1374 mjsonrpc_db_get_values(["/Programs/Sequencer/Start command"]).then(function(rpc2) {
1375 let isRunning = (rpc1.result.status === 1);
1376 let isDefined = ((rpc2.result.data[0] !== null) && (rpc2.result.data[0] !== ""));
1377 if (isRunning && isDefined) return;
1378 // sequencer not running or not defined, stop it just in case and check the reason
1379 mjsonrpc_stop_program("Sequencer");
1380 let message = "";
1381 if (isDefined) {
1382 message = "Sequencer program is not running.<br>Should I start it?"
1383 } else {
1384 message = "Sequencer program is not configured and not running.<br>Should I try to start it anyway?"
1385 }
1386 dlgConfirm(message,function(resp) {
1387 if (resp) {
1388 if (!isDefined) {
1389 // assume that sequencer in path and create a start command, sleep 2s,
1390 // set value to "msequencer -D", sleep 2s, start program
1391 mjsonrpc_db_create([{"path" : "/Programs/Sequencer/Start command", "type" : TID_STRING}]).then(function (rpc3) {
1392 setTimeout(function(){
1393 mjsonrpc_db_paste(["/Programs/Sequencer/Start command"],["msequencer -D"]).then(function (rpc4) {
1394 if (rpc4.result.status[0] === 1) {
1395 mjsonrpc_start_program("Sequencer");
1396 }
1397 }).catch(function (error) {
1398 console.error(error);
1399 });
1400 }, 2000);
1401 }).catch(function (error) {
1402 console.error(error);
1403 });
1404 } else {
1405 mjsonrpc_start_program("Sequencer");
1406 }
1407 // take 3 seconds and check that it actually started
1408 setTimeout(function(){
1409 mjsonrpc_call('cm_exist', {"name":"Sequencer","unique":true}).then(function (rpc5) {
1410 if (rpc5.result.status === 1) {
1411 dlgAlert("Sequencer started successfully.");
1412 } else {
1413 dlgAlert("Failed to start Sequencer!<br>Try to start it manually (msequencer -D)");
1414 }
1415 });
1416 }, 3000);
1417 }
1418 });
1419 }).catch (function (error) {
1420 console.error(error);
1421 });
1422 }).catch(function (error) {
1423 console.error(error);
1424 });
1425}
1426
1427// Populate a modal with a general html
1428function general_dialog(html = "", iddiv = "dlgGeneral", width, height, x, y) {
1429 /* general dialog containing html code, optional parameters
1430 iddiv - the name of the dialog div (optional)
1431 width/height - minimal width/height of dialog (optional)
1432 x/y - initial position of dialog (optional)
1433 */
1434
1435 // First make sure you removed exisitng iddiv
1436 if (document.getElementById(iddiv)) document.getElementById(iddiv).remove();
1437 let d = document.createElement("div");
1438 d.className = "dlgFrame";
1439 d.id = iddiv;
1440 d.style.zIndex = "30";
1441 d.style.overflow = "hidden";
1442 d.style.resize = "both";
1443 d.style.minWidth = width ? width + "px" : "400px";
1444 d.style.minHeight = height ? height + "px" : "200px";
1445 //d.style.maxWidth = "50vw";
1446 //d.style.maxHeight = "50vh";
1447 d.style.width = width + "px";
1448 d.style.height = height ? height + "px" : "200px";
1449 d.shouldDestroy = true;
1450
1451 const dlgTitle = document.createElement("div");
1452 dlgTitle.className = "dlgTitlebar";
1453 dlgTitle.id = "dlgMessageTitle";
1454 dlgTitle.innerText = iddiv ? iddiv + " dialog" : "General dialog";
1455 d.appendChild(dlgTitle);
1456
1457 const dlgPanel = document.createElement("div");
1458 dlgPanel.className = "dlgPanel";
1459 dlgPanel.id = "dlgPanel";
1460 d.appendChild(dlgPanel);
1461
1462 const content = document.createElement("div");
1463 content.id = "dlgHTML";
1464 content.style.overflow = "auto";
1465 content.innerHTML = html;
1466 dlgPanel.appendChild(content);
1467
1468 document.body.appendChild(d);
1469 console.log( content.style.width, d.style.minWidth);
1470 dlgShow(d);
1471
1472 if (x !== undefined && y !== undefined)
1473 dlgMove(d, x, y);
1474
1475 // Initial size based on content
1476 d.style.height = (content.offsetHeight + dlgTitle.offsetHeight + 5 ) + "px";
1477 // adjust size when resizing modal
1478 const resizeObs = new ResizeObserver(() => {
1479 content.style.height = (d.offsetHeight - dlgTitle.offsetHeight - 5 ) + "px";
1480 //d.style.height = (content.offsetHeight + dlgTitle.offsetHeight + 5 ) + "px";
1481 });
1482 resizeObs.observe(d);
1483
1484 return d;
1485}