MIDAS
Loading...
Searching...
No Matches
sequencer2.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 todo:
30 - start -> load, switch to main tab and confirm
31 - progress to embed in infocolumn
32*/
33
34// Using Solarized color scheme - https://ethanschoonover.com/solarized/
35
36// Default msl definitions, keywords, indentation and file name
37const mslDefs = {
38 groups : {
39 strings: {
40 keywords: [/(["'])(.*?)\1/g],
41 color: "#008000",
42 fontWeight: "",
43 mslclass: "msl_string",
44 },
45 variables: {
46 keywords: [/(\$[\w]+|^\s*[\w]+(?=\s*=))/gm],
47 color: "#8B0000",
48 fontWeight: "",
49 mslclass: "msl_variable",
50 },
51 controlFlow: {
52 keywords: ["GOTO", "CALL", "SCRIPT", "SUBROUTINE", "ENDSUBROUTINE", "TRANSITION", "INCLUDE", "EXIT"],
53 color: "#268bd2", // blue
54 fontWeight: "bold",
55 mslclass: "msl_control_flow",
56 },
57 dataManagement: {
58 keywords: ["ODBSET", "ODBGET", "ODBCREATE", "ODBDELETE", "ODBINC", "ODBLOAD", "ODBSAVE", "ODBSUBDIR", "PARAM", "SET", "CAT"],
59 color: "#2aa198", // cyan
60 fontWeight: "bold",
61 mslclass: "msl_data_management",
62 },
63 info: {
64 keywords: ["RUNDESCRIPTION", "LIBRARY", "MESSAGE", "MSG"],
65 color: "#6c71c4", // violet
66 fontWeight: "bold",
67 mslclass: "msl_info",
68 },
69 cond: {
70 keywords: ["IF", "ELSE", "ENDIF", "WAIT"],
71 color: "#c577f6", // pink (not solarized)
72 fontWeight: "bold",
73 mslclass: "msl_cond",
74 },
75 loops: {
76 keywords: ["BREAK", "LOOP", "ENDLOOP"],
77 color: "#d33682", // magenta
78 fontWeight: "bold",
79 mslclass: "msl_loops",
80 },
81 dataTypes: {
82 keywords: ["UNIT8", "INT8", "UNIT16", "INT16", "UNIT32", "INT32", "BOOL", "FLOAT", "DOUBLE", "STRING"],
83 color: "#859900", // green
84 fontWeight: "bold",
85 mslclass: "msl_data_types",
86 },
87 units: {
88 keywords: ["SECONDS", "EVENTS", "ODBVALUE"],
89 color: "#cb4b16", // orange
90 fontWeight: "bold",
91 mslclass: "msl_units",
92 },
93 actions: {
94 keywords: ["start", "stop", "pause", "resume"],
95 color: "#dc322f", // red
96 fontWeight: "bold",
97 mslclass: "msl_actions",
98 },
99 bool: {
100 keywords: ["true","false"],
101 color: "#61b7b7",
102 fontWeight: "",
103 mslclass: "msl_bool",
104 },
105 numbers: {
106 keywords: [/\b(?<![0-9a-fA-F#])\d+(\.\d+)?([eE][-+]?\d+)?\b/g],
107 color: "#b58900", // yellow
108 fontWeight: "",
109 mslclass: "msl_number",
110 },
111 comments: {
112 keywords: ["#","COMMENT"],
113 color: "#839496", // base0
114 fontWeight: "italic",
115 mslclass: "msl_comment",
116 },
117 },
118 defIndent: {
119 indentplus: ["IF", "LOOP", "ELSE", "SUBROUTINE"],
120 indentminos: ["ENDIF", "ENDLOOP", "ELSE", "ENDSUBROUTINE"],
121 },
122 filename: {
123 ext : "*.msl",
124 path : "sequencer",
125 },
126};
127
128// Iindentation keywords
129const defIndent = mslDefs.defIndent;
130// Default values for MSL
131const fnExt = mslDefs.filename.ext;
132const fnPath = mslDefs.filename.path;
133
134var seq_css = `
135/* For buttons */
136.seqbtn {
137 display: none;
138}
139
140#iconsRow img {
141 cursor: pointer;
142 padding: 0.4em;
143 margin: 0;
144}
145#iconsRow img:hover {
146 background: #C0D0D0;
147}
148
149.edt_linenum {
150 background-color: #f0f0f0;
151 user-select: none;
152 width: 3em;
153 overflow: hidden;
154 color: gray;
155 text-align: right;
156 padding: 5px;
157 box-sizing: border-box;
158 vertical-align: top;
159 display: inline-block;
160 margin: 0 0 0 0;
161 font-family: monospace;
162 white-space: pre;
163 -moz-user-select: text;
164 pointer-events: none;
165}
166.edt_linenum span {
167 display: block;
168}
169.edt_linenum_curr {
170 color: #000000;
171 font-weight: bold;
172}
173.edt_area {
174 width: 100%;
175 height: 100%;
176 overflow:auto;
177 background-color:white;
178 color: black;
179 padding: 5px;
180 box-sizing: border-box;
181 display: inline-block;
182 margin: -1px 0 0 0;
183 border-top: 1px solid gray;
184 font-family: monospace;
185 white-space: pre;
186 -moz-user-select: text;
187}
188
189.sline {
190 display: inline-block;
191}
192.esline {
193 display: inline-block;
194}
195
196.msl_current_line {
197 background-color: #FFFF00;
198 font-weight: bold;
199}
200.msl_fatal {
201 background-color: #FF0000;
202 font-weight: bold;
203}
204.msl_err {
205 background-color: #FF8800;
206 font-weight: bold;
207}
208.msl_msg {
209 background-color: #11FF11;
210 font-weight: bold;
211}
212.msl_current_cmd {
213 background-color: #FFFF00;
214 font-weight: bold;
215}
216.info_column{
217 padding-left: 5px;
218 width: 19.5em;
219 height: 100%;
220 overflow:auto;
221 display: none;
222}
223.info_container{
224
225}
226.infotable {
227 width: 100%;
228 border-spacing: 0px;
229 border: 1px solid black;
230 border-radius: 5px;
231 text-align: left;
232 padding: 0px;
233 float: right;
234 line-height: 30px; /* since modbvalue forces resize */
235}
236.infotable th{
237 white-space: nowrap;
238}
239.infotable tr:first-child th:first-child,
240.infotable tr:first-child td:first-child {
241 border-top-left-radius: 5px;
242}
243.infotable tr:first-child th:last-child,
244.infotable tr:first-child td:last-child {
245 border-top-right-radius: 5px;
246}
247.infotable tr:last-child td:first-child {
248 border-bottom-left-radius: 5px;
249}
250.infotable tr:last-child td:last-child {
251 border-bottom-right-radius: 5px;
252}
253.waitlooptxt {
254 z-index: 33;
255 position: absolute;
256 top: 1px;
257 left: 5px;
258 display: inline-block;
259}
260.waitloopbar {
261 z-index: 32;
262 width: calc(100% - 2px);
263 height: 100%;
264 text-align: left;
265}
266
267/* Dropdown button styles*/
268.dropmenu {
269 background-color: Transparent;
270 font-size: 100%;
271 font-family: verdana,tahoma,sans-serif;
272 border: none;
273 cursor: pointer;
274 text-align: left;
275 padding: 5px;
276 line-height: 1;
277 color:#404040;
278}
279.dropmenu:hover {
280 background-color: #C0D0D0;
281}
282
283/* Style the dropdown content (hidden by default) */
284.dropdown {
285 position: relative;
286 display: inline-block;
287}
288.dropdown-content {
289 display: none;
290 position: absolute;
291 background-color: #f9f9f9;
292 min-width: 150px;
293 box-shadow: 0 8px 16px rgba(0,0,0,0.2);
294 z-index: 10;
295 font-size: 90%;
296 color: black;
297 white-space: nowrap;
298 cursor: pointer;
299}
300.dropdown-content a{
301 display: block;
302 padding: 4px 8px;
303}
304.dropdown-content div{
305 display: flex;
306 justify-content: space-between;
307 align-items: center;
308 padding: 4px 8px;
309}
310.dropdown-content a:hover {
311 background-color: #C0D0D0;
312}
313.dropdown-content div:hover {
314 background-color: #C0D0D0;
315}
316.dropdown:hover .dropdown-content {
317 display: block;
318}
319
320 /* Style the tab */
321.etab {
322 position: relative;
323 top: 2px;
324 z-index: 1;
325 overflow-x: auto;
326 white-space: nowrap;
327 width: 100%;
328 display: block;
329 -webkit-overflow-scrolling: touch;
330 border: none;
331 /* background-color: #475057;*/
332}
333.etab button {
334 background-color: #D0D0D0;
335 float: left;
336 margin: 4px 2px 0px 2px;
337 border-top-left-radius: 10px;
338 border-top-right-radius: 10px;
339 border: none;
340 border-top: 1px solid Transparent;
341 border-right: 1px solid Transparent;
342 border-left: 1px solid Transparent;
343 cursor: pointer;
344 padding: 3px 5px 3px 5px;
345}
346.etab button:hover {
347 background-color: #FFFFFF;
348 border-bottom: 5px solid #FFFFFF;
349 /*opacity: 0.7;*/
350}
351.etab button.edt_active:hover {
352 /*opacity: 0.7;*/
353 background-color: #FFFFFF;
354 border-bottom: 5px solid #FFFFFF;
355}
356.etab button.edt_active {
357 background-color: white;/*Transparent;*/
358 border-top: 1px solid gray;
359 border-right: 1px solid gray;
360 border-left: 1px solid gray;
361 border-bottom: 5px solid white;
362 color: black;
363}
364.etabcontent {
365 display: none;
366 /*padding: 6px 12px;*/
367 padding: 0;
368 border: 1px solid #ccc;
369 border-top: none;
370 height: 100%;
371 /*width: 100%;
372 display: flex;*/
373}
374.editbtn{
375 background-color: #f0f0f0;
376/*
377 font-family: verdana,tahoma,sans-serif;
378 font-size: 1.0em;
379*/
380 border: 1px solid black;
381 border-radius: 3px;
382 cursor: pointer;
383 padding: 0px 2px 0px 2px;
384 margin-left: 3px;
385}
386.editbtn:hover{
387 background-color: #C0D0D0;
388}
389.closebtn {
390 font-family: verdana,tahoma,sans-serif;
391 border: none;
392 border-radius: 3px;
393 cursor: pointer;
394 padding: 2px;
395 margin-left: 5px;
396}
397.closebtn:hover {
398 background-color: #FD5E59;
399}
400
401.progress {
402 width: 200px;
403 font-size: 0.7em;
404}
405.dlgProgress {
406 border: 1px solid black;
407 box-shadow: 6px 6px 10px 4px rgba(0,0,0,0.2);
408 border-radius: 6px;
409 display: none;
410}
411
412.empty-dragrow td{
413 border: 2px dashed #6bb28c;
414 background-color: white;
415}
416tr.dragstart td{
417 background-color: gray;
418 color: white;
419}
420tr.dragrow td{
421 overflow: hidden;
422 text-overflow: ellipsis;
423 max-width: calc(10em - 30px);
424}
425#nextFNContainer img {
426 cursor: pointer;
427 height: 1em;
428 vertical-align: center;
429}
430`;
431
432// Implement colors and styles from KeywordsGroups in CSS
433for (const group in mslDefs.groups) {
434 const { mslclass, color, fontWeight } = mslDefs.groups[group];
435
436 if (mslclass) {
437 seq_css += `.${mslclass} { color: ${color}; font-weight: ${fontWeight}; }\n`;
438 }
439}
440
441const seqStyle = document.createElement('style');
442seqStyle.textContent = seq_css;
443document.head.appendChild(seqStyle);
444
445// line connector string
446const lc = "\n";
447// revisions array, maximum nRevisions
448const previousRevisions = {};
449const nRevisions = 20;
450var revisionIndex = {};
451var saveRevision = {};
452// Meta combo keydown flag
453var MetaCombo = false;
454
455// -- Sequencer specific functions --
456// Setup the correct sequencer state visually
457function seqState(funcCall) {
458 /* Arguments:
459 funcCall - (optional) a function to be called when the state is set (with the state text)
460 */
461 let stateText = "Stopped";
462 // Check sequence state
463 mjsonrpc_db_get_values(["/Sequencer/State/Running","/Sequencer/State/Paused","/Sequencer/State/Finished","/Sequencer/State/Debug"]).then(function(rpc) {
464 if (rpc.result.data[0] && !rpc.result.data[1] && !rpc.result.data[2]) {
465 stateText = "Running";
466 } else if (rpc.result.data[1] && rpc.result.data[0] && !rpc.result.data[2]) {
467 stateText = "Paused";
468 } else {
469 stateText = "Stopped";
470 }
471 if (funcCall) {
472 funcCall(stateText);
473 }
474 return stateText;
475 }).catch (function (error) {
476 console.error(error);
477 return "Unknown";
478 });
479}
480
481// Ask user to edit current sequence
482function askToEdit(flag,event) {
483 if (flag) {
484 openETab(document.getElementById("etab1-btn"));
485 const [lineNumbers,editor,btnLabel,label] = editorElements();
486 // make editable and add event listeners
487 //editor.contentEditable = true;
488 addETab(document.getElementById("addETab"));
489 seqOpen(label.title.split("\n")[0]);
490 event.stopPropagation();
491 return;
492 }
493 const message = "To edit the sequence it must be opened in an editor tab.<br>Would you like to proceed?";
494 dlgConfirm(message,function(resp) {
495 if (resp) {
496 const label = editorElements()[3];
497 addETab(document.getElementById("addETab"));
498 seqOpen(label.title.split("\n")[0]);
499 }
500 });
501}
502
503// Enable editing of sequence
504function editorEventListeners() {
505 let [lineNumbers,editor] = editorElements();
506 editor.contentEditable = true;
507 // Attached syntax highlight event editor
508 editor.addEventListener("keydown",checkSyntaxEventDown);
509 editor.addEventListener("keyup",checkSyntaxEventUp);
510 editor.addEventListener("paste", checkSyntaxEventPaste);
511 // Change in text
512 editor.addEventListener("input", function() {
513 seqIsChanged(true);
514 });
515 document.addEventListener("selectionchange", function(event) {
516 if (event.target.activeElement === editor) markCurrLineNum();
517 });
518 // Synchronize the scroll position of lineNumbers with editor
519 editor.addEventListener("scroll", function() {
520 lineNumbers.scrollTop = editor.scrollTop;
521 });
522}
523
524// apply changes of filename in the ODB (triggers reload)
525function seqChange(filename) {
526 /* Arguments:
527 filename - full file name with path to change
528 */
529 if (!filename) return;
530 const lastIndex = filename.lastIndexOf('/');
531 const path = filename.substring(0, lastIndex).replace(new RegExp('^' + fnPath),"").replace(/\/+/g, '/').replace(/^\/+|\/+$/g, '');
532 const file = filename.substring(lastIndex + 1);
533 // set path and filename, wait for completion and return
534 mjsonrpc_db_paste(["/Sequencer/State/Path","/Sequencer/State/Filename"],[path,file]).then(function (rpc1) {
535 sessionStorage.removeItem("depthDir");
536 if (rpc1.result.status[0] === 1 && rpc1.result.status[1] === 1) {
537 mjsonrpc_db_paste(["/Sequencer/Command/Load new file"],[true]).then(function (rpc2) {
538 if (rpc2.result.status[0] === 1) {
539 return;
540 }
541 }).catch(function (error) {console.error(error);});
542 } else {
543 dlgAlert("Something went wrong, I could not set the filename!");
544 }
545 }).catch(function (error) {console.error(error);});
546}
547
548// Save sequence text in filename.
549function seqSave(filename) {
550 /* Arguments:
551 filename (opt) - save to provided filename with path. If undefined save to original
552 filename and if empty trigger file_picker.
553 */
554 let [lineNumbers,editor,label] = editorElements();
555 let text = editor.innerText;
556 if (editor.id !== "editorTab1") {
557 if (filename === undefined) {
558 // take name from button title
559 filename = label.title;
560 if (filename.endsWith(".msl")) {
561 file_save_ascii_overwrite(filename,text);
562 seqIsChanged(false);
563 } else {
564 seqSave("");
565 }
566 } else if (filename === "") {
567 filename = label.title;
568 let file = filename.split("/").pop();
569 let path = filename.substring(0, filename.lastIndexOf("/") + 1).replace(/\/+/g, '/').replace(/^\/+|\/+$/g, '');
570 // If file/path are empty start with default value
571 if (path === "")
572 path = sessionStorage.getItem("pathName") ? sessionStorage.getItem("pathName") : fnPath + "/";
573 if (file === "")
574 file = sessionStorage.getItem("fileName") ? sessionStorage.getItem("fileName") : "filename.msl";
575 file_picker(path,fnExt,seqSave,true,{},true);
576 } else {
577 file_save_ascii_overwrite(filename,text);
578 let file = filename.split("/").pop();
579 let path = filename.substring(0, filename.lastIndexOf("/") + 1).replace(/\/+/g, '/').replace(/^\/+|\/+$/g, '');
580 label.title = filename;
581 label.innerText = file;
582 sessionStorage.setItem("fileName",file);
583 sessionStorage.setItem("pathName",path);
584 seqIsChanged(false);
585 }
586 // Check if filename is currently in editorTab1 and reload
587 let currFilename = document.getElementById("etab1-btn").title;
588 if (filename == currFilename) {
589 modbset("/Sequencer/Command/Load new file",true);
590 }
591 return;
592 }
593}
594
595// Open filename
596function seqOpen(filename) {
597 /* Arguments:
598 filename - file name to open (empty trigger file_picker)
599 */
600 // if a full filename is provided, open and return
601 if (filename && filename !== "") {
602 // Identify active tab
603 let [lineNumbers,editor,label] = editorElements();
604 // Check the option to open in new tab
605 if (document.getElementById("inNewTab").checked && editor.id !== "editorTab1") {
606 if (label.title !== "") { // if the tab is empty open new file there
607 addETab(document.getElementById("addETab"));
608 [lineNumbers,editor,label] = editorElements();
609 }
610 }
611 let file = filename.split("/").pop();
612 let path = filename.substring(0, filename.lastIndexOf("/") + 1).replace(/\/+/g, '/').replace(/^\/+|\/+$/g, '');
613 label.title = filename.replaceAll(/\/+/g, '/');
614 label.innerText = file;
615 sessionStorage.setItem("fileName",file);
616 sessionStorage.setItem("pathName",path);
617 if (editor.id === "editorTab1") {
618 seqChange(filename);
619 } else {
620 file_load_ascii(filename, function(text) {
621 editor.innerHTML = syntax_msl(text).join(lc).slice(0,-1);
622 updateLineNumbers(lineNumbers,editor);
623 });
624 }
625 return;
626 } else {
627 // empty or undefined file name
628 mjsonrpc_db_get_values(["/Sequencer/State/Path"]).then(function(rpc) {
629 let path = fnPath + "/" + rpc.result.data[0].replace(/\/+/g, '/').replace(/^\/+|\/+$/g, '');
630 sessionStorage.setItem("pathName",path);
631 file_picker(path,fnExt,seqOpen,false);
632 }).catch(function (error) {
633 mjsonrpc_error_alert(error);
634 });
635}
636}
637
638// Show/hide buttons according to sequencer state
639function updateBtns(state) {
640 /* Arguments:
641 state - the state of the sequencer
642 */
643 const seqState = {
644 Running: {
645 color: "var(--mgreen)",
646 },
647 Stopped: {
648 color: "var(--mred)",
649 },
650 Editing: {
651 color: "var(--mred)",
652 },
653 Paused: {
654 color: "var(--myellow)",
655 },
656 }
657 const color = seqState[state].color;
658 const seqStateSpans = document.querySelectorAll('.seqstate');
659 seqStateSpans.forEach(e => {e.style.backgroundColor = color;});
660 // hide all buttons
661 const hideBtns = document.querySelectorAll('.seqbtn');
662 hideBtns.forEach(button => {
663 button.style.display = "none";
664 });
665 // then show only those belonging to the current state
666 const showBtns = document.querySelectorAll('.seqbtn.' + state);
667 showBtns.forEach(button => {
668 if (button.tagName === "IMG") {
669 button.style.display = "inline-block";
670 } else {
671 button.style.display = "flex";
672 }
673 });
674 // Hide progress modal when stopped
675 const hideProgress = document.getElementById("Progress");
676 if (state === "Stopped") hideProgress.style.display = "none";
677}
678
679// Show sequencer messages if present
680function mslMessage(message) {
681 // Empty message, return
682 if (!message) return;
683 // Check message and message wait
684 mjsonrpc_db_get_values(["/Sequencer/State/Message","/Sequencer/State/Message Wait"]).then(function(rpc) {
685 const message = rpc.result.data[0];
686 const hold = rpc.result.data[1];
687 if (hold) {
688 dlgMessage("Message", message, true, false,clrMessage);
689 } else {
690 dlgAlert(message);
691 }
692 }).catch (function (error) {
693 console.error(error);
694 });
695}
696
697// Clear sequencer messages
698function clrMessage() {
699 mjsonrpc_db_paste(["Sequencer/State/Message"], [""]).then(function (rpc) {
700 return;
701 }).catch(function (error) {
702 console.error(error);
703 });
704}
705
706// Adjust size of sequencer editor according to browser window size
707function windowResize() {
708 const [lineNumbers,editor] = editorElements();
709 const seqTable = document.getElementById("seqTable");
710 editor.style.height = document.documentElement.clientHeight - editor.getBoundingClientRect().top - 20 + "px";
711 editor.style.maxWidth = document.documentElement.clientWidth - editor.getBoundingClientRect().left - 20 + "px";
712 seqTable.style.width = (window.innerWidth - seqTable.getBoundingClientRect().left - 10) + "px";
713 seqTable.style.maxWidth = (window.innerWidth - seqTable.getBoundingClientRect().left - 10) + "px";
714 seqTable.style.height = (window.innerHeight - seqTable.getBoundingClientRect().top - 10) + "px";
715 seqTable.style.maxHeight = (window.innerHeight - seqTable.getBoundingClientRect().top - 10) + "px";
716}
717
718// Load the current sequence from ODB (only on main tab)
719function load_msl() {
720 const editor = document.getElementById("editorTab1");
721 const btn = document.getElementById("etab1-btn");
722 mjsonrpc_db_get_values(["/Sequencer/Script/Lines","/Sequencer/State/Running","/Sequencer/State/SCurrent line number","/Sequencer/State/Filename","/Sequencer/State/SFilename","/Sequencer/State/Path"]).then(function(rpc) {
723 let seqLines = rpc.result.data[0];
724 let seqState = rpc.result.data[1];
725 let currLine = rpc.result.data[2];
726 let filename = rpc.result.data[3];
727 //if (rpc.result.data[4] === null) dlgAlert("You may be using an old sequencer.<br>Please recompile and/or restart it.");
728 let sfilename = rpc.result.data[4] ? rpc.result.data[4].split('userfiles/sequencer/')[1] : "";
729 filename = (fnPath + "/" + rpc.result.data[5] + "/" + filename).replace("//","/");
730 // syntax highlight
731 if (seqLines) {
732 editor.innerHTML = syntax_msl(seqLines).join(lc);
733 updateLineNumbers(editor.previousElementSibling,editor);
734 if (seqState) hlLine(currLine);
735 }
736 let file = filename.split("/").pop();
737 let path = filename.substring(0, filename.lastIndexOf("/") + 1).replace(/\/+/g, '/').replace(/^\/+|\/+$/g, '');
738 sessionStorage.setItem("fileName",file);
739 sessionStorage.setItem("pathName",path);
740 // change button title to add sfilename if present
741 btn.title = (sfilename && sfilename !== filename) ? filename + "\n" + sfilename : filename;
742 }).catch (function (error) {
743 console.error(error);
744 });
745}
746
747// Highlight (background color) and scroll to current line
748function hlLine(lineNum,color) {
749 /* Arguments:
750 lineNum - the line number to be highlighted
751 color - (optional) background color
752 */
753 const lineId = "sline" + lineNum;
754 const lineHTML = document.getElementById(lineId);
755
756 // Remove highlight from all lines with the class "msl_current_line"
757 const highlightedLines = document.querySelectorAll(".msl_current_line");
758 highlightedLines.forEach((line) => line.classList.remove("msl_current_line"));
759
760 if (lineHTML) {
761 lineHTML.classList.add("msl_current_line");
762 if (color) lineHTML.style.backgroundColor = color;
763 // Scroll to the highlighted line if the checkbox is checked
764 const scrollToCurrCheckbox = document.getElementById("scrollToCurr");
765 if (scrollToCurrCheckbox && scrollToCurrCheckbox.checked) {
766 lineHTML.scrollIntoView({ block: "center" });
767 }
768 }
769}
770
771// Scroll to make line appear in the center of editor
772function scrollToCurr(scrToCur) {
773 if (scrToCur.checked) {
774 localStorage.setItem("scrollToCurr",true);
775 const currLine = document.querySelector(".msl_current_line");
776 if (currLine) {
777 currLine.scrollIntoView({ block: "center" });
778 }
779 } else {
780 localStorage.removeItem("scrollToCurr",true);
781 }
782}
783
784// Open files in new tabs
785function toggleCheck(e) {
786 if (e.checked) {
787 localStorage.setItem(e.id,true);
788 } else {
789 localStorage.removeItem(e.id);
790 }
791}
792
793// shortcut event handling to overtake default behaviour
794function shortCutEvent(event) {
795 const parEditor = editorElements();
796 const notFirstTab = (parEditor[1].id !== "editorTab1");
797 if (notFirstTab) {
798 // Check these only for editors
799 if (event.altKey && event.key === 's') {
800 event.preventDefault();
801 //save as with file_picker
802 seqSave('');
803 event.preventDefault();
804 } else if ((event.ctrlKey || event.metaKey) && event.key === 's') {
805 event.preventDefault();
806 //save to the same filename
807 seqSave();
808 event.preventDefault();
809 } else if ((event.ctrlKey || event.metaKey) && event.key === 'z') {
810 event.preventDefault();
811 undoEdit(event.target);
812 event.preventDefault();
813 } else if ((event.ctrlKey || event.metaKey) && event.key === 'r') {
814 event.preventDefault();
815 redoEdit(event.target);
816 event.preventDefault();
817 }
818 }
819 if (!notFirstTab) {
820 // Check these only for first tab
821 if ((event.ctrlKey || event.metaKey) && event.key === 'e') {
822 // open new tab and load current sequence
823 event.preventDefault();
824 addETab(document.getElementById("addETab"));
825 seqOpen(parEditor[3].title.split("\n")[0]);
826 event.preventDefault();
827 }
828 }
829 // Check these for all tabs
830 if (event.altKey && event.key === 'n') {
831 // open new tab
832 event.preventDefault();
833 addETab(document.getElementById("addETab"));
834 event.preventDefault();
835 } else if (event.altKey && event.key === 'o') {
836 event.preventDefault();
837 seqOpen();
838 event.preventDefault();
839 }
840
841}
842
843// Trigger syntax highlighting on keyup events
844function checkSyntaxEventUp(event) {
845 if (event.ctrlKey || event.altKey || event.metaKey || MetaCombo) return;
846 if (event.keyCode >= 0x30 || event.key === ' '
847 || event.key === 'Backspace' || event.key === 'Delete'
848 || event.key === 'Enter'
849 ) {
850 const e = event.target;
851 let caretPos = getCurrentCursorPosition(e);
852 let currText = e.innerText;
853 // Unclear why sometimes two \n are added
854 if (currText === sessionStorage.getItem("keydown") + "\n\n") {
855 e.innerText = currText.slice(0,-1);
856 currText = currText.slice(0,-1);
857 }
858 // save current revision for undo
859 if (event.key === ' ' || event.key === 'Backspace' || event.key === 'Delete' || event.key === 'Enter') {
860 saveState(currText,e);
861 }
862
863 // Indent according to previous line
864 if (event.key === 'Enter') {
865 event.preventDefault();
866 e.innerHTML = syntax_msl(e.innerText).join(lc).slice(0,-1);
867 setCurrentCursorPosition(e, caretPos);
868 // get previous and current line elements (before and after enter)
869 let pline = whichLine(e,-1);
870 let cline = whichLine(e);
871 let plineText = (pline) ? pline.innerText : "";
872 let clineText = (cline) ? cline.innerText : "";
873 let indentLevel = 0;
874 let preSpace = 0;
875 let indentString = "";
876 if (plineText) {
877 // indent line according to the previous line text
878 // if, loop, else, subroutine - increase indentation
879 const indentPlus = defIndent.indentplus.some(keyword => plineText.trim().toLowerCase().startsWith(keyword.toLowerCase()));
880 // else, endif, endloop, endsubroutine - decrease indentation
881 const indentMinos = defIndent.indentminos.some(keyword => plineText.trim().toLowerCase().startsWith(keyword.toLowerCase()));
882 /* (indentMinos/indentPlus)
883 true/false - pline indent -1, cline indent 0
884 fale/true - pline indent 0, cline indent +1
885 true/true - pline indent -1, cline indent +1
886 false/false- pline indent 0, cline indent 0
887 */
888 // Count number of white spaces at begenning of pline
889 preSpace = plineText.replace("\n","").search(/\S|$/);
890 pPreSpace = preSpace - indentMinos * 3;
891 if (pPreSpace < 0) pPreSpace = 0;
892 cPreSpace = pPreSpace + indentPlus * 3;
893 // Calculate and insert indentation
894 pIndentString = " ".repeat(pPreSpace);
895 cIndentString = " ".repeat(cPreSpace);
896 cline.innerText = cIndentString + clineText.trimStart();
897 caretPos += cline.innerText.length - clineText.length;
898 pline.innerText = pIndentString + plineText.trimStart();
899 caretPos += pline.innerText.length - plineText.length;
900 }
901 event.preventDefault();
902 }
903 e.innerHTML = syntax_msl(e.innerText).join(lc).slice(0,-1);
904 setCurrentCursorPosition(e, caretPos);
905 updateLineNumbers(e.previousElementSibling,e);
906 e.focus();
907 }
908 return;
909}
910
911
912// Trigger syntax highlighting on keydown events
913function checkSyntaxEventDown(event) {
914 sessionStorage.setItem("keydown",event.target.innerText);
915 // take care of Mac odd keyup behaviour
916 if (event.metaKey && (/^[a-z]$/.test(event.key) || event.shiftKey || event.altKey)) {
917 MetaCombo = true;
918 } else {
919 MetaCombo = false;
920 }
921
922 if (event.ctrlKey || event.altKey || event.metaKey) return;
923 // Only pass indentation related keys
924 if (event.key !== 'Tab' && event.key !== 'Escape') return;
925 event.preventDefault();
926 let e = event.target;
927 let caretPos = getCurrentCursorPosition(e);
928 let currText = e.innerText;
929 let lines = getLinesInSelection(e);
930 if (event.shiftKey && event.key === 'Tab') {
931 indent_msl(lines,-1);
932 } else if (event.key === 'Tab') {
933 indent_msl(lines,+1);
934 } else if (event.key === 'Escape') {
935 indent_msl(lines);
936 }
937 e.innerHTML = syntax_msl(e.innerText).join(lc).slice(0,-1);
938 let newText = e.innerText;
939 setCurrentCursorPosition(e, caretPos + newText.length - currText.length);
940 if (lines[0] !== lines[1]) selectLines(lines,e);
941 event.preventDefault();
942 return;
943}
944
945// Trigger syntax highlighting when you paste text
946function checkSyntaxEventPaste(event) {
947 // set time out to allow default to go first
948 setTimeout(() => {
949 let e = event.target;
950 // make sure you paste in the editor area
951 if (e.tagName !== 'PRE') e = e.parentElement;
952 let caretPos = getCurrentCursorPosition(e);
953 let currText = e.innerText;
954 // save current revision for undo
955 saveState(currText,e);
956 e.innerHTML = syntax_msl(currText).join(lc).slice(0,-1);
957 setCurrentCursorPosition(e, caretPos);
958 updateLineNumbers(e.previousElementSibling,e);
959 e.focus();
960 }, 0);
961 return;
962}
963
964// Find on which line is the current carret position in e
965// This assumes each line has an id="sline#" where # is the line number.
966function whichLine(e,offset = 0) {
967 // offset allows to pick previous line (after enter)
968 let pos = getCurrentCursorPosition(e);
969 if (!pos) return;
970 let lineNum = e.innerText.substring(0,pos).split("\n").length + offset;
971 let sline = e.querySelector("#sline" + lineNum.toString());
972 return sline;
973}
974
975// Return an array with the first and last line numbers of the selected region
976/* This assumes that the lines are in a <pre> element and that
977 each line has an id="sline#" where # is the line number.
978 When the caret in in an empty line, the anchorNode is the <pre> element.
979*/
980function getLinesInSelection(e) {
981 const selection = window.getSelection();
982 if (selection.rangeCount === 0) return [0,0];
983 // is it a single line?
984 const singleLine = selection.isCollapsed;
985 if (singleLine) {
986 const line = whichLine(e);
987 if (line) {
988 const startLine = parseInt(line.id.replace("sline",""));
989 return [startLine,startLine];
990 } else {
991 return [0,0];
992 }
993 }
994 const anchorNode = selection.anchorNode;
995 const range = selection.getRangeAt(0);
996 let startNode,endNode;
997 if (anchorNode.tagName === 'PRE') {
998 let startOffset = range.startOffset;
999 let endOffset = range.endOffset;
1000 startNode = range.startContainer.childNodes[startOffset];
1001 endNode = range.startContainer.childNodes[endOffset-1];
1002 } else {
1003 startNode = (range.startContainer && range.startContainer.parentElement.tagName !== 'PRE') ? range.startContainer : range.startContainer.nextSibling;
1004 if (startNode && startNode.tagName === 'PRE') startNode = startNode.firstChild;
1005 endNode = (range.endContainer && range.endContainer.parentElement.tagName !== 'PRE') ? range.endContainer : range.endContainer.previousSibling;
1006 if (endNode && endNode.tagName === 'PRE') endNode = endNode.lastChild;
1007 }
1008 let startID = (startNode && startNode.id) ? startNode.id : "";
1009 let endID = (endNode && endNode.id) ? endNode.id : "";
1010 // get first line
1011 while (startNode && !startID.startsWith("sline") && startNode.tagName !== 'PRE') {
1012 startNode = (startNode.parentNode.tagName !== 'PRE') ? startNode.parentNode : startNode.nextSibling;
1013 startID = (startNode && startNode.id) ? startNode.id : "";
1014 }
1015 // get last line
1016 while (endNode && !endID.startsWith("sline") && endNode.tagName !== 'PRE') {
1017 endNode = (endNode.parentNode.tagName !== 'PRE') ? endNode.parentNode : endNode.previousSibling;
1018 endID = (endNode && endNode.id) ? endNode.id : "";
1019 }
1020 let startLine = (startNode && startNode.id) ? parseInt(startNode.id.replace("sline","")) : 0;
1021 let endLine = (endNode && endNode.id) ? parseInt(endNode.id.replace("sline","")) : 0;
1022 if (singleLine) {
1023 startLine = endLine = Math.min(startLine, endLine);
1024 }
1025 return [startLine,endLine];
1026}
1027
1028// get current caret position in chars within element parent
1029function getCurrentCursorPosition(parent) {
1030 let sel = window.getSelection();
1031 if (!sel.focusNode || !parent) return;
1032 const range = sel.getRangeAt(0);
1033 const prefix = range.cloneRange();
1034 prefix.selectNodeContents(parent);
1035 prefix.setEnd(range.endContainer, range.endOffset);
1036 return prefix.toString().length;
1037}
1038
1039// set current caret position at pos within element parent
1040function setCurrentCursorPosition(parent,pos) {
1041 for (const node of parent.childNodes) {
1042 if (node.nodeType == Node.TEXT_NODE) {
1043 if (node.length >= pos) {
1044 const range = document.createRange();
1045 const sel = window.getSelection();
1046 range.setStart(node, pos);
1047 range.collapse(true);
1048 sel.removeAllRanges();
1049 sel.addRange(range);
1050 return -1;
1051 } else {
1052 pos = pos - node.length;
1053 }
1054 } else {
1055 pos = setCurrentCursorPosition(node, pos);
1056 if (pos < 0) {
1057 return pos;
1058 }
1059 }
1060 }
1061 return pos;
1062}
1063
1064// Update line numbers in lineNumbers div
1065function updateLineNumbers(lineNumbers,editor) {
1066 if (lineNumbers === undefined || editor === undefined)
1067 [lineNumbers,editor] = editorElements();
1068 // Clear existing line numbers
1069 lineNumbers.innerHTML = "";
1070 // Get the number of lines accurately
1071 let lineCount = editor.querySelectorAll('[id^="sline"]').length;
1072 let lineTextCount = editor.innerText.split("\n").length;
1073 lineCount = (lineTextCount - lineCount) < 2 ? lineTextCount : lineTextCount - 1;
1074 // Add line numbers to lineNumbers
1075 for (let i = 1; i <= lineCount; i++) {
1076 const lineNumber = document.createElement('span');
1077 lineNumber.id = "lNum" + i.toString();
1078 lineNumber.textContent = i;
1079 lineNumbers.appendChild(lineNumber);
1080 }
1081 lineNumbers.scrollTop = editor.scrollTop;
1082 windowResize();
1083}
1084
1085// Utility function to escape special characters in a string for use in a regular expression
1086function escapeRegExp(s) {
1087 if (!s) return "";
1088 return s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
1089}
1090
1091// Syntax highlight any text according to provided rules
1092function syntax_msl(seqLines,keywordGroups) {
1093 // If not provided use the default msl keywords and groups
1094 if (!keywordGroups) {
1095 // take msl default
1096 keywordGroups = mslDefs.groups;
1097 }
1098
1099 let mslText = Array.isArray(seqLines) ? seqLines.join("\n") : seqLines;
1100 // Make some cleanup of illegal characters
1101 mslText = escapeSpecialCharacters(mslText);
1102 // Keep original sequence lines (as array)
1103 let seqLines_org = mslText.split(/\n/);
1104 // Make full text if you get an array of lines
1105 let nLines = (mslText.match(/\n/g) || []).length;
1106 // These can be done on the text in one go
1107 // Strings
1108 let reg = /(["'])(.*?)\1/g;
1109 mslText = mslText.replace(reg,'<span class="msl_string">$1$2$1</span>');
1110
1111 // Comments
1112 //reg = /^(COMMENT|#.*?)(.*)$/gim;
1113 //mslText = mslText.replace(reg,'<span class="msl_comment">$&</span>');
1114
1115 // Variables
1116 //reg = /(\$[\w]+|^\s*[\w]+(?=\s*=))/gm;
1117 reg = /(?:\$[\w]+|^\b\w+(?=\s*=))/gm; // starting with $ or something =
1118 mslText = mslText.replace(reg,'<span class="msl_variable">$&</span>');
1119 reg = new RegExp("(^(?:\\s*)\\b(PARAM|CAT|SET)\\s+)(\\w+)\\b", "gim"); // after PARAM, CAT and SET
1120 mslText = mslText.replace(reg,'$1<span class="msl_variable">$3</span>');
1121
1122 // Data Management group excluding variables (must be after variables)
1123 reg = new RegExp("^(\\s*)\\b(" + keywordGroups.dataManagement.keywords.join("|") + ")\\b", "gim");
1124 mslText = mslText.replace(reg, "$1<span class='msl_data_management'>$2</span>");
1125
1126 // Data Type group (must have comma before the keyword)
1127 reg = new RegExp("(?<=,\\s*)\\b(" + keywordGroups.dataTypes.keywords.join("|") + ")\\b", "gim");
1128 mslText = mslText.replace(reg, "<span class='msl_data_types'>$1</span>");
1129
1130 // Loops group (must be at the begenning of the line)
1131 reg = new RegExp("^(\\s*)\\b(" + keywordGroups.loops.keywords.join("|") + ")\\b", "gim");
1132 mslText = mslText.replace(reg, "$1<span class='msl_loops'>$2</span>");
1133
1134 // Control Flow group
1135 reg = new RegExp("^(\\s*)\\b(" + keywordGroups.controlFlow.keywords.join("|") + ")\\b", "gim");
1136 mslText = mslText.replace(reg, "$1<span class='msl_control_flow'>$2</span>");
1137
1138 // Data Management group
1139 reg = new RegExp("^(\\s*)\\b(" + keywordGroups.dataManagement.keywords.join("|") + ")\\b", "gim");
1140 mslText = mslText.replace(reg, "$1<span class='msl_data_managemen'>$2</span>");
1141
1142 // Information group
1143 reg = new RegExp("^(\\s*)\\b(" + keywordGroups.info.keywords.join("|") + ")\\b", "gim");
1144 mslText = mslText.replace(reg, "$1<span class='msl_info'>$2</span>");
1145
1146 // Conditional group
1147 reg = new RegExp("^(\\s*)\\b(" + keywordGroups.cond.keywords.join("|") + ")\\b", "gim");
1148 mslText = mslText.replace(reg,"$1<span class='msl_cond'>$2</span>");
1149
1150 // Units group
1151 reg = new RegExp("\\b(" + keywordGroups.units.keywords.join("|") + ")\\b", "gi");
1152 mslText = mslText.replace(reg, "<span class='msl_units'>$1</span>");
1153
1154 // Action group
1155 reg = new RegExp("\\b(" + keywordGroups.actions.keywords.join("|") + ")\\b(\\s*)$", "gim");
1156 mslText = mslText.replace(reg, "<span class='msl_actions'>$1</span>$2");
1157
1158 // Numbers/boolean group
1159 reg = /\b(\d+(\.\d+)?([eE][-+]?\d+)?)\b/g;
1160 mslText = mslText.replace(reg, '<span class="msl_number">$1</span>');
1161 reg = /\b(true|false)\b/gi;
1162 mslText = mslText.replace(reg, '<span class="msl_bool">$1</span>');
1163
1164 // Break lines and handle one by one
1165 seqLines = mslText.split("\n");
1166
1167 // This is important for Firefox
1168 let emptyClass = "";
1169 let isChrome = browserType();
1170 if (isChrome === 1) emptyClass = "esline";
1171 // Loop and restore comment lines and empty lines
1172 for (let j = 0; j < seqLines_org.length ; j++) {
1173 let line = seqLines_org[j];
1174 commentIndex = line.indexOf("#");
1175 if (line.trim().startsWith("#") || line.trim().toLowerCase().startsWith("comment")) {
1176 // Restore comment lines without highlighting
1177 seqLines[j] = `<span class='msl_comment'>${line}</span>`;
1178 } else if (commentIndex > 0) {
1179 // Restore comment section at end of line
1180 const comment = line.slice(commentIndex);
1181 seqLines[j] = seqLines[j].slice(0, seqLines[j].indexOf("#")) + `</span><span class='msl_comment'>${comment}</span>`;
1182 }
1183
1184 // empty class is needed for cursor movement in Firefox
1185 // for Chrome empty lines are skipped with arrow up??
1186 if (line === "") {
1187 if ((j === seqLines_org.length - 1) && (isChrome === 1)) {
1188 seqLines[j] = "<span class=' ' id='sline" + (j+1).toString() + "'></span>";
1189 } else if (isChrome === 1) {
1190 seqLines[j] = "<span class='" + emptyClass + "' id='sline" + (j+1).toString() + "'></span>";
1191 } else {
1192 seqLines[j] = "<span class='" + emptyClass + "' id='sline" + (j+1).toString() + "'></span>";
1193 }
1194 } else {
1195 seqLines[j] = "<span class='sline' id='sline" + (j+1).toString() + "'>" + seqLines[j] + "</span>";
1196 }
1197 }
1198 return seqLines;
1199}
1200
1201// Adjust indentation of a selection of lines
1202function indent_msl(lines,addTab) {
1203 /* Arguments:
1204 lines - an array of two elements, first and last line numbers
1205 addTab - (opt) +/-1 to add/subtract three spaces to selected lines
1206 */
1207 let indentLevel = 0;
1208 let singleLine = false;
1209 let editor = editorElements()[1];
1210 // Avoid issues of begenning of single line
1211 if (lines[0] > lines[1] || lines[0] == lines[1]) {
1212 lines[0] = lines[0] > 0 ? lines[0] : 1;
1213 lines[1] = lines[0];
1214 singleLine = true;
1215 }
1216 for (let j = lines[0]; j <= lines[1] ; j++) {
1217 let lineId = "#sline" + j.toString();
1218 let prevLineId = "#sline" + (j-1).toString();
1219 let lineEl = editor.querySelector(lineId);
1220 let line = "";
1221 if (lineEl) line = lineEl.innerText;
1222 if (addTab === 1) {
1223 let indentString = " ".repeat(3);
1224 lineEl.innerText = indentString + line;
1225 } else if (addTab === -1) {
1226 lineEl.innerText = line.replace(/^\s{1,3}/, '');
1227 } else if (singleLine && editor.querySelector(prevLineId)) {
1228 let prevLineEl = editor.querySelector(prevLineId);
1229 let prevLine = prevLineEl.innerText;
1230 const indentMinos = defIndent.indentminos.some(keyword => prevLine.trim().toLowerCase().startsWith(keyword.toLowerCase()));
1231 const indentPlus = defIndent.indentplus.some(keyword => prevLine.trim().toLowerCase().startsWith(keyword.toLowerCase()));
1232 if (indentMinos) {
1233 indentLevel = -1;
1234 } else if (indentPlus) {
1235 indentLevel = 1;
1236 }
1237 let preSpace = prevLine.search(/\S|$/) + (indentLevel * 3);
1238 if (preSpace < 0) preSpace = 0;
1239 let indentString = " ".repeat(preSpace);
1240 lineEl.innerText = indentString + line.trimStart();
1241 } else {
1242 const indentMinos = defIndent.indentminos.some(keyword => line.trim().toLowerCase().startsWith(keyword.toLowerCase()));
1243 if (indentMinos && indentLevel > 0) indentLevel--;
1244 let indentString = " ".repeat(indentLevel * 3);
1245 if (line !== "" || indentString !== "") {
1246 lineEl.innerText = indentString + line.trimStart();
1247 }
1248 const indentPlus = defIndent.indentplus.some(keyword => line.trim().toLowerCase().startsWith(keyword.toLowerCase()));
1249 if (indentPlus) indentLevel++;
1250 }
1251 }
1252}
1253
1254// Prepare the parameters/variables (if present) from the ODB as an html table
1255// Also return default ODB Paths and their respective values
1256function varTable(id,odbTreeVar) {
1257 /* Arguments:
1258 id - ID of div to fill with the table of variables
1259 odbTreeVar - values of /Sequencer/Variables
1260 */
1261
1262 let e = document.getElementById(id);
1263 if (e === null) {
1264 dlgAlert("Container ID was not give.");
1265 return;
1266 }
1267 let nVars = 1,nVarsOld = 0;
1268 let oldTable = document.getElementById("varTable");
1269 if (oldTable) nVarsOld = oldTable.rows.length;
1270 // If /Sequencer/Variables are empty return empty
1271 if (!odbTreeVar) {
1272 // Clear container row
1273 e.innerHTML = "";
1274 return;
1275 }
1276 let html = "<table id='varTable' class='mtable infotable'>\n";
1277 html += "<tr><th style='width: 120px'>Variable&nbsp;&nbsp;</th><th style='width: 200px'>Current value&nbsp;&nbsp;</th></tr>\n";
1278
1279 // Go over all variables in ODB and count them
1280 let varCount = Object.keys(odbTreeVar).length;
1281 for (let key in odbTreeVar) {
1282 const match = key.match(/([^/]+)\/name$/);
1283 if (match) {
1284 nVars++;
1285 const name = match[1];
1286 const value = odbTreeVar[name];
1287 let isBool = (typeof(value) === "boolean");
1288 if (isBool) {
1289 html += `<tr><td>${name}</td><td><input type="checkbox" class="modbcheckbox" data-odb-path="/Sequencer/Variables/${name}"></span></td></tr>\n`;
1290 } else {
1291 html += `<tr><td>${name}</td><td><span class="modbvalue" data-odb-path="/Sequencer/Variables/${name}"></span></td></tr>\n`;
1292 }
1293 }
1294 }
1295 html += "</table>";
1296 if (nVars !== nVarsOld) {
1297 e.innerHTML = html;
1298 }
1299 return;
1300}
1301
1302// Prepare the parameters/variables (if present) from the ODB as an html table
1303// Also return default ODB Paths and their respective values
1304function parTable(odbTreeV,odbTreeC,odbTreeD,odbTreeO,odbTreeVar,editOnSeq) {
1305 /* Arguments:
1306 odbTree... - Objects of ODB values
1307 */
1308 let html = "";
1309 let odbDefPaths = [];
1310 let odbDefValues = [];
1311 let NValues = 0;
1312
1313 html += "<table id='paramTable' class='mtable infotable'>";
1314 html += "<tr><th>Parameter&nbsp;&nbsp;</th><th>Initial value&nbsp;&nbsp;</th><th>Comment</th></tr>";
1315
1316 const processParam = (name, value, isBool, defValue, optValue, comment) => {
1317 let parLine = `<tr><td>${name}</td>`;
1318 let inParLine = "";
1319 if (optValue) {
1320 // if not given the default is the first option
1321 if (defValue === undefined || defValue === "") defValue = optValue[0];
1322 const optionsHtml = optValue.map(option => `<option value="${option}" ${option === defValue ? 'selected' : ''}>${option}</option>`).join('');
1323 inParLine += `<select class="modbselect" data-odb-path="/Sequencer/Param/Value/${name}" data-odb-editable="1">${optionsHtml}</select>`;
1324 } else if (isBool) {
1325 inParLine += `<input type="checkbox" class="modbcheckbox" data-odb-path="/Sequencer/Param/Value/${name}" data-odb-editable="1"></input>`;
1326 } else {
1327 inParLine += `<span class="modbvalue" data-odb-path="/Sequencer/Param/Value/${name}" data-odb-editable="1" data-input="1"></span>`;
1328 }
1329 if (defValue !== undefined) {
1330 NValues++;
1331 odbDefPaths.push(`/Sequencer/Param/Value/${name}`);
1332 odbDefValues.push(defValue);
1333 }
1334
1335 parLine += `<td>${inParLine}</td>`;
1336 parLine += `<td>${comment}</td></tr>`;
1337 html += parLine;
1338 };
1339
1340 // Go over all parameters in ODB
1341 for (let key in odbTreeV) {
1342 const match = key.match(/([^/]+)\/name$/);
1343 if (match) {
1344 const name = match[1];
1345 // if variable is found use its value
1346 let value = (odbTreeVar && odbTreeVar[name]) ? odbTreeVar[name] : odbTreeV[name];
1347 let isBool = (typeof(value) === "boolean");
1348 let defValue = (value !== null && value !== undefined && value !== '') ? value : (odbTreeD && odbTreeD[name]) || value;
1349 let optValue = odbTreeO ? odbTreeO[name] : undefined;
1350 let comment = odbTreeC[name] || '';
1351 if (typeof value !== "object") {
1352 processParam(name, value, isBool, defValue, optValue, comment);
1353 }
1354 }
1355 }
1356
1357 // Go over Edit on sequence links
1358 for (let key in editOnSeq) {
1359 const match = key.match(/([^/]+)\/name$/);
1360 if (match) {
1361 const name = match[1];
1362 const value = editOnSeq[name];
1363 let isBool = (typeof(value) === "boolean");
1364 if (isBool) {
1365 html += `<tr><td>${name}</td><td><input type="checkbox" class="modbcheckbox" data-odb-path="/Experiment/Edit on sequence/${name}" data-odb-editable="1"></input></td><td></td></tr>\n`;
1366 } else {
1367 html += `<tr><td>${name}</td><td><span class="modbvalue" data-odb-path="/Experiment/Edit on sequence/${name}" data-odb-editable="1" data-input="1"></span></td><td></td></tr>\n`;
1368 }
1369 }
1370 }
1371
1372 html += "</table>";
1373 return {
1374 html: html,
1375 paths: odbDefPaths,
1376 values: odbDefValues,
1377 NValues: NValues
1378 };
1379}
1380
1381
1382// Prepare the parameters/variables (if present) from the ODB as a table
1383function dlgParam(debugFlag) {
1384 /* Arguments:
1385 debugFlag - (opt) true/false run in debug/normal mode
1386 */
1387
1388 let odbTree = JSON.parse(sessionStorage.getItem('parameters'));
1389 const editOnSeq = JSON.parse(sessionStorage.getItem('editonseq'));
1390 // If /Sequencer/Param are empty, start and return
1391 if (odbTree === null && editOnSeq === null ) {
1392 //if ((odbTree === null || Object.keys(odbTree).length) && (editOnSeq === null || Object.keys(editOnSeq).length)) {
1393 if (debugFlag) {
1394 modbset('/Sequencer/Command/Debug script',true);
1395 } else {
1396 modbset('/Sequencer/Command/Start script',true);
1397 }
1398 return;
1399 }
1400
1401 let odbTreeV = null;
1402 let odbTreeC = null;
1403 let odbTreeD = null;
1404 let odbTreeO = null;
1405 let odbTreeVar = null;
1406
1407 if (odbTree) {
1408 odbTreeV = odbTree.value;
1409 odbTreeC = odbTree.comment;
1410 odbTreeD = odbTree.defaults;
1411 odbTreeO = odbTree.options;
1412 odbTreeVar = JSON.parse(sessionStorage.getItem('variables'));
1413 }
1414
1415 // Go over all parameters in ODB
1416 let seqParTable = parTable(odbTreeV,odbTreeC,odbTreeD,odbTreeO,odbTreeVar,editOnSeq);
1417 let html = seqParTable.html;
1418 // set all default values and once finished produce dialog
1419 // Collect paths where values start with "/"
1420 let valuesLinkODB = seqParTable.values.map((valueLinkODB, indexLinkODB) => {
1421 if (valueLinkODB && valueLinkODB.startsWith("/")) {
1422 return seqParTable.values[indexLinkODB];
1423 } else {
1424 return null;
1425 }
1426 }).filter(path => path !== null);
1427 mjsonrpc_db_get_values(valuesLinkODB).then(function (rpc) {
1428 if (rpc.result.status.every(status => status === 1)) {
1429 // substitute values
1430 rpc.result.data.forEach((newData, index) => {
1431 let pathIndex = seqParTable.values.indexOf(valuesLinkODB[index]);
1432 seqParTable.values[pathIndex] = `${newData}`; // Update corresponding value
1433 });
1434 //console.log('Substituted seqParTable.values:', seqParTable.values);
1435 mjsonrpc_db_paste(seqParTable.paths,seqParTable.values).then(function (rpc) {
1436 if ((rpc.result.status.every(status => status === 1)) || seqParTable.values.length === 0) {
1437 //console.log("db_paste finished.\n",seqParTable.paths,seqParTable.values);
1438 // if parContainer not given produce a dialog
1439 let htmlDlg = `${html}<br><button class="dlgButtonDefault" id="dlgParamStart" type="button">Start</button><button class="dlgButton" id="dlgParamCancel" type="button">Cancel</button><br>`;
1440 let d = dlgGeneral({html: htmlDlg,iddiv: "Parameters",minWidth:500});
1441 let e = document.getElementById("parContainer");
1442 // Append the table to a container
1443 let startBtn = document.getElementById("dlgParamStart");
1444 let cancelBtn = document.getElementById("dlgParamCancel");
1445 cancelBtn.addEventListener("click", function () {d.remove();});
1446 startBtn.addEventListener("click", function () {
1447 d.remove();
1448 if (debugFlag) {
1449 modbset('/Sequencer/Command/Debug script',true);
1450 } else {
1451 modbset('/Sequencer/Command/Start script',true);
1452 }
1453 });
1454 } else {
1455 console.log("db_paste failed:",rpc.result.status," Trying again...");
1456 dlgParam(debugFlag);
1457 }
1458 }).catch(function (error) {
1459 console.error(error);
1460 });
1461 } else {
1462 let message = `ODB "${valuesLinkODB}" was not found.<br>Cannot start sequence!`;
1463 dlgAlert(message);
1464 }
1465 }).catch(function (error) {
1466 console.error(error);
1467 });
1468}
1469
1470
1471// helper debug function
1472function debugSeq(parContainer) {
1473 startSeq(parContainer,true);
1474}
1475
1476// helper start function
1477function startSeq(parContainer,debugFlag) {
1478 const [lineNumbers,editor,label] = editorElements();
1479 if (!debugFlag) debugFlag = false;
1480 if (editor.id !== "editorTab1" && parContainer === undefined) {
1481 let filename = label.title;
1482 if (!filename) {
1483 dlgAlert("Please give the file a name first (Save as).");
1484 return;
1485 }
1486 const message = debugFlag ? `Save and debug ${filename}?` : `Save and start ${filename}?`;
1487 dlgConfirm(message,function(resp) {
1488 if (resp) {
1489 seqSave(filename);
1490 openETab(document.getElementById("etab1-btn"));
1491 seqOpen(filename);
1492 // Make sure to load file an reset parameters
1493 mjsonrpc_db_paste(["/Sequencer/Command/Load new file"],[true]).then(function (rpc) {
1494 if (rpc.result.status[0] === 1) {
1495 dlgParam(debugFlag);
1496 }
1497 });
1498 }
1499 });
1500 } else {
1501 // make sure to load file first
1502 mjsonrpc_db_paste(["/Sequencer/Command/Load new file"],[true]).then(function (rpc) {
1503 if (rpc.result.status[0] === 1) {
1504 dlgParam(debugFlag);
1505 }
1506 });
1507 }
1508}
1509
1510// Helper function to add the current file to next files queue
1511function setAsNext() {
1512 const [lineNumbers,editor,label] = editorElements();
1513 // Not allowed from console
1514 if (editor.id === "editorTab1") return;
1515 // This is the addAsNext button cell
1516 const e = document.getElementById("addAsNext");
1517 let filename = label.title.split("\n")[0].replace(/^sequencer\//,'');
1518 let message = `Save and put ${filename} in the next file queue?`;
1519 dlgConfirm(message,function(resp) {
1520 if (resp) {
1521 seqSave(filename);
1522 let order = chngNextFilename(e,filename);
1523 //dlgAlert(`File save and placed in position ${order} in the queue.`);
1524 }
1525 });
1526}
1527
1528// helper stop function
1529function stopSeq() {
1530 const message = `Are you sure you want to stop the sequence?`;
1531 dlgConfirm(message,function(resp) {
1532 if (resp) {
1533 modbset('/Sequencer/Command/Stop immediately',true);
1534 }
1535 });
1536}
1537
1538// Show or hide parameters table
1539function showParTable(varContainer) {
1540 const e = document.getElementById(varContainer);
1541 const infoColumn = document.getElementById("infoColumn");
1542 const edtColumn = document.getElementById("edtColumn");
1543 let vis = document.getElementById("showParTable").checked;
1544 let visNF = (document.getElementById("nextFNContainer").style.display === "none");
1545 //let calcWidth = (window.innerWidth - e.parentElement.previousElementSibling.getBoundingClientRect().left - 28);
1546 if (vis) {
1547 //e.parentElement.previousElementSibling.style.width = `calc(${calcWidth}px - 19.5em)`;
1548 //e.parentElement.previousElementSibling.style.maxWidth = `calc(${calcWidth}px - 19.5em)`;
1549 infoColumn.style.display = "block";
1550 infoColumn.style.width = "19.5em";
1551 edtColumn.style.width = "100%";
1552 infoColumn.children[0].style.height = edtColumn.style.height;
1553 } else {
1554 if (visNF) {
1555 infoColumn.style.display = "none";
1556 edtColumn.style.width = "100%";
1557 //e.parentElement.style.width = "0";
1558 //e.parentElement.previousElementSibling.style.width = `calc(${calcWidth}px )`;
1559 //e.parentElement.previousElementSibling.style.maxWidth = `calc(${calcWidth}px )`;
1560 }
1561 }
1562}
1563
1564// Show or hide next file list
1565function showFNTable(nextFNContainer) {
1566 let e = document.getElementById(nextFNContainer);
1567 let vis = document.getElementById("showNextFile").checked;
1568 let visVar = (document.getElementById("varContainer").style.display === "none");
1569 let html = "";
1570 let addFileRow = "";
1571 let nFiles = 0;
1572 mjsonrpc_db_get_values(["/Sequencer/State/Next Filename"]).then(function(rpc) {
1573 if (rpc.result.status[0] !== 1) return;
1574 let fList = rpc.result.data[0];
1575 for (let i = 0; i < fList.length; i++) {
1576 if (fList[i] && fList[i].trim() !== "") {
1577 html += `<tr class="dragrow" draggable="true"><td style="cursor: all-scroll;"><img draggable="false" style="cursor: all-scroll;" src="icons/menu.svg" title="Drag and drop to reorder"></td><td><img draggable="false" src="icons/folder-open.svg" title="Change file" onclick="chngNextFilename(this.parentElement);"></td><td style="cursor: all-scroll;" title="Drag and drop to reorder" ondblclick="chngNextFilename(this);">${fList[i]}</td><td><img draggable="false" title="Remove file from list" onclick="remNextFilename(this.parentElement);" src="icons/trash-2.svg"></td></tr>`;
1578 nFiles++;
1579 }
1580 }
1581 //let calcWidth = (window.innerWidth - e.parentElement.previousElementSibling.getBoundingClientRect().left - 28);
1582 if (vis && html !== "") {
1583 //e.parentElement.previousElementSibling.style.width = `calc(${calcWidth}px - 19.5em)`;
1584 //e.parentElement.previousElementSibling.style.maxWidth = `calc(${calcWidth}px - 19.5em)`;
1585 //e.style.display = "flex";
1586 //e.parentElement.style.width = "19.5em";
1587 if (nFiles < 10) {
1588 addFileRow = `<tr><td id="addAsNext"><img src="icons/file-plus.svg" title="Add file" onclick="chngNextFilename(this.parentElement);"></td><td></td><td></td><td></td></tr>`;
1589 }
1590 e.innerHTML = `<table class="mtable infotable" style="width:100%"><tr><th style="width:10px;"></th><th style="width:10px;"></th><th>Next files</th><th style="width:10px;"></th></tr>${html}${addFileRow}</table>`;
1591 } else {
1592 e.style.display = "none";
1593 if (visVar) {
1594 //e.parentElement.style.width = "0";
1595 //e.parentElement.previousElementSibling.style.width = `calc(${calcWidth}px )`;
1596 //e.parentElement.previousElementSibling.style.maxWidth = `calc(${calcWidth}px )`;
1597 }
1598 }
1599 activateDragDrop(e);
1600 }).catch (function (error) {
1601 console.error(error);
1602 });
1603}
1604
1605// Show error state of sequencer
1606function checkError(element) {
1607 let e = element.parentElement.parentElement;
1608 if (element.value === "") {
1609 e.style.display = "none";
1610 } else {
1611 e.style.display = "table-row";
1612 }
1613}
1614
1615// Show extra rows for wait and loop
1616function extraRows(e) {
1617 /* Arguments:
1618 e - triggering element to identify wait or loop
1619 */
1620 // get current row, table and dialog
1621 let rIndex = e.parentElement.parentElement.rowIndex;
1622 let table = e.parentElement.parentElement.parentElement;
1623 let progressDlg = table.parentElement.parentElement.parentElement.parentElement.parentElement;
1624 // check if there is a wait or loop commands (if non-zero)
1625 if (e.value) {
1626 showProgress();
1627 if (e.id === "waitTrig") {
1628 // Make sure there is only one wait row
1629 const waitTRs = document.querySelectorAll('.waitTR');
1630 const waitFormula = (e.value === "Seconds") ? 'data-formula="x/1000"' : '';
1631 if (waitTRs.length) waitTRs.forEach(element => element.remove());
1632 // Insert a new row
1633 let tr = table.insertRow(rIndex+1);
1634 tr.className = "waitTR";
1635 tr.innerHTML = `<td style="position: relative;width:80%;" colspan="2">&nbsp;
1636 <span class="modbhbar waitloopbar" style="position: absolute; top: 0; left: 0; color: lightgreen;" data-odb-path="/Sequencer/State/Wait value" ${waitFormula} id="mwaitProgress"></span>
1637 <span class="waitlooptxt">
1638 Wait: [<span class="modbvalue" data-odb-path="/Sequencer/State/Wait value" ${waitFormula}></span>/<span class="modbvalue" data-odb-path="/Sequencer/State/Wait limit" ${waitFormula} onchange="if (document.getElementById('mwaitProgress')) document.getElementById('mwaitProgress').dataset.maxValue = this.value;"></span>] <span class="modbvalue" data-odb-path="/Sequencer/State/Wait type"></span>
1639 </span>
1640 </td>`;
1641 windowResize();
1642 } else if (e.id === "loopTrig") {
1643 mjsonrpc_db_get_values(["/Sequencer/State/Loop n"]).then(function(rpc) {
1644 let loopArray = rpc.result.data[0];
1645 for (let i = 0; i < loopArray.length; i++) {
1646 if (loopArray[i] === 0) break;
1647 let tr = table.insertRow(rIndex+1);
1648 tr.className = "loopTR";
1649 tr.innerHTML = `<td style="position: relative;width:80%;" colspan="2">&nbsp;
1650 <span class="modbhbar waitloopbar" style="position: absolute; top: 0; left: 0; color: #CBC3E3;" data-odb-path="/Sequencer/State/Loop counter[${i}]" id="mloopProgress${i}"></span>
1651 <span class="waitlooptxt">
1652 Loop ${i}: [<span class="modbvalue" data-odb-path="/Sequencer/State/Loop counter[${i}]"></span>/<span class="modbvalue" data-odb-path="/Sequencer/State/Loop n[${i}]" onchange="if (document.getElementById('mloopProgress${i}')) document.getElementById('mloopProgress${i}').dataset.maxValue = this.value;"></span>]
1653 </span>
1654 </td>`;
1655 windowResize();
1656 }
1657 }).catch (function (error) {
1658 console.error(error);
1659 });
1660 }
1661 } else {
1662 // remove rows
1663 if (e.id === "waitTrig") document.querySelectorAll('.waitTR').forEach(element => element.remove());
1664 if (e.id === "loopTrig") document.querySelectorAll('.loopTR').forEach(element => element.remove());
1665 // hide progress div
1666 //dlgHide(progressDlg);
1667 windowResize();
1668 }
1669}
1670
1671// Helper function to identify browser, 1 FF, 2 Chrome, 3, other
1672function browserType() {
1673 if (navigator.userAgent.indexOf("Chrome") !== -1) {
1674 return 1;
1675 } else if (navigator.userAgent.indexOf("Firefox") !== -1) {
1676 return 2;
1677 }
1678
1679 return 1;
1680}
1681
1682// make visual hint that file is changes
1683function seqIsChanged(flag) {
1684 // flag - true is change, false is saved
1685 let fileChangedId = "filechanged" + editorElements()[0].id.replace("lineNumbers","");
1686 let filechanged = document.getElementById(fileChangedId);
1687 if (filechanged) {
1688 if (flag) {
1689 filechanged.innerHTML = "&nbsp;&#9998;";
1690 } else if (flag === undefined) {
1691 // true if text has changed false if not
1692 return (filechanged.innerHTML !== "");
1693 } else {
1694 filechanged.innerHTML = "";
1695 }
1696 }
1697}
1698
1699// save history of edits in element editor
1700function saveState(mslText,editor) {
1701 editor = (editor) ? editor : editorElements()[1];
1702 const editorId = editor.id;
1703 if (saveRevision[editorId] === false) {
1704 return;
1705 } else if (saveRevision[editorId] === undefined){
1706 saveRevision[editorId] = true;
1707 }
1708
1709 if (!previousRevisions[editorId]) {
1710 previousRevisions[editorId] = [''];
1711 revisionIndex[editorId] = 0;
1712 }
1713
1714 // Add one more revision, and trim array if we had some undos
1715 revisionIndex[editorId]++;
1716 if (revisionIndex[editorId] < previousRevisions[editorId].length - 1) {
1717 previousRevisions[editorId].splice(revisionIndex[editorId] + 1);
1718 }
1719 // Push new revision and keep only nRevisions revisions
1720 previousRevisions[editorId].push(mslText)
1721 if (previousRevisions[editorId].length > nRevisions) {
1722 previousRevisions[editorId].shift();
1723 }
1724 revisionIndex[editorId] = previousRevisions[editorId].length - 1;
1725}
1726
1727// undo last edit
1728function undoEdit(editor) {
1729 editor = (editor) ? editor : editorElements()[1];
1730 const editorId = editor.id;
1731 if (revisionIndex[editorId] < 1) {
1732 // disable menu item
1733 disableMenuItems("undoMenu",true);
1734 return;
1735 } else {
1736 // enable menu item
1737 disableMenuItems("undoMenu",false);
1738 revisionIndex[editorId]--;
1739 }
1740 // reset redo
1741 disableMenuItems("redoMenu",false);
1742
1743 let caretPos = getCurrentCursorPosition(editor);
1744 let currText = editor.innerText;
1745 saveRevision[editorId] = false;
1746 editor.innerHTML = syntax_msl(previousRevisions[editorId][revisionIndex[editorId]]).join(lc).slice(0,-1);
1747 updateLineNumbers(editor.previousElementSibling,editor);
1748 saveRevision[editorId] = true;
1749 // calculate change in caret position based on length
1750 caretPos = caretPos + previousRevisions[editorId][revisionIndex[editorId]].length - currText.length;
1751 setCurrentCursorPosition(editor, caretPos);
1752}
1753
1754// redo the undo
1755function redoEdit(editor) {
1756 editor = (editor) ? editor : editorElements()[1];
1757 const editorId = editor.id;
1758 if (revisionIndex[editorId] >= previousRevisions[editorId].length - 1) {
1759 // disable menu item
1760 disableMenuItems("redoMenu",true);
1761 return;
1762 } else {
1763 // enable menu item
1764 disableMenuItems("redoMenu",false);
1765 revisionIndex[editorId]++;
1766 }
1767 // reset undo
1768 disableMenuItems("undoMenu",false);
1769
1770 let caretPos = getCurrentCursorPosition(editor);
1771 let currText = editor.innerText;
1772 saveRevision[editorId] = false;
1773 editor.innerHTML = syntax_msl(previousRevisions[editorId][revisionIndex[editorId]]).join(lc).slice(0,-1);
1774 updateLineNumbers(editor.previousElementSibling,editor);
1775 saveRevision[editorId] = true;
1776 // calculate change in caret position based on length
1777 caretPos = caretPos + previousRevisions[editorId][revisionIndex[editorId]].length - currText.length;
1778 setCurrentCursorPosition(editor, caretPos);
1779}
1780
1781// Select slines from startLine to endLine
1782function selectLines([startLine, endLine],e) {
1783 const selection = window.getSelection();
1784 // Remove existing selections
1785 selection.removeAllRanges();
1786 let startElementId = '#sline' + startLine;
1787 let endElementId = '#sline' + endLine;
1788 let startElement = null, endElement = null;
1789 if (e.querySelector(startElementId)) startElement = e.querySelector(startElementId).firstChild;
1790 if (e.querySelector(endElementId)) endElement = e.querySelector(endElementId).lastChild;
1791 // we need startElement and endElement with first/lastChild
1792 // the following prevents loosing selection but not ideal
1793 while (startElement === null && startLine <= endLine) {
1794 startLine++;
1795 startElementId = '#sline' + startLine;
1796 startElement = e.querySelector(startElementId).firstChild;
1797 }
1798 while (endElement === null && endLine > 0) {
1799 endLine--;
1800 endElementId = '#sline' + endLine;
1801 endElement = e.querySelector(endElementId).lastChild;
1802 }
1803 if (startElement && endElement) {
1804 const range = document.createRange();
1805 // Set the start of the range to the startElement at offset 0
1806 range.setStart(startElement, 0);
1807 // Set the end of the range to the endElement at its length
1808 range.setEnd(endElement, endElement.childNodes.length);
1809 // Add the range to the selection
1810 selection.addRange(range);
1811 }
1812}
1813
1814// switch between dark and light modes on request
1815function lightToDark(lToDcheck) {
1816 const edt_areas = document.querySelectorAll('.edt_area');
1817 if (lToDcheck.checked) {
1818 localStorage.setItem("darkMode", true);
1819 edt_areas.forEach(area => {
1820 area.style.backgroundColor = "black";
1821 area.style.color = "white";
1822 });
1823 updateCSSRule(".etab button:hover","background-color","black");
1824 updateCSSRule(".etab button:hover","color","white");
1825 updateCSSRule(".etab button:hover","border-bottom","5px solid black");
1826 updateCSSRule(".etab button.edt_active","background-color","black");
1827 updateCSSRule(".etab button.edt_active","color","white");
1828 updateCSSRule(".etab button.edt_active","border-bottom","5px solid black");
1829 updateCSSRule(".etab button.edt_active:hover","background-color","black");
1830 updateCSSRule(".etab button.edt_active:hover","border-bottom","5px solid black");
1831 } else {
1832 localStorage.removeItem("darkMode");
1833 edt_areas.forEach(area => {
1834 area.style.backgroundColor = "white";
1835 area.style.color = "black";
1836 });
1837 updateCSSRule(".etab button:hover","background-color","white");
1838 updateCSSRule(".etab button:hover","color","black");
1839 updateCSSRule(".etab button:hover","border-bottom","5px solid white");
1840 updateCSSRule(".etab button.edt_active","background-color","white");
1841 updateCSSRule(".etab button.edt_active","color","black");
1842 updateCSSRule(".etab button.edt_active","border-bottom","5px solid white");
1843 updateCSSRule(".etab button.edt_active:hover","background-color","white");
1844 updateCSSRule(".etab button.edt_active:hover","border-bottom","5px solid white");
1845 }
1846}
1847
1848function updateCSSRule(selector, property, value) {
1849 for (let i = 0; i < document.styleSheets.length; i++) {
1850 let styleSheet = document.styleSheets[i];
1851 let rules = styleSheet.cssRules || styleSheet.rules;
1852 if (!rules) continue;
1853 for (let j = 0; j < rules.length; j++) {
1854 let rule = rules[j];
1855 if (rule.selectorText === selector) {
1856 rule.style[property] = value;
1857 return;
1858 }
1859 }
1860 }
1861}
1862
1863// show/hide wait and loop progress
1864function showProgress(e) {
1865 //const progressDiv = document.getElementById("progressDiv");
1866 const progressDiv = document.getElementById("Progress");
1867 if (e === undefined) e = document.getElementById("showProgressBars");
1868 if (e.checked) {
1869 localStorage.setItem("showProgress",true);
1870 progressDiv.style.display = "block";
1871 //dlgShow(progressDiv);
1872 } else {
1873 localStorage.removeItem("showProgress");
1874 progressDiv.style.display = "none";
1875 //dlgHide(progressDiv);
1876 }
1877}
1878
1879// Mark the current line number
1880function markCurrLineNum() {
1881 const [lineNumbers,editor] = editorElements();
1882 const currLines = lineNumbers.querySelectorAll(".edt_linenum_curr");
1883 const [startLine,endLine] = getLinesInSelection(editor);
1884 if (startLine === 0 && endLine === 0) return;
1885 currLines.forEach((line) => line.classList.remove("edt_linenum_curr"));
1886 for (let i = startLine; i <= endLine; i++) {
1887 let lineNumId = "#lNum" + i.toString();
1888 let lineNum = lineNumbers.querySelector(lineNumId);
1889 if (lineNum)
1890 lineNum.className = "edt_linenum_curr";
1891 }
1892}
1893
1894// Check if sequencer program is in ODB and running.
1895// If not, try to get it going
1896function checkSequencer() {
1897 mjsonrpc_call('cm_exist', {"name":"Sequencer","unique":true}).then(function (rpc1) {
1898 mjsonrpc_db_get_values(["/Programs/Sequencer/Start command"]).then(function(rpc2) {
1899 let isRunning = (rpc1.result.status === 1);
1900 let isDefined = ((rpc2.result.data[0] !== null) && (rpc2.result.data[0] !== ""));
1901 if (isRunning && isDefined) return;
1902 // sequencer not running or not defined, stop it just in case and check the reason
1903 mjsonrpc_stop_program("Sequencer");
1904 let message = "";
1905 if (isDefined) {
1906 message = "Sequencer program is not running.<br>Should I start it?"
1907 } else {
1908 message = "Sequencer program is not configured and not running.<br>Should I try to start it anyway?"
1909 }
1910 dlgConfirm(message,function(resp) {
1911 if (resp) {
1912 if (!isDefined) {
1913 // assume that sequencer in path and create a start command, sleep 2s,
1914 // set value to "msequencer -D", sleep 2s, start program
1915 mjsonrpc_db_create([{"path" : "/Programs/Sequencer/Start command", "type" : TID_STRING}]).then(function (rpc3) {
1916 setTimeout(function(){
1917 mjsonrpc_db_paste(["/Programs/Sequencer/Start command"],["msequencer -D"]).then(function (rpc4) {
1918 if (rpc4.result.status[0] === 1) {
1919 mjsonrpc_start_program("Sequencer");
1920 }
1921 }).catch(function (error) {
1922 console.error(error);
1923 });
1924 }, 2000);
1925 }).catch(function (error) {
1926 console.error(error);
1927 });
1928 } else {
1929 mjsonrpc_start_program("Sequencer");
1930 }
1931 // take 3 seconds and check that it actually started
1932 setTimeout(function(){
1933 mjsonrpc_call('cm_exist', {"name":"Sequencer","unique":true}).then(function (rpc5) {
1934 if (rpc5.result.status === 1) {
1935 dlgAlert("Sequencer started successfully.");
1936 } else {
1937 dlgAlert("Failed to start Sequencer!<br>Try to start it manually (msequencer -D)");
1938 }
1939 });
1940 }, 3000);
1941 }
1942 });
1943 }).catch (function (error) {
1944 console.error(error);
1945 });
1946 }).catch(function (error) {
1947 console.error(error);
1948 });
1949}
1950
1951function captureSelected() {
1952 if (window.getSelection) {
1953 let selection = window.getSelection();
1954 let text = selection.toString();
1955 let range = selection.getRangeAt(0);
1956 if (text && range) {
1957 const rangeData = {
1958 startPath: range.startContainer,
1959 startOffset: range.startOffset,
1960 endPath: range.endContainer,
1961 endOffset: range.endOffset
1962 };
1963 sessionStorage.setItem("tempSelText", text);
1964 sessionStorage.setItem("tempSelRange", JSON.stringify(rangeData));
1965 }
1966 }
1967}
1968
1969function editMenu(action) {
1970 let text = sessionStorage.getItem("tempSelText") ?? "";
1971 let storedRange = sessionStorage.getItem("tempSelRange") ?? "";
1972 /*
1973 if (storedRange) {
1974 let rangeData = JSON.parse(storedRange);
1975 let startContainer = nodeFromPath(rangeData.startPath);
1976 let endContainer = nodeFromPath(rangeData.endPath);
1977
1978 // Create a new range
1979 let newRange = new Range();
1980 newRange.setStart(startContainer, rangeData.startOffset);
1981 newRange.setEnd(endContainer, rangeData.endOffset);
1982
1983 // Select the new range
1984 let selection = window.getSelection();
1985 selection.removeAllRanges();
1986 selection.addRange(newRange);
1987 }
1988 */
1989 if (action === "Copy") {
1990 if (text) {
1991 sessionStorage.setItem("copiedText",text);
1992 }
1993 } else if (action === "Paste") {
1994 const copiedText = sessionStorage.getItem("copiedText");
1995 if (copiedText) {
1996 newRange.deleteContents();
1997 newRange.insertNode(document.createTextNode(copiedText));
1998 }
1999 } else if (action === "Cut") {
2000 if (text) {
2001 sessionStorage.setItem("copyText",text);
2002 //document.execCommand("cut");
2003 newRange.deleteContents();
2004 }
2005 } else if (action === "Undo") {
2006 undoEdit();
2007 } else if (action === "Redo") {
2008 redoEdit();
2009 }
2010}
2011
2012// Switch to the clicked tab
2013function openETab(btn) {
2014 const tabcontent = document.querySelectorAll(".etabcontent");
2015 const tablinks = document.querySelectorAll(".etablinks");
2016 const tab = btn ? btn : document.querySelectorAll(".edt_active")[0];
2017 const tabID = tab.id.replace("-btn","")
2018 tabcontent.forEach(content => {
2019 content.style.display = "none";
2020 });
2021 tablinks.forEach(link => {
2022 link.classList.remove("edt_active");
2023 });
2024 tab.className += " edt_active";
2025 document.getElementById(tabID).style.display = "flex";
2026 // For the main sequence tab disable Save and Save as...
2027 if (tabID === "etab1") {
2028 disableMenuItems("noteditor",true);
2029 } else {
2030 disableMenuItems("noteditor",false);
2031 }
2032 // Adjust height of active editor
2033 windowResize();
2034}
2035
2036// Close the clicked tab
2037function closeTab(tab,event) {
2038 const tablinks = document.querySelectorAll(".etablinks");
2039 if (tablinks.length < 3) return;
2040 const tabCount = parseInt(tab.parentElement.id.replace("-btn","").replace("etab",""));
2041 const tabBtn = document.getElementById(`etab${tabCount}-btn`);
2042 const tabContent = document.getElementById(`etab${tabCount}`);
2043 if (seqIsChanged()) {
2044 dlgConfirm("File was changed, close anyway?", function(resp) {
2045 if (resp) {
2046 tabBtn.remove();
2047 tabContent.remove();
2048 // switch to previous tab
2049 openETab(document.getElementById(`etab${(tabCount-1)}-btn`));
2050 }
2051 });
2052 } else {
2053 tabBtn.remove();
2054 tabContent.remove();
2055 // switch to previous tab
2056 openETab(document.getElementById(`etab${(tabCount-1)}-btn`));
2057 }
2058 // need this since the close button is inside the tab button
2059 event.stopPropagation();
2060}
2061
2062// Create and add a new editor tab
2063function addETab(btn) {
2064 // Create tab button
2065 const tabBtn = document.createElement("button");
2066 tabBtn.className = "etablinks";
2067 const tabCount = (btn.previousElementSibling) ? parseInt(btn.previousElementSibling.id.replace("-btn","").replace("etab","")) + 1 : 1;
2068 tabBtn.id = "etab" + tabCount + "-btn";
2069 tabBtn.innerHTML = `<span id="etab${tabCount}-lbl">new file${tabCount}</span><span style="color: var(--mred);" id="filechanged${tabCount}"></span><span onclick="closeTab(this,event);" class="closebtn">&times;</span>`;
2070 btn.parentNode.insertBefore(tabBtn,btn);
2071 tabBtn.onclick = function () { openETab(this);};
2072
2073 // Create editor area
2074 const tabContent = document.createElement("div");
2075 tabContent.id = "etab" + tabCount;
2076 tabContent.className = "etabcontent";
2077 let makeDark = "";
2078 if (localStorage.getItem("darkMode")) makeDark = "style='background-color: black; color: white;'";
2079 const html =
2080 `<pre id="lineNumbers${tabCount}" class="edt_linenum"></pre><pre id="editorTab${tabCount}" ${makeDark} class="edt_area" spellcheck="false" contenteditable="false"></pre>`;
2081 tabContent.innerHTML = html;
2082 const lastETab = document.getElementById("lastETab");
2083 lastETab.parentNode.insertBefore(tabContent,lastETab);
2084 tabBtn.click();
2085 windowResize();
2086 // Add event listeners
2087 editorEventListeners();
2088}
2089
2090// Return the pre of lineNumbers, editor, tab label and tab button element of the active tab
2091function editorElements() {
2092 const btn = (document.querySelectorAll(".edt_active")[0]) ? document.querySelectorAll(".edt_active")[0] : document.getElementById("etab1-btn");
2093 const tab = document.getElementById(btn.id.replace("-btn",""));
2094 const [lineNumbers,editor] = tab.children;
2095 const btnLabel = (btn.id !== "etab1-btn") ? btn.children[0] : btn.children[1];
2096 return [lineNumbers,editor,btnLabel,btn];
2097}
2098
2099// disable and enable clicking on menu item
2100function disableMenuItems(className,flag) {
2101 /* Arguments:
2102 className - the class name of the item
2103 flag - true/false to enable/disable item
2104 */
2105 const els = document.querySelectorAll(`.${className}`);
2106 els.forEach(e => {
2107 if (flag) {
2108 e.style.opacity = 0.5;
2109 e.style.pointerEvents = "none";
2110 } else {
2111 e.style.opacity = "";
2112 e.style.pointerEvents = "";
2113 }
2114 });
2115}
2116
2117// Function to replace some special characters in the text
2118function escapeSpecialCharacters(text) {
2119 return text.replace(/&/g, "&amp;")
2120 .replace(/</g, "&lt;")
2121 .replace(/>/g, "&gt;")
2122 .replace(/\r\n|\n\r/g, '\n')
2123 //.replace(/"/g, "&quot;")
2124 //.replace(/'/g, "&#39;")
2125 .replace(/\t/g, " ");
2126}
2127
2128// Produce a help window
2129function dlgHelp() {
2130 let help = `
2131<span style='text-align: left;'>
2132<b>Hidden features of the sequencer editor</b>
2133<ul style='white-space: pre;font-family: monospace;'>
2134<li>Double click on the edit area of the first (main) tab to edit the currently loaded sequence.</li>
2135<li>Tab - Indent selected lines.</li>
2136<li>Shift+Tab - Unindent selected lines.</li>
2137<li>Escape - Autoindent selected lines according to syntax rules.</li>
2138<li>Ctrl+C - Copy selected text.</li>
2139<li>Ctrl+V - Paste selected text.</li>
2140<li>Ctrl+A - Select all text.</li>
2141<li>Ctrl+Z - Undo last change.</li>
2142<li>Ctrl+R - Redo last undo.</li>
2143</ul>
2144</span>
2145`;
2146 const d = dlgMessage("Editor help",help, false, false);
2147 const btn = d.querySelectorAll('.dlgButton')[0];
2148 btn.className = "dlgButtonDefault";
2149 btn.focus();
2150
2151}
2152
2153// Activate drag and drop events on next files table
2154function activateDragDrop(table) {
2155 /* Arguments:
2156 table - The table element containing the list of next files.
2157 */
2158 // collect all rows with class dragrow
2159 const rows = table.querySelectorAll('.dragrow');
2160 let dragStartIndex,dragEndIndex;
2161 let emptyRow;
2162 // add event listeners
2163 rows.forEach(row => {
2164 row.addEventListener('dragstart', dragStart);
2165 row.addEventListener('dragover', dragOver);
2166 row.addEventListener('dragend', dragEnd);
2167 });
2168
2169 function dragStart(e) {
2170 dragStartIndex = Array.from(rows).indexOf(this);
2171 rows.forEach(row => row.classList.remove('dragstart'));
2172 this.classList.add('dragstart');
2173 }
2174 function dragOver(e) {
2175 e.preventDefault();
2176 dragEndIndex = Array.from(rows).indexOf(this);
2177 // Create or update the empty row element
2178 if (!emptyRow) {
2179 emptyRow = document.createElement('tr');
2180 emptyRow.innerHTML = "<td colspan=4>&nbsp;</td>";
2181 emptyRow.classList.add('empty-dragrow');
2182 }
2183 // Insert the empty row element at the appropriate position
2184 if (dragEndIndex > dragStartIndex) {
2185 this.parentNode.insertBefore(emptyRow, this.nextSibling);
2186 } else if (dragEndIndex < dragStartIndex) {
2187 this.parentNode.insertBefore(emptyRow, this);
2188 }
2189 }
2190 function dragEnd(e) {
2191 if (emptyRow) {
2192 emptyRow.remove();
2193 emptyRow = null;
2194 }
2195 reorderNextFilenames(dragStartIndex,dragEndIndex);
2196 rows.forEach(row => {
2197 row.classList.remove('dragstart');
2198 });
2199 }
2200}
2201
2202// Move next filename from dragStarIndex to dragEndIndex position
2203function reorderNextFilenames(dragStarIndex,dragEndIndex) {
2204 /* Arguments:
2205 dragStarIndex - move file from this indext.
2206 dragEndIndex - destination index of the file.
2207 */
2208 if (dragStarIndex === dragEndIndex) return;
2209 let istep = 1;
2210 let istart = dragStarIndex;
2211 let iend = dragEndIndex;
2212 let odbpath = "/Sequencer/State/Next Filename";
2213 if (istart > iend) {
2214 step = -1;
2215 istart = dragEndIndex;
2216 iend = dragStarIndex;
2217 }
2218 mjsonrpc_db_get_values([odbpath]).then(function(rpc) {
2219 if (rpc.result.status[0] !== 1) return;
2220 let fList = rpc.result.data[0];
2221 let draggedFile = fList.splice(dragStarIndex,1)[0];
2222 fList.splice(dragEndIndex,0,draggedFile);
2223 let paths = [];
2224 let values = [];
2225 let j = 0;
2226 for (let i = istart; i <= iend; i=i+istep) {
2227 paths[j] = odbpath + "[" + i.toString() + "]";
2228 values[j] = fList[i];
2229 j++;
2230 }
2231 mjsonrpc_db_paste(paths,values).then(function (rpc2) {
2232 if (rpc2.result.status[0] !== 1) {
2233 dlgAlert("Failed to move the file!<br>Please check.");
2234 } else if (rpc2.result.status[0] === 1) {
2235 showFNTable('nextFNContainer');
2236 }
2237 }).catch(function (error) {
2238 console.error(error);
2239 });
2240 }).catch (function (error) {
2241 console.error(error);
2242 });
2243}
2244
2245// Change the next file name in the clicked row
2246function chngNextFilename(e,filename) {
2247 /* Arguments:
2248 e - (optional) cell element on the same row of "next file" to be changed.
2249 If last row (or undefined) add a file to the end of the queue.
2250 filename - (optional) The file name to add to the end of the queue.
2251 */
2252 if (e === undefined) e = document.getElementById("addAsNext");
2253 // file index from table row index
2254 let index = e ? e.parentElement.rowIndex - 1 : 0;
2255 // Only 10 files are allowed
2256 if (index > 9) {
2257 dlgAlert("Maximum number (10) of next files reached!");
2258 return;
2259 }
2260 let odbpath = "/Sequencer/State/Next Filename";
2261 mjsonrpc_db_get_values(["/Sequencer/State/Path"]).then(function(rpc) {
2262 let path = fnPath + "/" + rpc.result.data[0].replace(/\/+/g, '/').replace(/^\/+|\/+$/g, '');
2263 sessionStorage.setItem("pathName",path);
2264 if (filename) {
2265 odbpath = odbpath + "[" + index.toString() + "]";
2266 modbset(odbpath,filename);
2267 } else {
2268 file_picker(path,fnExt,function(filename) {
2269 filename = filename.replace(/^sequencer\//,'').replace(/^\//,'');
2270 odbpath = odbpath + "[" + index.toString() + "]";
2271 if (filename) {
2272 modbset(odbpath,filename);
2273 }
2274 });
2275 }
2276 return index+1;
2277 }).catch(function (error) {
2278 mjsonrpc_error_alert(error);
2279 });
2280}
2281
2282// Reomve the next file name from the queue
2283function remNextFilename(e) {
2284 /* Arguments:
2285 e - cell element on the same row of "next file" to be removed
2286 */
2287 // file index from table row index
2288 let index = e.parentElement.rowIndex - 1;
2289 let odbpath = "/Sequencer/State/Next Filename";
2290 mjsonrpc_db_get_values([odbpath]).then(function(rpc) {
2291 if (rpc.result.status[0] !== 1) return;
2292 let fList = rpc.result.data[0];
2293 // empty last element
2294 fList.push("");
2295 let paths = [];
2296 let values = [];
2297 let j = 0;
2298 for (let i = index+1; i < fList.length; i++) {
2299 paths[j] = odbpath + "[" + (i-1).toString() + "]";
2300 values[j] = fList[i];
2301 j++;
2302 }
2303 mjsonrpc_db_paste(paths,values).then(function (rpc2) {
2304 if (rpc2.result.status[0] !== 1) {
2305 dlgAlert("Failed to remove file from list!<br>Please check.");
2306 } else if (rpc2.result.status[0] === 1) {
2307 showFNTable('nextFNContainer');
2308 }
2309 }).catch(function (error) {
2310 console.error(error);
2311 });
2312 }).catch (function (error) {
2313 console.error(error);
2314 });
2315}
2316