3 Sequencer syntax highlighting and operation functions
5 Created by Zaher Salman on September 16th, 2023
10 Some of these functions, although specific to the midas sequencer,
11 can be used for general syntax highlighting and editing.
13 All files created and read from subdirectories within
14 experiment_directory/userfiles/sequencer. The path defined in ODB
15 /Sequencer/State/Path is added to this folder.
17 Generate highlighted script text:
18 syntax_msl(seqLines,keywordGroups)
19 - seqLines - is the text of the script (or array of lines).
20 - keywordGroups - (optional) object defining the command groups for
21 highlighting. If not provided, a msl default will
24 Highlight the current line:
26 - lineNum - the current line number.
27 - color - (optional) the bg color to use
30// Using Solarized color scheme - https://ethanschoonover.com/solarized/
33const defKeywordGroups = {
35 keywords: [/(["'])(.*?)\1/g],
38 mslclass: "msl_string",
41 keywords: [/(\$[\w]+|^\s*[\w]+(?=\s*=))/gm],
44 mslclass: "msl_variable",
47 keywords: ["GOTO", "CALL", "SCRIPT", "SUBROUTINE", "ENDSUBROUTINE", "TRANSITION", "INCLUDE", "EXIT"],
48 color: "#268bd2", // blue
50 mslclass: "msl_control_flow",
53 keywords: ["ODBSET", "ODBGET", "ODBCREATE", "ODBDELETE", "ODBINC", "ODBLOAD", "ODBSAVE", "ODBSUBDIR", "PARAM", "SET", "CAT"],
54 color: "#2aa198", // cyan
56 mslclass: "msl_data_management",
59 keywords: ["RUNDESCRIPTION", "LIBRARY", "MESSAGE", "MSG"],
60 color: "#6c71c4", // violet
65 keywords: ["IF", "ELSE", "ENDIF", "WAIT"],
66 color: "#c577f6", // pink (not solarized)
71 keywords: ["BREAK", "LOOP", "ENDLOOP"],
72 color: "#d33682", // magenta
74 mslclass: "msl_loops",
77 keywords: ["UNIT8", "INT8", "UNIT16", "INT16", "UNIT32", "INT32", "BOOL", "FLOAT", "DOUBLE", "STRING"],
78 color: "#859900", // green
80 mslclass: "msl_data_types",
83 keywords: ["SECONDS", "EVENTS", "ODBVALUE"],
84 color: "#cb4b16", // orange
86 mslclass: "msl_units",
89 keywords: ["start", "stop", "pause", "resume"],
90 color: "#dc322f", // red
92 mslclass: "msl_actions",
95 keywords: ["true","false"],
101 keywords: [/\b(?<![0-9a-fA-F#])\d+(\.\d+)?([eE][-+]?\d+)?\b/g],
102 color: "#b58900", // yellow
104 mslclass: "msl_number",
107 keywords: ["#","COMMENT"],
108 color: "#839496", // base0
109 fontWeight: "italic",
110 mslclass: "msl_comment",
114// Iindentation keywords
116 indentplus: ["IF", "LOOP", "ELSE", "SUBROUTINE"],
117 indentminos: ["ENDIF", "ENDLOOP", "ELSE", "ENDSUBROUTINE"],
128 background-color: #f0f0f0;
135 box-sizing: border-box;
137 display: inline-block;
139 font-family: monospace;
141 -moz-user-select: text
154 width: calc(100% - 3.5em);
157 background-color:white;
160/* background-color:black;
163 box-sizing: border-box;
165 display: inline-block;
167 font-family: monospace;
169 -moz-user-select: text;
172/*span[id^="sline"] {*/
175 display: inline-block;
182 background-color: #FFFF00;
186 background-color: #FF0000;
190 background-color: #FF8800;
194 background-color: #11FF11;
198 background-color: #FFFF00;
202/* Dropdown button styles*/
204 background-color: Transparent;
206 font-family: verdana,tahoma,sans-serif;
215 background-color: #C0D0D0;
218/* Style the dropdown content (hidden by default) */
222 background-color: #f9f9f9;
224 box-shadow: 0 8px 16px rgba(0,0,0,0.2);
228/* Style the dropdown links */
232 text-decoration: none;
237.dropdown-content div{
240 text-decoration: none;
245/* Change color on hover */
246.dropdown-content a:hover {
247 background-color: #ddd;
249.dropdown-content div:hover {
250 background-color: #ddd;
253/* Show the dropdown menu when the button is hovered over */
254.dropdown:hover .dropdown-content {
259// Implement colors and styles from KeywordsGroups in CSS
260for (const group in defKeywordGroups) {
261 const { mslclass, color, fontWeight } = defKeywordGroups[group];
264 seq_css += `.${mslclass} { color: ${color}; font-weight: ${fontWeight}; }\n`;
268const seqStyle = document.createElement('style');
269seqStyle.textContent = seq_css;
270document.head.appendChild(seqStyle);
272// line connector string
274// revisions array, maximum nRevisions
275const previousRevisions = [];
276const nRevisions = 20;
277var revisionIndex = -1;
278var saveRevision = true;
279// Meta combo keydown flag
280var MetaCombo = false;
282// -- Sequencer specific functions --
283// Setup the correct sequencer state visually
284function seqState(funcCall) {
286 funcCall - (optional) a function to be called when the state is set (with the state text)
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";
296 stateText = "Stopped";
302 }).catch (function (error) {
303 console.error(error);
308// Enable editing of sequence
309function editCurrentSeq(divID) {
311 divID - ID of <pre> of the editor area
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);
321 editor.addEventListener("input", function() {
324 document.addEventListener("selectionchange", function(event) {
325 if (event.target.activeElement === editor) markCurrLineNum(editor);
328 // Short cuts have to be attached to window
329 window.addEventListener("keydown",shortCutEvent);
332// apply changes of filename in the ODB (triggers reload)
333function seqChange(filename) {
335 filename - full file name with path to change
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");
344 }).catch(function (error) {
345 console.error(error);
349// Load the sequence text from the file name in the ODB
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);
361// Save sequence text in filename.
362function seqSave(filename) {
364 filename (opt) - undeined save to file in ODB
365 - empty trigger file_picker
366 - save to provided filename with path
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');
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);
384 filename = (path === "") ? "sequencer/" + filenameText : "sequencer/" + path + "/" + filenameText;
387 file_save_ascii_overwrite(filename,text,seqChange);
388 updateBtns('Stopped');
390 }).catch(function (error) {
391 mjsonrpc_error_alert(error);
395// Show/hide buttons according to sequencer state
396function updateBtns(state) {
398 state - the state of the sequencer
402 color: "var(--mgreen)",
405 color: "var(--mred)",
408 color: "var(--mred)",
411 color: "var(--myellow)",
414 const color = seqState[state].color;
415 const seqStateSpan = document.getElementById("seqState");
416 seqStateSpan.innerHTML = state;
417 seqStateSpan.style.backgroundColor = color;
419 const hideBtns = document.querySelectorAll('.seqbtn');
420 hideBtns.forEach(button => {
421 button.style.display = "none";
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";
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];
439 dlgMessage("Message", message, true, false,clrMessage);
443 }).catch (function (error) {
444 console.error(error);
448// Clear sequencer messages
449function clrMessage() {
450 mjsonrpc_db_paste(["Sequencer/State/Message"], [""]).then(function (rpc) {
452 }).catch(function (error) {
453 console.error(error);
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";
469// Load the current sequence from ODB
470function load_msl(divID) {
472 divID - (optional) div id of editor
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];
483 seqLines = syntax_msl(seqLines);
484 let seqHTML = seqLines.join(lc);
485 editor.innerHTML = seqHTML;
486 if (seqState) hlLine(currLine);
488 // Make not editable until edit button is pressed
489 editor.contentEditable = false;
491 window.removeEventListener("keydown",shortCutEvent);
492 }).catch (function (error) {
493 console.error(error);
497// Highlight (background color) and scroll to current line
498function hlLine(lineNum,color) {
500 lineNum - the line number to be highlighted
501 color - (optional) background color
503 const lineId = "sline" + lineNum;
504 const lineHTML = document.getElementById(lineId);
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"));
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" });
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");
527 currLine.scrollIntoView({ block: "center" });
530 localStorage.removeItem("scrollToCurr",true);
534// shortcut event handling to overtake default behaviour
535function shortCutEvent(event) {
536 if (event.altKey && event.key === 's') {
537 event.preventDefault();
539 event.preventDefault();
540 } else if ((event.ctrlKey || event.metaKey) && event.key === 's') {
541 event.preventDefault();
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();
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'
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;
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()));
583 } else if (indentPlus) {
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;
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;
603 // still needs to handle else which gives indentPlus=indentMinos=true
606 e.innerHTML = syntax_msl(e.innerText).join(lc);
607 setCurrentCursorPosition(e, caretPos);
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)) {
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') {
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();
646// Trigger syntax highlighting when you paste text
647function checkSyntaxEventPaste(event) {
648 // set time out to allow default to go first
650 let e = event.target;
651 let caretPos = getCurrentCursorPosition(e);
652 e.innerHTML = syntax_msl(e.innerText).join(lc);
653 setCurrentCursorPosition(e, caretPos);
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);
665 let lineNum = e.innerText.substring(0,pos).split("\n").length + offset;
666 let sline = document.getElementById("sline" + lineNum);
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.
675function getLinesInSelection(e) {
676 const selection = window.getSelection();
678 if (selection.rangeCount === 0) return [0,0];
679 // is it a single line?
680 const singleLine = selection.isCollapsed;
682 const line = whichLine(e);
684 const startLine = parseInt(line.id.replace("sline",""));
685 return [startLine,startLine];
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];
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;
702 let startID = (startNode && startNode.id) ? startNode.id : "";
703 let endID = (endNode && endNode.id) ? endNode.id : "";
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 : "";
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 : "";
714 let startLine = (startNode && startNode.id) ? parseInt(startNode.id.replace("sline","")) : 0;
715 let endLine = (endNode && endNode.id) ? parseInt(endNode.id.replace("sline","")) : 0;
717 startLine = endLine = Math.min(startLine, endLine);
719 return [startLine,endLine];
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;
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();
746 pos = pos - node.length;
749 pos = setCurrentCursorPosition(node, pos);
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);
773// Utility function to escape special characters in a string for use in a regular expression
774function escapeRegExp(s) {
776 return s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
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) {
784 keywordGroups = defKeywordGroups;
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, " ");
794 let nLines = (mslText.match(/\n/g) || []).length;
795 // save current revision for undo
797 // These can be done on the text in one go
799 let reg = /(["'])(.*?)\1/g;
800 mslText = mslText.replace(reg,'<span class="msl_string">$1$2$1</span>');
803 //reg = /^(COMMENT|#.*?)(.*)$/gim;
804 //mslText = mslText.replace(reg,'<span class="msl_comment">$&</span>');
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>');
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>");
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>");
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>");
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>");
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>");
834 reg = new RegExp("^(\\s*)\\b(" + keywordGroups.info.keywords.join("|") + ")\\b", "gim");
835 mslText = mslText.replace(reg, "$1<span class='msl_info'>$2</span>");
838 reg = new RegExp("^(\\s*)\\b(" + keywordGroups.cond.keywords.join("|") + ")\\b", "gim");
839 mslText = mslText.replace(reg,"$1<span class='msl_cond'>$2</span>");
842 reg = new RegExp("\\b(" + keywordGroups.units.keywords.join("|") + ")\\b", "gi");
843 mslText = mslText.replace(reg, "<span class='msl_units'>$1</span>");
846 reg = new RegExp("\\b(" + keywordGroups.actions.keywords.join("|") + ")\\b(\\s*)$", "gim");
847 mslText = mslText.replace(reg, "<span class='msl_actions'>$1</span>$2");
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>');
855 // Break lines and handle one by one
856 seqLines = mslText.split("\n");
858 // This is important for Firefox
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>`;
874 // empty class is needed for cursor movement in Firefox
876 //if (j === seqLines_org.length - 1) {
877 seqLines[j] = "<span class='" + emptyClass + "' id='sline" + (j+1).toString() + "'></span>";
879 seqLines[j] = "<span class='sline' id='sline" + (j+1).toString() + "'>" + seqLines[j] + "</span>";
882 //seqLines = seqLines.slice(0, nLines + 1);
883 updateLineNumbers(seqLines.length);
887// Adjust indentation of a selection of lines
888function indent_msl(lines,addTab) {
890 lines - an array of two elements, first and last line numbers
891 addTab - (opt) +/-1 to add/subtract three spaces to selected lines
894 let singleLine = false;
895 // Avoid issues of begenning of single line
896 if (lines[0] > lines[1] || lines[0] == lines[1]) {
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);
905 if (lineEl) line = lineEl.innerText;
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()));
918 } else if (indentPlus) {
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();
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();
932 const indentPlus = defIndent.indentplus.some(keyword => line.trim().toLowerCase().startsWith(keyword.toLowerCase()));
933 if (indentPlus) indentLevel++;
938// Prepare the parameters/variables (if present) from the ODB as a table
939function seqParVarTable(odbTreeV,odbTreeC,odbTreeD,odbTreeO,odbTreeVar,varFlag) {
941 odbTree... - Objects of ODB values
942 varFlag - (opt) true/false return variable/param table
945 let html = `<span class="modb" data-odb-path="/Sequencer/Variables" onchange="seqParVar('parContainer');"></span>`;
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>`;
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 </th><th style='width: 200px'>Current value </th><th>Comment</th></tr>" : "<tr><th>Parameter </th><th>Initial value </th><th>Comment</th></tr>";
957 const processParam = (name, value, isBool, defValue, optValue, comment) => {
959 let parLine = `<tr><td>${name}</td>`;
962 // set default value in ODB
963 addString = `value="${defValue}"`;
964 modbset(`/Sequencer/Param/Value/${name}`, defValue);
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>`;
971 let initState = defValue ? "checked" : "";
972 inParLine += `<input type="checkbox" ${initState} class="modbcheckbox" data-odb-path="/Sequencer/Param/Value/${name}"></input>`;
974 inParLine += `<input ${addString} onchange="modbset('/Sequencer/Param/Value/${name}', this.value);"></input>`;
977 parLine += `<td>${inParLine}<span class="modb" data-odb-path="/Sequencer/Param/Value/${name}"></span></td>`;
978 parLine += `<td>${comment}</td></tr>`;
983 // Go over all variables in ODB
984 for (let key in odbTreeVar) {
985 const match = key.match(/([^/]+)\/key$/);
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`;
994 // Go over all parameters in ODB
995 for (let key in odbTreeV) {
996 const match = key.match(/([^/]+)\/key$/);
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] || '';
1006 if (typeof value !== "object") {
1007 processParam(name, value, isBool, defValue, optValue, comment);
1014 html = `<td colspan="4">${html}</td>`;
1018// Prepare the parameters/variables (if present) from the ODB as a table
1019function seqParVar(parContainer,debugFlag) {
1021 parContainer - (opt) id of element to be filled with html param table
1022 debugFlag - (opt) true/false run in debug/normal mode
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) {
1037 parContainer = "parContainer";
1039 modbset('/Sequencer/Command/Debug script',true);
1041 modbset('/Sequencer/Command/Start script',true);
1044 html = seqParVarTable(odbTreeV,odbTreeC,odbTreeD,odbTreeO,odbTreeVar,!dlgTable);
1045 if (document.getElementById(parContainer))
1046 document.getElementById(parContainer).innerHTML = html;
1050 // For dialog use parameters
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");
1062 cancelBtn.addEventListener("click", function () {d.remove();});
1063 startBtn.addEventListener("click", function () {
1067 modbset('/Sequencer/Command/Debug script',true);
1069 modbset('/Sequencer/Command/Start script',true);
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;
1079 }).catch (function (error) {
1080 console.error(error);
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>";
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");
1095 cancelBtn.addEventListener("click", function () {
1098 startBtn.addEventListener("click", function () {
1099 e.innerHTML = "<td colspan='4'>" + html + "</td>";
1102 modbset('/Sequencer/Command/Debug script',true);
1104 modbset('/Sequencer/Command/Start script',true);
1109// helper debug function
1110function debugSeq(parContainer) {
1111 seqParVar(parContainer,true);
1114// helper start function
1115function startSeq(parContainer) {
1116 seqParVar(parContainer,false);
1119// helper stop function
1120function stopSeq(flag) {
1121 modbset('/Sequencer/Command/Stop immediately',flag);
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";
1133 e.style.display = "none";
1137// Show error state of sequencer
1138function checkError(element) {
1139 let e = element.parentElement.parentElement;
1140 if (element.value === "") {
1141 e.style.display = "none";
1143 e.style.display = "table-row";
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) {
1151 e - triggering element to identify wait or loop
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)
1158 if (e.id === "waitTrig") {
1159 // Make sure there is only one wait row
1160 document.querySelectorAll('.waitTR').forEach(element => element.remove());
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>
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>]
1188 }).catch (function (error) {
1189 console.error(error);
1194 if (e.id === "waitTrig") document.querySelectorAll('.waitTR').forEach(element => element.remove());
1195 if (e.id === "loopTrig") document.querySelectorAll('.loopTR').forEach(element => element.remove());
1200function extraCell(e) {
1202 // check if there is a wait or loop commands (if non-zero)
1204 if (e.id === "waitTrig") {
1205 // Make sure there is only one wait row
1206 document.querySelectorAll('.waitTR').forEach(element => element.remove());
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"> </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>
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">
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>]
1232 }).catch (function (error) {
1233 console.error(error);
1238 if (e.id === "waitTrig") document.querySelectorAll('.waitTR').forEach(element => element.remove());
1239 if (e.id === "loopTrig") document.querySelectorAll('.loopTR').forEach(element => element.remove());
1243// Helper function to identify browser, 1 FF, 2 Chrome, 3, other
1244function browserType() {
1245 if (navigator.userAgent.indexOf("Chrome") !== -1) {
1247 } else if (navigator.userAgent.indexOf("Firefox") !== -1) {
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 = "";
1260 filechanged.innerHTML = " ✎"; // 2022 too small
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();
1272 revisionIndex = previousRevisions.length - 1;
1276function undoEdit(editor) {
1277 if (revisionIndex < 1) {
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);
1293function redoEdit(editor) {
1294 if (revisionIndex >= previousRevisions.length - 1) {
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);
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) {
1323 startElementId = 'sline' + startLine;
1324 startElement = document.getElementById(startElementId).firstChild;
1326 while (endElement === null && endLine > 0) {
1328 endElementId = 'sline' + endLine;
1329 endElement = document.getElementById(endElementId).lastChild;
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);
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";
1350 localStorage.removeItem("darkMode");
1351 msl_area.style.backgroundColor = "white";
1352 msl_area.style.color = "black";
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);
1366 lineNum.className = "msl_linenum_curr";
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");
1382 message = "Sequencer program is not running.<br>Should I start it?"
1384 message = "Sequencer program is not configured and not running.<br>Should I try to start it anyway?"
1386 dlgConfirm(message,function(resp) {
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");
1397 }).catch(function (error) {
1398 console.error(error);
1401 }).catch(function (error) {
1402 console.error(error);
1405 mjsonrpc_start_program("Sequencer");
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.");
1413 dlgAlert("Failed to start Sequencer!<br>Try to start it manually (msequencer -D)");
1419 }).catch (function (error) {
1420 console.error(error);
1422 }).catch(function (error) {
1423 console.error(error);
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)
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";
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;
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);
1457 const dlgPanel = document.createElement("div");
1458 dlgPanel.className = "dlgPanel";
1459 dlgPanel.id = "dlgPanel";
1460 d.appendChild(dlgPanel);
1462 const content = document.createElement("div");
1463 content.id = "dlgHTML";
1464 content.style.overflow = "auto";
1465 content.innerHTML = html;
1466 dlgPanel.appendChild(content);
1468 document.body.appendChild(d);
1469 console.log( content.style.width, d.style.minWidth);
1472 if (x !== undefined && y !== undefined)
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";
1482 resizeObs.observe(d);