3 Full ODB browser to be used by odb.html as the standard
4 ODB browser or as a key picker with the function
6 odb_picker('/', (flag,path) => {
11 where the '/' is the starting path of the picker and
12 "flag" is true if the OK button has been pressed and
13 false if the Cancel button has been pressed. The
14 "path" variable then contains the selected ODB key
17 Created by Stefan Ritt on July 21st, 2022
22 .odbBrowser .colHeader {
31 -webkit-user-select: none;
35 border: 5px solid #E0E0E0;
38 .odbBrowser img:hover {
39 border: 5px solid #C8C8C8;
45<!-- Create key dialog -->
46<div id="dlgCreate" class="dlgFrame">
47 <div class="dlgTitlebar">Create ODB entry</div>
48 <div class="dlgPanel">
49 <table class="dialogTable" style="border-spacing:10px;">
51 <td style="text-align: right">Directory:</td>
52 <td style="text-align: left" id="odbCreateDir"></td>
55 <td style="text-align: right">Type:</td>
56 <td style="text-align: left">
57 <select id="odbCreateType"
59 document.getElementById('odbCreateStrLenTR').style.visibility =
60 (this.value==='12') ? 'visible' : 'hidden';
62 <option selected value=7>Integer (32-bit)
63 <option value=9>Float (4 Bytes)
64 <option value=12>String
66 <option value=2>Signed Byte
67 <option value=3>Character (8-bit)
68 <option value=4>Word (16-bit)
69 <option value=5>Short Integer (16-bit)
70 <option value=6>Double Word (32-bit)
71 <option value=17>Long Integer (64-bit)
72 <option value=18>Quad Word (64-bit)
73 <option value=8>Boolean
74 <option value=10>Double Float (8 Bytes)
79 <td style="text-align: right">Name:</td>
80 <td style="text-align: left"><input type="text" size="20" maxlength="31"
81 id="odbCreateName" onkeydown="return dlgCreateKeyDown(event, this)"></td>
84 <td style="text-align: right">Array size:</td>
85 <td style="text-align: left"><input type="text" size="6" value="1" id="odbCreateSize"></td>
87 <tr id="odbCreateStrLenTR" style="visibility: hidden">
88 <td style="text-align: right">String length:</td>
89 <td style="text-align: left"><input type="text" size="6" value="32" id="odbCreateStrLen"></td>
92 <button class="dlgButtonDefault" onclick="if(do_new_key(this))dlgHide('dlgCreate');">Create</button>
93 <button class="dlgButton" onclick="dlgHide('dlgCreate')">Cancel</button>
97<!-- Create link dialog -->
98<div id="dlgCreateLink" class="dlgFrame">
99 <div class="dlgTitlebar">Create ODB link</div>
100 <div class="dlgPanel">
101 <table class="dialogTable" style="border-spacing:10px;">
103 <td style="text-align: right">Directory:</td>
104 <td style="text-align: left" id="odbCreateLinkDir"></td>
107 <td style="text-align: right">Link name:</td>
108 <td style="text-align: left"><input type="text" size="20" maxlength="31"
109 id="odbCreateLinkName" onkeydown="return dlgCreateLinkKeyDown(event, this)"></td>
113 <td style="text-align: right">Link target:</td>
114 <td style="text-align: left">
115 <input type="text" size="50"
116 id="odbCreateLinkTarget"
117 onkeydown="return dlgCreateLinkKeyDown(event, this)">
118 <button onclick="pickLinkTarget()">...</button>
122 <button class="dlgButton" onclick="if(do_new_link(this))dlgHide('dlgCreateLink');">Create</button>
123 <button class="dlgButton" onclick="dlgHide('dlgCreateLink')">Cancel</button>
127<!-- Select file dialog -->
128<div id="dlgFileSelect" class="dlgFrame">
129 <div class="dlgTitlebar">Select file</div>
130 <div class="dlgPanel">
132 <div style="margin: auto;width:70%">
133 <input type="file" id="fileSelector" accept=".json">
136 <button class="dlgButton" onclick="loadFileFromSelector(this.parentNode.parentNode);dlgHide('dlgFileSelect');">Upload</button>
137 <button class="dlgButton" onclick="dlgHide('dlgFileSelect')">Cancel</button>
142function odb_browser(id, path, picker) {
144 if (path === undefined) {
146 let url = new URL(window.location.href);
147 path = url.searchParams.get("odb_path");
148 if (path === undefined || path === null || path === "")
154 // add special styles
155 document.head.insertAdjacentHTML("beforeend", odb_css);
157 // add special dialogs
158 let d = document.createElement("div");
159 d.innerHTML = odb_dialogs;
160 document.body.appendChild(d);
164 if (document.title.indexOf("ODB") !== -1) {
166 document.title = "ODB";
168 document.title = "ODB " + path;
171 // push current path to history
172 let url = window.location.href;
173 if (url.search("&odb_path") !== -1)
174 url = url.slice(0, url.search("&odb_path"));
176 url += "&odb_path=" + encodeURIComponent(path);
177 window.history.pushState({'path': path}, '', url);
180 // crate table header
181 d = document.getElementById(id);
182 d.classList.add("odbBrowser");
183 let table = document.createElement('TABLE');
184 table.className = "mtable";
185 table.id = "odbTable";
186 let tb = document.createElement('TBODY');
188 table.style.width = "100%";
189 table.style.marginTop = "0";
190 table.style.marginBottom = "0";
192 table.style.minWidth = "600px";
194 table.appendChild(tb);
196 // Attach ODB to table body
205 handleColumn : false,
206 detailsColumn: false,
207 dragSource: undefined,
208 dragDestination: undefined,
209 dragTargetRow: undefined,
210 dragRowContent : undefined,
214 let tr = document.createElement('TR');
216 let th = document.createElement('TH');
218 th.className = "mtableheader";
220 th.appendChild(document.createTextNode("Online Database Browser"));
222 tr.style.display = 'none';
225 tr = document.createElement('TR');
227 let td = document.createElement('TD');
232 tr.addEventListener('mousedown', select_key); // used to deselect any kay
235 tr = document.createElement('TR');
237 td = document.createElement('TD');
241 '<img src="icons/file-plus.svg" title="Create key CTRL+K" ' +
242 'onmousedown="new_key(this)"> ' +
243 '<img src="icons/folder-plus.svg" title="Create subdirectory" ' +
244 'onmousedown="new_subdir(this)"> ' +
245 '<img src="icons/link.svg" title="Create link" ' +
246 'onmousedown="new_link(this)"> ' +
247 '<img src="icons/edit-3.svg" title="Rename key" ' +
248 'onmousedown="rename_key(this)"> ' +
249 '<img src="icons/shuffle.svg" title="Reorder keys" ' +
250 'onmousedown="toggle_handles(this)"> ' +
251 '<img src="icons/copy.svg" title="Copy from ODB Ctrl+C" ' +
252 'onmousedown="odb_copy(this)"> ' +
253 '<img src="icons/clipboard.svg" title="Paste to ODB Ctrl+V" ' +
254 'onmousedown="odb_paste(this)"> ' +
255 '<img src="icons/folder-open.svg" title="Load ODB Ctrl+O" ' +
256 'onmousedown="odb_load(this)"> ' +
257 '<img src="icons/save.svg" title="Save ODBn Ctrl+S" ' +
258 'onmousedown="odb_save_picker(this)"> ' +
259 '<img src="icons/download.svg" title="Export ODB" ' +
260 'onmousedown="odb_export(this)"> ' +
261 '<img src="icons/upload.svg" title="Import ODB" ' +
262 'onmousedown="odb_import(this)"> ' +
263 '<img src="icons/search.svg" title="Search keys" ' +
264 'onmousedown="search_key(this)"> ' +
265 '<img src="icons/trash-2.svg" title="Delete keys Ctrl+Delete" ' +
266 'onmousedown="odb_delete(this)"> ' +
267 '<img src="icons/more-vertical.svg" title="More menu commands" ' +
268 'onmousedown="more_menu(event)"> ' +
271 tr.addEventListener('mousedown', select_key); // used to deselect any kay
273 tr.style.display = 'none';
276 tr = document.createElement('TR');
278 let a = [ "Handle", "Key", "Value", "Type", "#Val", "Size", "Written", "Mode" ];
280 td = document.createElement('TD');
281 td.className = 'colHeader';
282 if (t === "Handle") {
283 td.setAttribute('name', 'odbHandle');
284 td.style.display = tb.odb.handleColumn ? 'table-cell' : 'none';
285 td.style.width = "10px";
286 } else if (t === "Value") {
287 td.setAttribute('name', 'valueHeader');
289 } else if (t !== "Key" && t !== "Value") {
291 td.setAttribute('name', 'odbExt');
292 td.style.display = tb.odb.detailColumn ? 'table-cell' : 'none';
300 tr.childNodes[2].innerHTML +=
301 "<div title='Show key details' style='display:inline;float:right'>" +
302 "<a id='expRight' href='#' onclick='expand(this);return false;'>⇥</a>"+
305 tr.childNodes[7].innerHTML +=
306 " <span title='Hide key details' style='display:inline;float:right'>" +
307 "<a id='expRight' href='#' onclick='expand(this);return false;'>⇤</a>"+
309 tr.childNodes[7].style.width = '65px';
311 tr.addEventListener('mousedown', select_key); // used to deselect any kay
313 d.appendChild(table);
315 // install shortcut key handler
316 window.addEventListener('keydown', event => {
317 global_keydown(event, tb);
320 // install global click handler to unselect submenus
321 window.addEventListener('mousedown', (event) => {
322 close_submenus(event);
323 // don't unselect if we click on an anchor
324 if (event.target.tagName !== 'A' && event.target.tagName !== 'SELECT') {
325 unselect_all_keys(tb);
326 unselect_all_array_elements(tb);
332 // install event handler for browser history popstate events
333 window.addEventListener('popstate', (event) => {
335 subdir_goto(tb, event.state.path);
341function escapeHTML(text) {
342 let div = document.createElement('div');
343 div.innerText = text;
344 return div.innerHTML;
347function global_keydown(event, tb) {
348 if (event.target.tagName === 'INPUT')
352 if ((event.ctrlKey || event.metaKey) && event.key === 'c') {
358 if ((event.ctrlKey || event.metaKey) && event.key === 'v') {
364 if ((event.ctrlKey || event.metaKey) && event.key === 'k') {
365 event.preventDefault();
371 if ((event.ctrlKey || event.metaKey) && event.key === 's') {
372 event.preventDefault();
378 if ((event.ctrlKey || event.metaKey) && event.key === 'o') {
379 event.preventDefault();
385 if ((event.ctrlKey || event.metaKey) && event.key === 'Backspace') {
391 if (event.keyCode === 27)
392 return close_submenus();
397function close_submenus(event) {
399 // hide submenu if visible
400 let m = document.getElementById('moreMenu');
401 if (m !== null && m.style.display === 'block') {
402 // check if click in moreMenu
403 flag = event.target.parentElement?.parentElement?.parentElement?.id !== "moreMenu";
405 m.style.display = 'none';
408 // hide context menu if visible
409 m = document.getElementById('contextMenu');
410 if (m !== null && m.style.display === 'block') {
411 // check if click in contextMenu
412 flag = event.target.parentElement?.parentElement?.parentElement?.id !== "contextMenu";
414 m.style.display = 'none';
420function odb_picker(path, callback, param) {
421 let d = document.createElement("div");
422 d.className = "dlgFrame odbBrowser";
423 d.style.zIndex = "31";
424 d.style.width = "400px";
425 d.callback = callback;
426 d.callbackParam = param;
427 d.shouldDestroy = true;
429 d.innerHTML = "<div class=\"dlgTitlebar\" id=\"dlgMessageTitle\">" +
430 "Please select ODB key</div>" +
431 "<div class=\"dlgPanel\" style=\"padding: 2px;text-align: left;\">" +
432 "<div id=\"dlgOdbPicker\"></div>" +
433 "<div style='text-align: center;'>" +
434 "<button class=\"dlgButton\" id=\"dlgMessageButton\" type=\"button\" " +
435 " onClick=\"pickerButton(this, true)\">OK</button>" +
436 "<button class=\"dlgButton\" id=\"dlgMessageButton\" type=\"button\" " +
437 " onClick=\"pickerButton(this, false);\">Cancel</button>" +
441 document.body.appendChild(d);
443 // remove any trailing '/' from path
444 if (path.length > 2 && path.slice(-1) == '/')
445 path = path.slice(0, -1);
447 odb_browser('dlgOdbPicker', path, true);
450 d.style.top = "100px";
455function pickerButton(e, flag) {
456 let d = e.parentElement.parentElement.parentElement;
457 let path = d.childNodes[1].childNodes[0].childNodes[0].childNodes[0].odb.selectedKey;
459 path = d.childNodes[1].childNodes[0].childNodes[0].childNodes[0].odb.path;
460 d.callback(flag, path);
461 dlgMessageDestroy(e.parentElement);
464function getOdbTb(e) {
467 while (e.tagName !== 'TBODY') {
476function inline_edit_keydown(event, p) {
477 let keyCode = ('which' in event) ? event.which : event.keyCode;
479 if (keyCode === 27) {
481 p.odbParam.inEdit = false;
482 p.innerHTML = p.odbParam.oldhtml;
486 if (keyCode === 13) {
487 inline_edit_finish(p);
494function inline_edit_cancel(p) {
496 if (p.odbParam.inEdit) {
497 p.odbParam.inEdit = false;
498 p.innerHTML = p.odbParam.oldhtml;
502function inline_edit_finish(p) {
504 let value = p.childNodes[0].value;
505 p.odbParam.inEdit = false;
506 p.innerHTML = p.odbParam.oldhtml;
507 if (p.odbParam.callback)
508 p.odbParam.callback(p, value, p.odbParam.param);
511function inline_edit(event, p, str, callback, size, param) {
512 if (p.odbParam !== undefined && p.odbParam.inEdit)
515 if (event !== undefined)
516 event.stopPropagation(); // don't propagate to rename_key()
519 p.odbParam.param = param;
520 p.odbParam.callback = callback;
521 p.odbParam.oldhtml = p.innerHTML;
522 p.odbParam.inEdit = true;
524 if (size === undefined)
529 p.innerHTML = "<input type='text' size='" + size + "' value='" + str +
530 "' onKeydown='return inline_edit_keydown(event, this.parentNode);'" +
531 " onBlur='return inline_edit_cancel(this.parentNode);'>";
533 // needed for Firefox
534 setTimeout(function () {
535 p.childNodes[0].focus();
536 p.childNodes[0].select();
540function option_edit(event, e, flag) {
541 event.stopPropagation();
542 e.parentNode.inEdit = flag;
545function odb_setoption(p, index) {
546 let tr = p.parentNode;
547 while (tr.tagName !== 'TR')
549 let tb = getOdbTb(tr);
550 let path = tr.odbPath;
552 path += '[' + index + ']';
555 // remove any selected to force update when the ODB value changes later
556 if (p.innerHTML.includes('selected=""'))
557 p.innerHTML = p.innerHTML.replace('selected=""', '');
558 if (p.innerHTML.includes('selected'))
559 p.innerHTML = p.innerHTML.replace('selected=""', '');
561 mjsonrpc_db_set_value(path, value).then(() => {
562 tb.odb.skip_yellow = true;
563 p.parentNode.inEdit = false;
565 }).catch(error => mjsonrpc_error_alert(error));
568function odb_setall(p, value) {
569 // obtain selected array elements
570 let tr = p.parentNode;
571 while (tr.tagName !== 'TR')
573 let tb = getOdbTb(tr);
575 let atr = getArrayTr(tr);
578 if (atr && atr[0]) { // rows could be hidden
586 let path = p.odbParam.param + '[*]';
587 mjsonrpc_db_set_value(path, value).then(() =>
589 ).catch(error => mjsonrpc_error_alert(error));
591 let path = p.odbParam.param;
592 let key = p.parentNode.parentNode.key;
593 path = path.substring(0, path.lastIndexOf('/'));
594 path += '/' + key.name + '[';
596 for (let i=0 ; i<atr.length ; i++)
597 if (atr[i].odbSelected) {
599 if (key.type === TID_STRING || key.type === TID_LINK)
602 values.push(parseFloat(value));
604 path = path.slice(0, -1) + ']';
606 mjsonrpc_db_set_value(path, values).then(() =>
608 ).catch(error => mjsonrpc_error_alert(error));
612function odb_setall_key(p, path, value) {
617 // obtain selected array elements
618 let tr = p.parentNode;
619 while (tr.tagName !== 'TR')
621 let tb = getOdbTb(tr);
623 let atr = getArrayTr(tr);
626 if (atr && atr[0]) { // rows could be hidden
635 mjsonrpc_db_set_value(path, value).then(() =>
637 ).catch(error => mjsonrpc_error_alert(error));
639 let key = p.parentNode.parentNode.key;
642 for (let i=0 ; i<atr.length ; i++)
643 if (atr[i].odbSelected) {
645 values.push(parseFloat(value));
647 path = path.slice(0, -1) + ']';
649 mjsonrpc_db_set_value(path, values).then(() =>
651 ).catch(error => mjsonrpc_error_alert(error));
655function odb_setlink(p, value) {
656 mjsonrpc_call("db_link", {"new_links":[p.odbParam.param],"target_paths":[value]}).then(rpc => {
657 }).catch(error =>{ mjsonrpc_error_alert(error); });
661 let tb = getOdbTb(e);
662 document.getElementById('odbCreateDir').innerHTML = tb.odb.path;
663 dlgShow('dlgCreate', true, e);
664 document.getElementById('odbCreateName').focus();
665 document.getElementById('odbCreateName').select();
668function do_new_key(e) {
669 if (e.parentElement.parentElement.param)
670 e = e.parentElement.parentElement.param;
672 e = e.parentElement.parentElement.parentElement.parentElement.parentElement.parentElement.param;
674 let tb = getOdbTb(e);
675 let path = document.getElementById('odbCreateDir').innerText;
676 if (path === '/') path = "";
677 let name = document.getElementById('odbCreateName').value.trim();
678 let type = parseInt(document.getElementById('odbCreateType').value);
679 let size = parseInt(document.getElementById('odbCreateSize').value);
680 let strlen = parseInt(document.getElementById('odbCreateStrLen').value);
682 if (name.length < 1) {
683 dlgAlert("No name specified");
688 dlgAlert("Bad array length: " + size);
693 dlgAlert("Bad string length " + strlen);
698 param.path = path + "/" + name;
701 param.array_length = size;
703 param.string_length = strlen;
705 mjsonrpc_db_create([param]).then(rpc => {
706 let status = rpc.result.status[0];
707 if (status === 311) {
708 dlgMessage("Error", "ODB entry \"" + name + "\" exists already");
709 } else if (status !== 1) {
710 dlgMessage("Error", "db_create_key() error " + status + ", see MIDAS messages");
712 let tb = getOdbTb(e);
714 }).catch(error => mjsonrpc_error_alert(error));
719function new_link(e) {
720 let tb = getOdbTb(e);
721 document.getElementById('odbCreateLinkDir').innerHTML = tb.odb.path;
722 dlgShow('dlgCreateLink', true, e);
723 document.getElementById('odbCreateLinkName').focus();
724 document.getElementById('odbCreateLinkName').select();
727function pickLinkTarget() {
728 let p = document.getElementById('odbCreateLinkTarget').value;
729 if (p.lastIndexOf('/') > 0)
730 p = p.substring(0, p.lastIndexOf('/'));
733 odb_picker(p, setLinkTarget);
736function setLinkTarget(flag, path) {
738 document.getElementById('odbCreateLinkTarget').value = path;
741function do_new_link(e) {
742 e = e.parentElement.parentElement.param;
743 let tb = getOdbTb(e);
745 let path = document.getElementById('odbCreateLinkDir').innerHTML;
746 if (path === '/') path = "";
747 let name = document.getElementById('odbCreateLinkName').value.trim();
748 let target = document.getElementById('odbCreateLinkTarget').value.trim();
750 if (name.length < 1) {
751 dlgAlert("No name specified");
755 if (target.length < 1) {
756 dlgAlert("No link target specified");
760 if (target[0] !== '/') {
761 dlgAlert("Link target must be absolute (start with a \"/\")");
765 path = path + "/" + name;
767 mjsonrpc_call("db_link", {"new_links": [path], "target_paths":[target]}).then(rpc => {
768 let status = rpc.result.status[0];
769 if (status === 311) {
770 dlgMessage("Error", "ODB entry \"" + name + "\" exists already");
771 } else if (status !== 1) {
772 dlgMessage("Error", "db_create_key() error " + status + ", see MIDAS messages");
776 }).catch(error => mjsonrpc_error_alert(error));
781function new_subdir(e) {
782 dlgQuery('Subdirectory name:', "", do_new_subdir, e);
785function do_new_subdir(subdir, e) {
786 if (subdir === false)
789 // remove any leading or trailing spaces
790 subdir = subdir.trim();
792 if (subdir.length < 1) {
793 dlgAlert("No name specified");
797 let tb = getOdbTb(e);
798 let path = tb.odb.path;
802 param.path = path + "/" + subdir;
803 param.type = TID_KEY;
805 mjsonrpc_db_create([param]).then(rpc => {
806 let status = rpc.result.status[0];
807 if (status === 311) {
808 dlgMessage("Error", "ODB key \"" + subdir + "\" exists already");
809 } else if (status !== 1) {
810 dlgMessage("Error", "db_create_key() error " + status + ", see MIDAS messages");
813 }).catch(error => mjsonrpc_error_alert(error));
816function more_menu(event) {
818 event.stopPropagation(); // don't send click to select_key()
820 let odb = getOdbTb(event.target).odb;
822 let d = document.getElementById('moreMenu');
826 d = document.createElement("div");
828 d.style.display = "none";
829 d.style.position = "absolute";
830 d.className = "msidenav";
831 d.style.borderRadius = "0";
832 d.style.border = "2px solid #808080";
833 d.style.margin = "0";
834 d.style.backgroundColor = "#F0F0F0";
836 let cm = document.createElement("div");
838 // Show open records ----------
840 let mDiv = document.createElement("div");
841 mDiv.className = 'mmenuitem mmenulink';
842 mDiv.innerHTML = "<nobr>Show open records...</nobr>";
843 mDiv.title = "Show ODB keys which are open by other programs";
844 mDiv.onclick = function () {
845 d.style.display = 'none';
846 // window.location.href = "?cmd=odb_sor&odb_path=" + encodeURIComponent(odb.path);
847 show_open_records(event.target);
850 cm.appendChild(mDiv);
852 // Show ODB clients ----------
854 mDiv = document.createElement("div");
855 mDiv.className = 'mmenuitem mmenulink';
856 mDiv.innerHTML = "<nobr>Show ODB clients...</nobr>";
857 mDiv.title = "Show clients currently attached to ODB";
858 mDiv.onclick = function () {
859 d.style.display = 'none';
860 // window.location.href = "?cmd=odb_scl";
861 show_open_clients(event.target);
864 cm.appendChild(mDiv);
866 document.body.appendChild(d);
869 let rect = event.target.getBoundingClientRect();
871 d.style.display = 'block';
872 d.style.left = (rect.left + window.scrollX) + 'px';
873 d.style.top = (rect.bottom + 4 + window.scrollY) + 'px';
875 if (parseInt(d.style.left) + d.offsetWidth > document.body.clientWidth)
876 d.style.left = (document.body.clientWidth - d.offsetWidth) + 'px';
879function change_color(e, color) {
880 if (e.style !== undefined && (e.odb === undefined || !e.odb.inEdit))
881 e.style.color = color;
882 if (e.childNodes && (e.odb === undefined || !e.odb.inEdit))
883 for (const c of e.childNodes)
884 if (c.tagName !== 'SELECT')
885 change_color(c, color);
888function unselect_all_keys(tb) {
889 for (let i=4 ; i<tb.childNodes.length ; i++) {
890 let tr = tb.childNodes[i];
893 tr.odbSelected = false;
894 tr.odbLastSelected = false;
895 tr.style.backgroundColor = '';
896 change_color(tr, '');
900function get_selected_keys(tb) {
902 for (let i=4 ; i<tb.childNodes.length ; i++) {
903 let tr = tb.childNodes[i];
905 paths.push({"path": tr.odbPath, "key": tr.key});
910function unselect_all_array_elements(tb) {
911 for (let i=4 ; i<tb.childNodes.length ; i++) {
912 let tr = tb.childNodes[i];
914 // selected array elements
915 if (tr.childNodes.length > 2) {
916 tr.childNodes[2].style.backgroundColor = '';
917 change_color(tr.childNodes[2], '');
922function select_key(event) {
924 // stay off anchors, input boxes and dragging handles
925 if (event.target.tagName === 'A' || event.target.tagName === 'SELECT' ||
926 event.target.tagName === "INPUT" || event.target.parentNode.getAttribute('name') === 'odbHandle')
929 event.preventDefault();
930 event.stopPropagation();
932 let tr = event.target;
933 let tb = getOdbTb(tr);
934 if (tb === undefined)
939 // hide submenu if visible
940 if (event.type !== "mousemove") {
941 let m = document.getElementById('moreMenu');
942 if (m !== null && m.style.display === 'block')
943 m.style.display = 'none';
945 // hide context menu if visible
946 m = document.getElementById('contextMenu');
947 if (m !== null && m.style.display === 'block')
948 m.style.display = 'none';
950 // un-select all array elements
951 unselect_all_array_elements(tb);
953 // don't select key when we are in edit mode
954 while (tr.tagName !== 'TR') {
959 if (find_input_element(tr))
964 for (let i = 4; i < tb.childNodes.length; i++)
965 tb.childNodes[i].odbSelected = false;
967 // don't select array values
968 if (tr.childNodes[1] && tr.childNodes[1].innerHTML !== "")
969 tr.odbSelected = true;
971 odb.selectedKey = tr.odbPath;
973 // check if click is on header row, if so remove selection further down
974 let headerRow = false;
975 for (let i = 0; i < 4; i++)
976 if (tb.childNodes[i] === tr) {
981 if (event.type === "mousemove") {
983 if (event.buttons === 1 && !headerRow) {
984 // search last selected row
986 for (i1 = 4; i1 < tb.childNodes.length; i1++)
987 if (tb.childNodes[i1].odbLastSelected)
989 if (i1 === tb.childNodes.length)
990 i1 = 4; // none selected, so use first one
993 for (i2 = 4; i2 < tb.childNodes.length; i2++)
994 if (tb.childNodes[i2] === tr)
997 if (i2 < tb.childNodes.length) {
1001 for (let i = i1; i <= i2; i++) {
1002 // don't select arrays
1003 if (tb.childNodes[i].childNodes[1].innerHTML !== "")
1004 tb.childNodes[i].odbSelected = true;
1009 } else { // mousedown
1011 if (event.shiftKey && !headerRow) {
1012 // search last selected row
1014 for (i1 = 4; i1 < tb.childNodes.length; i1++)
1015 if (tb.childNodes[i1].odbLastSelected)
1017 if (i1 === tb.childNodes.length)
1018 i1 = 4; // none selected, so use first one
1021 for (i2 = 4; i2 < tb.childNodes.length; i2++)
1022 if (tb.childNodes[i2] === tr)
1025 if (i2 < tb.childNodes.length) {
1027 [i1, i2] = [i2, i1];
1029 for (let i = i1; i <= i2; i++) {
1030 // don't select arrays
1031 if (tb.childNodes[i].childNodes[1].innerHTML !== "")
1032 tb.childNodes[i].odbSelected = true;
1036 } else if ((event.metaKey || event.ctrlKey) && !headerRow) {
1038 // command key just toggles current selection
1039 tr.odbSelected = !tr.odbSelected;
1041 for (let i = 4; i < tb.childNodes.length; i++)
1042 tb.childNodes[i].odbLastSelected = false;
1044 tr.odbLastSelected = true;
1048 if (event.button !== 2) {
1049 // no key pressed -> un-select all but current
1050 for (let i = 4; i < tb.childNodes.length; i++) {
1051 tb.childNodes[i].odbSelected = false;
1052 tb.childNodes[i].odbLastSelected = false;
1055 // don't select header row and array values
1056 if (!headerRow && tr.childNodes[1].innerHTML !== "") {
1057 tr.odbSelected = true;
1058 tr.odbLastSelected = true;
1059 tr.odbSelectTime = new Date().getTime();
1066 // change color of all rows according to selection
1067 for (let i=4 ; i<tb.childNodes.length ; i++) {
1068 let tr = tb.childNodes[i];
1070 tr.style.backgroundColor = tr.odbSelected ? '#004CBD' : '';
1071 change_color(tr, tr.odbSelected ? '#FFFFFF' : '');
1075function getArrayTr(tr) {
1076 // collect all rows belonging to "tr" in an array
1077 let tb = getOdbTb(tr);
1080 for (i = 0; i < tb.childNodes.length; i++)
1081 if (tb.childNodes[i] === tr)
1083 while (tb.childNodes[i].odbPath.indexOf('[') !== -1)
1085 i++; // first element
1087 atr.push(tb.childNodes[i++]);
1088 } while (i < tb.childNodes.length && tb.childNodes[i].odbPath.indexOf('[') !== undefined);
1092let mouseDragged = false;
1094function mouseStopDragging() {
1095 mouseDragged = false;
1098function select_array_element(event) {
1100 if (event.type === 'mousemove' && !mouseDragged)
1103 // keep off secondary mouse buttons
1104 if (event.button !== 0)
1107 // keep off drop-down boxes
1108 if (event.target.tagName === "SELECT")
1111 event.preventDefault();
1112 event.stopPropagation();
1114 let tr = event.target;
1115 let tb = getOdbTb(tr);
1116 if (tb === undefined)
1119 // don't select key when we are in edit mode
1120 while (tr.tagName !== 'TR') {
1125 if (find_input_element(tr))
1130 // hide submenu if visible
1131 let m = document.getElementById('moreMenu');
1132 if (m !== null && m.style.display === 'block')
1133 m.style.display = 'none';
1135 // hide context menu if visible
1136 m = document.getElementById('contextMenu');
1137 if (m !== null && m.style.display === 'block')
1138 m.style.display = 'none';
1140 // remove selection from all non-array keys
1141 for (let i=0 ; i<tb.childNodes.length ; i++) {
1142 if (tb.childNodes[i].childNodes.length > 1 &&
1143 tb.childNodes[i].childNodes[1].innerHTML !== "" &&
1144 tb.childNodes[i].odbSelected) {
1145 tb.childNodes[i].odbSelected = false;
1146 tb.childNodes[i].style.backgroundColor = '';
1147 change_color(tb.childNodes[i], '');
1151 // create array of all array elements
1152 let atr = getArrayTr(tr);
1154 if (event.type === 'mousedown') {
1155 mouseDragged = true;
1158 // un-select all but current
1159 for (const tr of atr) {
1160 tr.odbSelected = false;
1161 tr.odbLastSelected = false;
1163 tr.odbSelected = true;
1164 tr.odbLastSelected = true;
1165 odb.selectedKey = tr.odbPath;
1169 if (event.shiftKey) {
1170 // search last selected row
1172 for (i1 = 0; i1 < atr.length; i1++)
1173 if (atr[i1].odbLastSelected)
1175 if (i1 === atr.length)
1176 i1 = 0; // non selected, so use first one
1179 for (i2 = 0; i2 < atr.length; i2++)
1183 if (i2 < atr.length) {
1185 [i1, i2] = [i2, i1];
1187 for (let i = i1; i <= i2; i++)
1188 atr[i].odbSelected = true;
1191 } else if (event.metaKey || event.ctrlKey) {
1193 // command key just toggles current selection
1194 tr.odbSelected = !tr.odbSelected;
1196 for (const tr of atr)
1197 tr.odbLastSelected = false;
1199 tr.odbLastSelected = true;
1203 // no key pressed -> un-select all but current
1204 for (const tr of atr) {
1205 tr.odbSelected = false;
1206 tr.odbLastSelected = false;
1208 tr.odbSelected = true;
1209 tr.odbLastSelected = true;
1214 if (event.type === 'mousemove' && mouseDragged) {
1216 if (!event.shiftKey && !event.metaKey && !event.ctrlKey) {
1218 for (const tr of atr)
1219 tr.odbSelected = false;
1221 // search last selected row
1222 for (i1 = 0; i1 < atr.length; i1++)
1223 if (atr[i1].odbLastSelected)
1225 if (i1 === atr.length)
1226 i1 = 0; // non selected, so use first one
1229 for (i2 = 0; i2 < atr.length; i2++)
1233 if (i2 < atr.length) {
1235 [i1, i2] = [i2, i1];
1237 for (let i = i1; i <= i2; i++)
1238 atr[i].odbSelected = true;
1243 // change color of all rows according to selection
1244 for (const e of atr) {
1245 if (e.childNodes.length >= 3) {
1246 e.childNodes[2].style.backgroundColor = e.odbSelected ? '#004CBD' : '';
1247 change_color(e.childNodes[2], e.odbSelected ? '#FFFFFF' : '');
1252function context_menu(event) {
1253 // don't show context menu if we are in input box (user wants traditional copy/paste)
1254 if (event.target.tagName === 'INPUT')
1257 event.preventDefault();
1258 event.stopPropagation();
1260 let tr = event.target;
1261 while (tr.tagName !== 'TR')
1263 let tb = getOdbTb(tr);
1265 let path = tr.odbPath;
1267 if (!tr.odbSelected)
1270 let d = document.getElementById('contextMenu');
1272 // create menu on the first call
1274 d = document.createElement("div");
1275 d.id = "contextMenu";
1276 d.style.display = "none";
1277 d.style.position = "absolute";
1278 d.className = "msidenav";
1279 d.style.borderRadius = "0";
1280 d.style.border = "2px solid #808080";
1281 d.style.margin = "0";
1282 d.style.backgroundColor = "#F0F0F0";
1284 let cm = document.createElement("div");
1286 let menu = ["Copy key", "Copy plain text", "Delete key", "Rename key...",
1287 "Open in new tab...", "Open in new window..."];
1290 for (const m of menu) {
1291 mDiv = document.createElement("div");
1292 mDiv.className = 'mmenuitem mmenulink';
1293 mDiv.innerHTML = '<nobr>' + m + '</nobr>';
1295 cm.appendChild(mDiv);
1299 document.body.appendChild(d);
1302 // set event handler for copy menu
1303 d.childNodes[0].childNodes[0].onmousedown = function () {
1304 d.style.display = 'none';
1308 // set event handler for copy plain text menu
1309 d.childNodes[0].childNodes[1].onmousedown = function () {
1310 d.style.display = 'none';
1311 let selKeys = get_selected_keys(tb);
1313 let dirFlag = false;
1314 if (selKeys.length === 0) {
1315 paths.push(tb.odb.path);
1318 for (const k of selKeys)
1322 mjsonrpc_db_copy(paths).then(rpc => {
1325 let needPath = false;
1328 dir = tb.odb.path.substring(tb.odb.path.lastIndexOf('/') + 1);
1331 text += odbASCII(rpc.result.data[0], dir);
1334 for (let i = 0; i < rpc.result.data.length; i++) {
1335 if (selKeys[i].key.type === TID_KEY) {
1336 text += odbASCII(rpc.result.data[i], paths[i]);
1340 text += odbASCII(rpc.result.data[i], tb.odb.path);
1342 text += odbASCII(rpc.result.data[i]);
1350 query = "Copy ODB directory \"" + dir + "\" to clipboard?";
1351 else if (paths.length === 1)
1352 query = "Copy ODB key \"" + selKeys[0].key.name + "\" to clipboard?";
1354 query = "Copy " + paths.length + " ODB keys to clipboard?";
1356 dlgConfirm(query, function (flag, t) {
1358 if (navigator.clipboard && navigator.clipboard.writeText) {
1360 navigator.clipboard.writeText(text).then( () => {
1361 }).catch(e => dlgAlert(e));
1369 }).catch(error => mjsonrpc_error_alert(error));
1372 // set event handler for delete key menu
1373 d.childNodes[0].childNodes[2].onmousedown = function () {
1374 d.style.display = 'none';
1378 // set event handler for rename menu
1379 d.childNodes[0].childNodes[3].onmousedown = function () {
1380 d.style.display = 'none';
1383 if (tr.key.type === TID_KEY)
1384 elem = tr.childNodes[1].childNodes[2];
1386 elem = tr.childNodes[1].childNodes[1];
1388 let key = elem.innerHTML.trim();
1399 // set event handler for open in new tab
1400 d.childNodes[0].childNodes[4].onmousedown = function () {
1401 d.style.display = 'none';
1403 let url = window.location.href;
1404 if (url.search("&odb_path") !== -1)
1405 url = url.slice(0, url.search("&odb_path"));
1406 url += "&odb_path=" + encodeURIComponent(tr.odbPath); // convert spaces to %20 etc
1408 window.open(url, '_blank').focus();
1411 // set event handler for open in new window
1412 d.childNodes[0].childNodes[5].onmousedown = function () {
1413 d.style.display = 'none';
1415 let url = window.location.href;
1416 if (url.search("&odb_path") !== -1)
1417 url = url.slice(0, url.search("&odb_path"));
1418 url += "&odb_path=" + encodeURIComponent(tr.odbPath); // convert spaces to %20 etc
1420 window.open(url, "", "menubar=yes,location=yes,status=yes");
1423 let rect = event.target.getBoundingClientRect();
1425 d.style.display = 'block';
1426 d.style.left = (event.offsetX + rect.left + window.scrollX) + 'px';
1427 d.style.top = (event.offsetY+rect.top + window.scrollY) + 'px';
1429 if (parseInt(d.style.left) + d.offsetWidth > document.body.clientWidth)
1430 d.style.left = (document.body.clientWidth - d.offsetWidth) + 'px';
1436 let tb = getOdbTb(e);
1439 let rarr = document.getElementById('expRight');
1440 let cells = document.getElementsByName('odbExt');
1441 let table = document.getElementById('odbTable');
1442 odb.detailsColumn = !odb.detailsColumn;
1444 if (odb.detailsColumn) {
1445 for (const d of cells)
1446 d.style.display = 'table-cell';
1447 rarr.style.display = 'none';
1448 table.style.minWidth = '800px';
1450 for (const d of cells)
1451 d.style.display = 'none';
1452 rarr.style.display = 'inline';
1453 table.style.minWidth = '600px';
1455 odb.skip_yellow = true;
1459function clear_expanded(odb) {
1460 for (const key of odb.key) {
1461 if (key.type === TID_KEY)
1462 key.subdir_open = false;
1464 key.expanded = false;
1468function toggle_handles(e) {
1469 let tb = getOdbTb(e);
1472 // clear all expanded flags
1473 clear_expanded(odb);
1476 let n = document.getElementsByName('odbHandle');
1477 odb.handleColumn = !odb.handleColumn;
1479 if (odb.handleColumn) {
1480 odb.subdirFirst = false;
1481 for (const d of n) {
1482 let tr = d.parentNode;
1483 if (tr.firstChild.className !== 'colHeader') {
1484 tr.setAttribute('draggable', true);
1485 tr.addEventListener('dragstart', drag_start);
1486 tr.addEventListener('dragover', drag_move);
1487 tr.addEventListener('dragend', drag_end);
1490 d.style.display = 'table-cell';
1493 odb.subdirFirst = true;
1494 for (const d of n) {
1495 let tr = d.parentNode;
1496 if (tr.firstChild.className !== 'colHeader') {
1497 tr.removeAttribute('draggable');
1498 tr.removeEventListener('dragstart', drag_start);
1499 tr.removeEventListener('dragover', drag_move);
1500 tr.removeEventListener('dragend', drag_end);
1503 d.style.display = 'none';
1510function toggle_expanded(e) {
1512 while (tr.tagName !== 'TR')
1515 if (tr.key.num_values >= 5000 && !tr.key.expanded) {
1516 dlgConfirm("ODB key \""+ tr.key.name + "\" has " + tr.key.num_values +
1517 " array elements. This can take very long. Do you want to continue?", do_toggle_expanded, tr);
1519 do_toggle_expanded(true, tr);
1522function do_toggle_expanded(flag, tr) {
1524 let tb = getOdbTb(tr);
1525 tr.key.expanded = !tr.key.expanded;
1526 unselect_all_keys(tb);
1527 unselect_all_array_elements(tb);
1532function odb_delete(e) {
1534 let tb = getOdbTb(e);
1535 let currentPath = tb.odb.path;
1537 for (const tr of tb.childNodes) {
1539 paths.push(tr.odbPath);
1542 if (paths.length == 0) {
1543 // strip last directory from current path
1544 let newPath = currentPath;
1545 if (newPath.lastIndexOf('/') !== -1)
1546 newPath = newPath.substring(0, newPath.lastIndexOf('/'));
1550 dlgConfirm('Are you sure to delete odb directory \"' + currentPath + '\" ?',
1551 do_odb_delete, {"e": e, "paths": [currentPath], "newPath": newPath} );
1552 } else if (paths.length == 1)
1553 dlgConfirm('Are you sure to delete odb key \"' + paths[0] + '\" ?',
1554 do_odb_delete, {"e": e, "paths": paths});
1556 dlgConfirm('Are you sure to delete ' + paths.length + ' keys ?',
1557 do_odb_delete, {"e": e, "paths": paths});
1560function do_odb_delete(flag, param) {
1562 mjsonrpc_db_delete(param.paths).then((rpc) => {
1564 if (rpc.result.status[0] === 318) {
1565 dlgAlert("ODB key is write protected and cannot be deleted");
1566 } else if (rpc.result.status[0] === 320) {
1567 dlgAlert("ODB key is locked by other program and cannot be deleted");
1568 } else if (rpc.result.status[0] !== 1) {
1569 dlgAlert("ODB error: " + rpc.result.status[0]);
1572 subdir_goto(param.e, param.newPath);
1574 let tb = getOdbTb(param.e);
1578 }).catch(error => mjsonrpc_error_alert(error));
1582function rename_key(element) {
1583 let tb = getOdbTb(element);
1586 for (const t of tb.childNodes) {
1587 if (t.odbSelected) {
1594 dlgAlert("Please select key to be renamed");
1596 dlgAlert("Please select only single key to be renamed");
1598 tr.odbSelected = false;
1599 tr.style.backgroundColor = '';
1600 change_color(tr, '');
1604 if (tr.key.type === TID_KEY)
1605 elem = tr.childNodes[1].childNodes[2];
1607 elem = tr.childNodes[1].childNodes[1];
1609 let key = elem.innerHTML;
1612 inline_edit(undefined,
1621function do_rename_key(p, str, path) {
1623 dlgAlert("Empty name not allowed");
1627 mjsonrpc_call("db_rename", { "paths": [path], "new_names": [str]})
1629 .catch(error => mjsonrpc_error_alert(error));
1631 let old = p.parentElement.parentElement.oldKey;
1633 p.innerHTML = p.innerHTML.replace(old, str);
1636function search_key() {
1637 dlgQuery("Enter key name (substring case insensitive):", "", do_search_key);
1640function do_search_key(str) {
1642 window.location.href = "?cmd=Find&value=" + str;
1645function odb_copy(e) {
1646 let tb = getOdbTb(e);
1647 let selKeys = get_selected_keys(tb);
1649 let dirFlag = false;
1650 if (selKeys.length === 0) {
1651 paths.push(tb.odb.path);
1654 for (const k of selKeys)
1657 mjsonrpc_db_copy(paths).then(rpc => {
1660 // generate text from objects, add top directoy which is missing from db_copy
1661 for (const [i,d] of rpc.result.data.entries()) {
1664 let dir = tb.odb.path.substring(tb.odb.path.lastIndexOf('/')+1);
1667 text += '{ \"'+ dir +
1668 '\": '+JSON.stringify(d, null, ' ')+'}';
1669 } else if (selKeys[i].key.type === TID_KEY) {
1670 text += '{ \"'+ selKeys[i].key.name +'\": '+JSON.stringify(d, null, ' ')+'}';
1672 text += JSON.stringify(d, null, ' ');
1674 text += JSON.stringify(d, null, ' ').substring(1); // strip first '{'
1675 if (i < rpc.result.data.length - 1)
1676 text = text.substring(0, text.lastIndexOf('}')) + ','; // replace last '}' by ','
1681 query = "Copy ODB directory \"" + paths + "\" to clipboard?";
1683 if (paths.length === 1)
1684 query = "Copy ODB key \"" + paths + "\" to clipboard?";
1686 query = "Copy " + paths.length + " ODB keys to clipboard?";
1689 dlgConfirm(query, function (flag, t) {
1690 if (flag && navigator.clipboard && navigator.clipboard.writeText) {
1693 navigator.clipboard.writeText(t).then( () => {
1694 }).catch(e => dlgAlert(e));
1701 // store text locally as a backup
1703 sessionStorage.setItem('odbclip', text);
1708 }).catch(error => mjsonrpc_error_alert(error));
1711function odb_paste(e) {
1712 let tb = getOdbTb(e);
1715 let text = sessionStorage.getItem('odbclip');
1717 let keys = JSON.parse(text);
1718 odb_paste_keys(tb, keys);
1722 dlgAlert("Invalid JSON format in clipboard:<br /><br />" + error);
1726 if (navigator.clipboard && navigator.clipboard.readText) {
1727 navigator.clipboard.readText().then(text => {
1730 let keys = JSON.parse(text);
1731 odb_paste_keys(tb, keys);
1734 dlgAlert("Nothing stored in clipboard");
1737 dlgAlert("Invalid JSON format in clipboard:<br /><br />" + error);
1741 dlgAlert("Paste not possible in this browser");
1744function odb_paste_keys(tb, newKeys) {
1747 for (const key in newKeys) {
1748 if (key.indexOf('/') !== -1)
1751 // check for existing key in current keys
1752 let cKeyMaxIndex = 0;
1757 for (const cKey of odb.key) {
1758 if (cKey.name.indexOf(' copy') === -1)
1761 ckb = cKey.name.substring(0, cKey.name.indexOf(' copy'));
1766 if (cKey.name.indexOf(' copy') !== -1) {
1767 cKeyIndex = parseInt(cKey.name.substring(cKey.name.indexOf(' copy')+5));
1768 if (isNaN(cKeyIndex))
1770 if (cKeyIndex > cKeyMaxIndex)
1771 cKeyMaxIndex = cKeyIndex;
1776 // rename new key if name exists
1778 let newName = cKeyBare + ' copy';
1780 newName += ' ' + (cKeyIndex+1).toString();
1782 newKeys[newName] = newKeys[key];
1783 delete newKeys[key];
1785 newKeys[newName + '/key'] = newKeys[key + '/key'];
1786 delete newKeys[key + '/key'];
1790 mjsonrpc_db_paste([odb.path], [newKeys]).then(rpc =>
1791 odb_update(tb)).catch(error =>
1796function odbASCII(o, path) {
1797 // convert ODB keys to plain ASCII representation
1799 let need_path = true;
1800 if (path === undefined)
1802 for (const key in o) {
1803 if (key.indexOf('/') !== -1)
1806 if (o[key+'/key'] === undefined && path) {
1807 t += odbASCII(o[key], path + '/' + key);
1814 t += '\n[' + path + ']\n';
1817 let tid = o[key+'/key'].type;
1818 let num_values = o[key+'/key'].num_values;
1821 if (num_values > 1) {
1823 for (let i=0 ; i<num_values ; i++) {
1824 t += '[' + i + ']\t';
1825 t += mie_to_string(tid, o[key][i]);
1830 t += mie_to_string(tid, o[key]);
1837function odb_export(e) {
1838 let tb = getOdbTb(e);
1839 mjsonrpc_db_copy([tb.odb.path]).then(rpc => {
1841 let dirs = tb.odb.path.split('/');
1842 let filename = dirs[dirs.length - 1];
1843 if (filename === '')
1845 filename += ".json";
1848 "/MIDAS version": "2.1",
1849 "/filename": filename,
1850 "/ODB path": tb.odb.path
1852 header = JSON.stringify(header, null, ' ');
1853 header = header.substring(0, header.length-2) + ','; // strip trailing '}'
1855 let odbJson = JSON.stringify(rpc.result.data[0], null, ' ');
1856 if (odbJson.indexOf('{') === 0)
1857 odbJson = odbJson.substring(1); // strip leading '{'
1859 odbJson = header + odbJson;
1861 // use trick from FileSaver.js
1862 let a = document.getElementById('downloadHook');
1864 a = document.createElement("a");
1865 a.style.display = "none";
1866 a.id = "downloadHook";
1867 document.body.appendChild(a);
1870 let blob = new Blob([odbJson], {type: "text/json"});
1871 let url = window.URL.createObjectURL(blob);
1874 a.download = filename;
1876 window.URL.revokeObjectURL(url);
1877 dlgAlert("ODB subtree \"" + tb.odb.path +
1878 "\" downloaded to file \"" + filename + "\"");
1880 }).catch(error => mjsonrpc_error_alert(error));
1884function odb_save_picker(e) {
1885 let tb = getOdbTb(e);
1886 _odb_path = tb.odb.path;
1887 file_picker("", "*.json", odb_save, true);
1889function odb_save(filename) {
1890 mjsonrpc_db_copy([_odb_path]).then(rpc => {
1892 if (filename.indexOf('.json') === -1)
1893 filename += '.json';
1896 "/MIDAS version": "2.1",
1897 "/filename": filename,
1898 "/ODB path": _odb_path
1900 header = JSON.stringify(header, null, ' ');
1901 header = header.substring(0, header.length-2) + ','; // strip trailing '}'
1903 let odbJson = JSON.stringify(rpc.result.data[0], null, ' ');
1904 if (odbJson.indexOf('{') === 0)
1905 odbJson = odbJson.substring(1); // strip leading '{'
1907 odbJson = header + odbJson;
1909 file_save_ascii(filename, odbJson, "ODB subtree \"" + _odb_path +
1910 "\" saved to file \"" + filename + "\"");
1912 }).catch(error => mjsonrpc_error_alert(error));
1915async function odb_import(e) {
1917 // Check if showOpenFilePicker is available in the browser
1918 // The "else" is more of a JS standard
1919 if (window.showOpenFilePicker) {
1921 [fileHandle] = await window.showOpenFilePicker();
1922 const file = await fileHandle.getFile();
1923 const text = await file.text();
1924 pasteBuffer(e, text, file.name);
1926 // Show a traditional file input element
1927 const fileInput = document.createElement('input');
1928 fileInput.type = 'file';
1929 fileInput.accept = 'application/JSON';
1930 fileInput.addEventListener('change', (event) => {
1931 const file = event.target.files[0];
1933 const reader = new FileReader();
1934 reader.onload = async (evObj) => {
1935 const text = evObj.target.result;
1936 pasteBuffer(e, text, file.name);
1938 reader.readAsText(file);
1941 // Trigger the file input element
1945 if (error.name !== 'AbortError') {
1946 // Handle errors as needed
1952function loadFileFromSelector(e) {
1954 let input = document.getElementById('fileSelector');
1955 let file = input.files[0];
1956 if (file !== undefined) {
1957 let reader = new FileReader();
1958 reader.readAsText(file);
1960 reader.onerror = function () {
1961 dlgAlert('File read error: ' + reader.error);
1964 reader.onload = function () {
1965 pasteBuffer(e.param, reader.result, file.name);
1970function odb_load(e) {
1971 let tb = getOdbTb(e);
1972 let path = tb.odb.path;
1973 _odb_path = tb.odb.path;
1974 file_picker("", "*.json", loadFile);
1977function loadFile(filename) {
1978 file_load_ascii(filename, (text) => pasteBuffer(undefined, text, filename));
1981function pasteBuffer(e, text, filename) {
1985 if (e === undefined)
1994 odbJson = JSON.parse(text);
1999 // delete /MIDAS version, /filename etc.
2000 for (let [name, value] of Object.entries(odbJson)) {
2001 if (name[0] === '/')
2002 delete odbJson[name];
2004 mjsonrpc_db_paste([path], [odbJson]).then(rpc =>
2005 dlgAlert("File \"" + filename + "\" successfully loaded to ODB at " + path)).catch(error =>
2006 mjsonrpc_error_alert(error));
2009function make_dialog_scroll(tb) {
2010 let d = tb.parentElement.parentElement.parentElement.parentElement;
2013 d.style.position = "absolute"; // remove "fixed"
2016function subdir_open_click(event, e) {
2017 event.stopPropagation();
2018 let tr = e.parentNode;
2019 while (tr.tagName !== 'TR')
2021 let path = tr.odbPath;
2023 subdir_open(event.target, path);
2026// user clicks on subdirectory, so open it
2027function subdir_open(tr, path) {
2028 while (tr.tagName !== 'TR')
2030 let tb = getOdbTb(tr);
2033 // don't open subdirs if in reorder mode
2034 if (odb.handleColumn)
2037 unselect_all_keys(tb);
2038 unselect_all_array_elements(tb);
2039 make_dialog_scroll(tb);
2041 // find key belonging to 'path'
2042 tr.key.subdir_open = !tr.key.subdir_open;
2043 tb.odb.skip_yellow = true;
2047function subdir_goto_click(event, e) {
2048 event.stopPropagation();
2049 let tr = e.parentNode;
2050 while (tr.tagName !== 'TR')
2052 let path = tr.odbPath;
2054 // don't open subdir if we are in rename mode
2055 if (tr.childNodes[1].childNodes[2].odbParam &&
2056 tr.childNodes[1].childNodes[2].odbParam.inEdit)
2059 subdir_goto(event.target, path);
2062function subdir_goto(e, path) {
2063 let tb = getOdbTb(e);
2066 unselect_all_keys(tb);
2067 unselect_all_array_elements(tb);
2070 if (odb.updateTimer !== undefined)
2071 window.clearTimeout(odb.updateTimer);
2075 let url = window.location.href;
2076 if (url.search("&odb_path") !== -1)
2077 url = url.slice(0, url.search("&odb_path"));
2078 url += "&odb_path=" + encodeURIComponent(path); // convert spaces to %20 etc
2081 // "cmd=ODB" vs. "cmd=ODB&odb_path=/"
2082 if (window.location.href.indexOf('&') === -1 &&
2086 if (url !== window.location.href && !skip)
2087 window.history.pushState({'path': path}, '', url);
2091 if (document.title.indexOf("ODB") !== -1) {
2093 document.title = "ODB";
2095 document.title = "ODB " + path;
2098 // update ODB object
2103 odb.skip_yellow = true;
2108// push all paths of open subdirectories recursively for a following db_ls
2109function push_paths(paths, odb, path) {
2110 odb.dataIndex = paths.length;
2112 for (let i=0 ; i< odb.key.length ; i++) {
2113 if (odb.key[i].type === TID_KEY && odb.key[i].subdir_open) {
2114 if (odb.key[i].value.id === undefined) {
2115 odb.key[i].value = {
2117 path: odb.path === '/' ? '/' + odb.key[i].name : odb.path + '/' + odb.key[i].name,
2118 level: odb.level + 1,
2119 dataIndex: paths.length,
2123 push_paths(paths, odb.key[i].value, odb.key[i].value.path);
2128// update overall ODB display
2129function odb_update(tb) {
2131 // console.log("odb_update" + new Date());
2133 if (tb === undefined)
2134 tb = document.getElementById("odbTable").firstChild;
2137 if (odb.updateTimer !== undefined)
2138 window.clearTimeout(odb.updateTimer);
2139 let row = { i:4, nvk:0 };
2141 // show clickable ODB path in top row
2143 if (odb.path === '/')
2146 dirs = odb.path.split('/');
2148 let s = "<a href=\"#\" onclick=\"subdir_goto(this, '/');return false;\" title=\"Root directory\">" +
2149 "<img src=\"icons/slash-square.svg\" style=\"border:none; height: 16px; vertical-align: middle;margin-bottom: 2px;\">" +
2151 for (let i=1 ; i<dirs.length ; i++) {
2152 path += "/" + dirs[i];
2153 s += "<a href=\"#\" onclick=\"subdir_goto(this, '" + escapeHTML(path) + "');return false;\">"+escapeHTML(dirs[i])+"</a>";
2154 if (i < dirs.length - 1)
2155 s += " / ";
2157 if (s !== tb.childNodes[1].firstChild.innerHTML)
2158 tb.childNodes[1].firstChild.innerHTML = s;
2162 push_paths(paths, odb, odb.path);
2164 mjsonrpc_db_ls(paths).then(rpc => {
2165 odb_extract(odb, rpc.result.data);
2166 odb_print(tb, row, tb.odb);
2167 odb.skip_yellow = false;
2169 // call timer in one second to update again
2170 odb.updateTimer = window.setTimeout(odb_update, 1000, tb);
2174// extract keys and put them into odb tree from db_ls result in dataArray
2175function odb_extract(odb, dataArray) {
2176 let data = dataArray[odb.dataIndex];
2178 for (const item in data) {
2179 if (item.indexOf('/') !== -1)
2184 key.value = data[item];
2185 let linkFlag = data[item + '/key'] !== undefined && data[item + '/key'].link !== undefined;
2186 if (!linkFlag && typeof key.value === 'object' && Object.keys(key.value).length === 0) {
2188 key.subdir_open = false;
2191 key.last_written = 0;
2192 key.access_mode = 0;
2194 key.type = data[item + '/key'].type;
2195 key.link = data[item + '/key'].link;
2196 key.num_values = data[item + '/key'].num_values;
2197 if (key.num_values === undefined)
2199 if (data[item + '/key'].item_size !== undefined)
2200 key.item_size = data[item + '/key'].item_size;
2202 key.item_size = tid_size[key.type];
2203 key.last_written = data[item + '/key'].last_written;
2204 key.access_mode = data[item + '/key'].access_mode;
2206 key.options = (key.name.substring(0, 7) === "Options");
2208 if (odb.key.length <= n) {
2209 key.expanded = (key.num_values > 1 && key.num_values <= 10);
2211 key.expanded = false;
2214 if (odb.key[n].subdir_open) {
2215 key.subdir_open = true;
2216 key.value = odb.key[n].value;
2218 if (odb.key[n].expanded !== undefined) {
2219 key.expanded = odb.key[n].expanded;
2225 // if current key is a subdirectory, call us recursively
2226 if (key.type === TID_KEY && key.subdir_open) {
2227 odb.key[n-1].value.picker = odb.picker;
2228 odb_extract(odb.key[n-1].value, dataArray);
2232 // if local data remaining, ODB key must have been deleted, so delete also local data
2233 if (odb.key.length > n) {
2234 odb.key = odb.key.slice(0, n);
2235 odb.skip_yellow = true;
2239function odb_print_all(tb) {
2241 let row = { i:4, nvk:0 };
2243 odb.skip_yellow = true;
2244 odb_print(tb, row, tb.odb);
2247function odb_print(tb, row, odb) {
2249 // first print all directories
2250 if (odb.subdirFirst) {
2251 for (const key of odb.key) {
2252 if (key.type !== TID_KEY)
2255 // Print current key
2256 odb_print_key(tb, row, odb.path, key, odb.level);
2259 // Print whole subdirectory if open
2260 if (key.subdir_open) {
2262 // Propagate flags to all subkeys
2263 key.value.skip_yellow = odb.skip_yellow;
2264 key.value.subdirFirst = odb.subdirFirst;
2266 // Print whole subdirectory
2267 odb_print(tb, row, key.value);
2272 // now print all keys
2273 for (const key of odb.key) {
2275 if (odb.subdirFirst)
2276 if (key.type === TID_KEY)
2279 // search for "Options <key>"
2280 let options = odb.key.find(o => o.name === 'Options ' + key.name);
2282 // Print current key
2283 odb_print_key(tb, row, odb.path, key, odb.level, options ? options.value : undefined);
2288 // Hide 'value' if only directories are listed
2289 let ds = document.getElementsByName('valueHeader');
2292 d.style.display = row.nvk > 0 ? 'table-cell' : 'none';
2294 // At the end, remove old rows if subdirectory has been closed
2295 if (odb.level === 0)
2296 while (tb.childNodes.length > row.i)
2297 tb.removeChild(tb.childNodes[tb.childNodes.length-1]);
2300function find_input_element(e) {
2301 if (e === undefined)
2303 for (const c of e.childNodes) {
2304 if (c.tagName === 'INPUT')
2306 if (c.tagName === 'SELECT' && e.inEdit)
2308 if (c.childNodes.length > 0)
2309 if (find_input_element(c))
2315function bnToHex(val) {
2318 hex = ((1n << 64n) + BigInt(val)).toString(16).toUpperCase();
2319 hex = hex.padStart(16, '0');
2321 hex = val.toString(16).toUpperCase();
2322 if (hex.length % 2) {
2330function odb_print_key(tb, row, path, key, level, options) {
2334 let keyPath = (path === '/' ? '/' + key.name : path + '/' + key.name);
2337 let tr = document.createElement('TR');
2340 let td = document.createElement('TD');
2341 td.setAttribute('name', 'odbHandle');
2342 td.style.display = odb.handleColumn ? 'table-cell' : 'none';
2343 td.style.width = "10px";
2345 td.innerHTML = '<img style="cursor: all-scroll; height: 13px; padding: 0; border: 0" src="icons/menu.svg">';
2347 td.childNodes[0].setAttribute('draggable', false);
2350 td = document.createElement('TD');
2353 // Add three spaces of indent for each level
2354 let indent = "<span>";
2355 for (let i = 0; i < level; i++)
2356 indent += " ";
2357 indent += "</span>";
2359 // Remember indent level for key rename
2360 tr.level = odb.level;
2362 // Remember key (need to toggle expanded flag)
2365 // Remember key path
2366 tr.odbPath = keyPath;
2368 // Set special class for "Options ..." keys
2369 if (key.options && !odb.detailsColumn)
2370 tr.style.display = 'none';
2372 // Print subdir with open subdir handler
2373 if (key.type === TID_KEY) {
2374 if (odb.handleColumn) {
2375 // do not show any links in re-order mode
2376 td.innerHTML = indent + " \u25B8 " + escapeHTML(key.name);
2378 let handler = "onclick=\"subdir_open_click(event, this);return false;\" ";
2380 if (key.subdir_open)
2381 td.innerHTML = indent + "<a href='#' " +
2383 "> \u25BE </a>";
2385 td.innerHTML = indent + "<a href='#' " +
2387 "> \u25B8 </a>";
2389 handler = "onclick=\"subdir_goto_click(event, this);return false;\" ";
2390 td.innerHTML += "<a href='#' " + handler + ">" + escapeHTML(key.name) + "</a>";
2395 td.innerHTML += ' → <span>' + key.link + '</span>';
2397 td.innerHTML += ' → <span>'+ '<a href="#" ' +
2398 'onclick="inline_edit(event, this.parentNode, \'' +
2399 key.link + '\', odb_setlink, undefined, \'' + keyPath +
2400 '\'); return false;" ' +
2401 ' title="Change value">' + key.link +
2405 } else if (key.link === undefined) {
2406 td.innerHTML = indent + '<span>' + escapeHTML(key.name) + '</span>';
2408 td.innerHTML = indent + '<span>' + escapeHTML(key.name) + '</span>';
2410 td.innerHTML += ' → <span>' + escapeHTML(key.link) + '</span>';
2412 td.innerHTML += ' → <span>'+ '<a href="#" ' +
2413 'onclick="inline_edit(event, this.parentNode, \'' +
2414 key.link + '\', odb_setlink, undefined, \'' + keyPath +
2415 '\'); return false;" ' +
2416 ' title="Change value">' + escapeHTML(key.link) +
2420 // Subdirs occupy all 8 columns
2421 if (key.type === TID_KEY) {
2424 if (tb.childNodes.length > row.i) {
2425 let selected = tb.childNodes[row.i].odbSelected;
2427 tr.style.backgroundColor = '#004CBD';
2428 change_color(tr, '#FFFFFF');
2431 if (!tb.childNodes[row.i].isEqualNode(tr)) {
2432 // check for edit mode
2433 let inlineEdit = find_input_element(tb.childNodes[row.i]);
2435 if (tb.childNodes[row.i].innerHTML != tr.innerHTML)
2436 tb.childNodes[row.i].innerHTML = tr.innerHTML;
2442 // Remove and install mouse click handlers
2443 if (tb.childNodes[row.i].childNodes[2] !== undefined) {
2444 tb.childNodes[row.i].childNodes[2].removeEventListener('mousedown', select_array_element);
2445 tb.childNodes[row.i].childNodes[2].removeEventListener('mousemove', select_array_element);
2447 tb.childNodes[row.i].addEventListener('mousedown', select_key);
2449 tb.childNodes[row.i].addEventListener('mousemove', select_key);
2450 tb.childNodes[row.i].addEventListener('contextmenu', context_menu);
2453 // Remember ODB path in row
2454 tb.childNodes[row.i].odbPath = keyPath;
2455 tb.childNodes[row.i].key = key;
2457 // Remove and odbOptions class or invisibility
2458 tb.childNodes[row.i].className = "";
2459 tb.childNodes[row.i].style.display = 'table-row';
2465 td = document.createElement('TD');
2468 if (Array.isArray(key.value)) {
2471 '<div style="display: inline;"></div>';
2473 if (key.type === TID_BOOL) {
2475 '<div style="display: inline;">' +
2476 '<select title="Set array elements to same value" ' +
2477 'onchange="odb_setall_key(this, \'' + keyPath + '\', this.value);">' +
2478 '<option value=""></option>' +
2479 '<option value="0">No</option>' +
2480 '<option value="1">Yes</option>' +
2485 '<div style="display: inline;"><a href="#" ' +
2486 'onclick="inline_edit(event, this.parentNode, \'' +
2487 key.value[0] + '\', odb_setall, 10, \'' + keyPath +
2488 '\');return false;" ' +
2489 'title="Set array elements to same value">*</a></div>';
2492 if (key.expanded === false)
2494 '<div style="display: inline;float: right">' +
2496 'onclick="toggle_expanded(this.parentNode);return false;" ' +
2497 'title="Show array elements">\u25B8</a>' +
2501 '<div style="display: inline;float: right">' +
2503 'onclick="toggle_expanded(this.parentNode);return false;" ' +
2504 'title="Hide array elements">\u25BE</a>' +
2508 if (key.type === TID_BOOL) {
2510 let h = "<select onchange='odb_setoption(this)' " +
2511 " onfocus='option_edit(event, this, true)'" +
2512 " onblur='option_edit(event, this, false)'" +
2515 if (key.value === false) {
2516 h += "<option selected value='" + 0 + "'>No</option>";
2517 h += "<option value='" + 1 + "'>Yes</option>";
2519 h += "<option value='" + 0 + "'>No</option>";
2520 h += "<option selected value='" + 1 + "'>Yes</option>";
2525 } else if (options) {
2527 let oc = options.slice(); // make copy of all elements
2529 let h = "<select onchange='odb_setoption(this)' " +
2530 " onfocus='option_edit(event, this, true)'" +
2531 " onblur='option_edit(event, this, false)'" +
2534 // check if current value is in options, add it if not
2535 if (!oc.includes(key.value))
2536 oc.unshift(key.value);
2538 for (const o of oc) {
2539 if (key.value === o)
2540 h += "<option selected value='" + o + "'>" + o + "</option>";
2542 h += "<option value='" + o + "'>" + o + "</option>";
2549 let edit = '<a href="#" onclick="this.parentNode.dataset.format=\'\';' +
2550 'this.parentNode.refreshPage=odb_update;' +
2551 'ODBInlineEdit(this.parentNode, \'' + keyPath + '\');return false;" ' +
2552 'onfocus="ODBInlineEdit(this.parentNode, \'' + keyPath + '\')" title="Change value">';
2553 let editHex = '<a href="#" onclick="this.parentNode.dataset.format=\'x\';' +
2554 'this.parentNode.refreshPage=odb_update;' +
2555 'ODBInlineEdit(this.parentNode, \'' + keyPath + '\');return false;" ' +
2556 'onfocus="ODBInlineEdit(this.parentNode, \'' + keyPath + '\')" title="Change hex value">';
2558 let v = escapeHTML(key.value.toString());
2560 let hexValue = false;
2561 if (key.type === TID_STRING && v === "")
2563 else if (key.type === TID_STRING && v.trim() === "")
2565 else if (key.type === TID_LINK)
2566 v = '<span style="color:red;background-color:yellow">(cannot resolve link)</span>';
2567 else if (v.substring(0, 2) === "0x") {
2569 vHex = "0x" + v.substring(2).toUpperCase();
2570 if (key.type === TID_UINT64)
2571 v = BigInt(key.value).toString();
2573 v = parseInt(key.value);
2574 } else if (key.type !== TID_STRING && key.type !== TID_LINK && key.type !== TID_FLOAT && key.type !== TID_DOUBLE) {
2575 if (key.type === TID_INT8)
2576 vHex = "0x" + (key.value & 0xFF).toString(16).toUpperCase();
2577 else if (key.type === TID_INT16)
2578 vHex = "0x" + (key.value & 0xFFFF).toString(16).toUpperCase();
2579 else if (key.type === TID_INT32)
2580 vHex = "0x" + (key.value >>> 0).toString(16).toUpperCase();
2581 else if (key.type === TID_INT64)
2582 vHex = "0x" + bnToHex(BigInt(key.value));
2584 vHex = "0x" + key.value.toString(16).toUpperCase();
2586 if (odb.picker || odb.handleColumn)
2587 td.innerHTML = v + vHex;
2590 td.innerHTML = edit + v + '</a>';
2593 td.innerHTML = editHex + vHex + '</a> ' + ' (' + edit + v + '</a>)';
2595 td.innerHTML = edit + v + '</a> ' + ' (' + editHex + vHex + '</a>)';
2602 td = document.createElement('TD');
2603 td.setAttribute('name', 'odbExt');
2604 td.style.display = odb.detailsColumn ? 'table-cell' : 'none';
2606 td.appendChild(document.createTextNode(tid_name[key.type]));
2609 td = document.createElement('TD');
2610 td.setAttribute('name', 'odbExt');
2611 td.style.display = odb.detailsColumn ? 'table-cell' : 'none';
2613 td.innerHTML = '<a href="#" onclick="resize_array(event, \''+keyPath+'\','+key.num_values+');return false;">'
2614 + key.num_values + '</a>';
2617 td = document.createElement('TD');
2618 td.setAttribute('name', 'odbExt');
2619 td.style.display = odb.detailsColumn ? 'table-cell' : 'none';
2621 if (key.type === TID_STRING)
2622 td.innerHTML = '<a href="#" onclick="resize_string(event, \''+keyPath+'\','+key.item_size+','+key.num_values+');return false;">'
2623 + key.item_size + '</a>';
2625 td.innerHTML = key.item_size;
2628 td = document.createElement('TD');
2629 td.setAttribute('name', 'odbExt');
2630 td.style.display = odb.detailsColumn ? 'table-cell' : 'none';
2632 let s = Math.floor(0.5 + new Date().getTime() / 1000 - key.last_written);
2636 else if (s < 60 * 60)
2637 t = Math.floor(0.5 + s / 60) + 'm';
2638 else if (s < 60 * 60 * 24)
2639 t = Math.floor(0.5 + s / 60 / 60) + 'h';
2640 else if (s < 60 * 60 * 24 * 99)
2641 t = Math.floor(0.5 + s / 60 / 60 / 24) + 'd';
2644 td.appendChild(document.createTextNode(t));
2647 td = document.createElement('TD');
2648 td.setAttribute('name', 'odbExt');
2649 td.style.display = odb.detailsColumn ? 'table-cell' : 'none';
2651 let mode = key.access_mode;
2653 if (mode & MODE_READ)
2655 if (mode & MODE_WRITE)
2657 if (mode & MODE_DELETE)
2659 if (mode & MODE_EXCLUSIVE)
2661 if (mode & MODE_WATCH)
2663 td.appendChild(document.createTextNode(m));
2665 // invert color if selected
2666 if (tb.childNodes.length > row.i) {
2667 let selected = tb.childNodes[row.i].odbSelected;
2669 tr.style.backgroundColor = '#004CBD';
2670 change_color(tr, '#FFFFFF');
2674 if (row.i >= tb.childNodes.length) {
2675 // append new row if nothing exists
2678 // replace current row if it differs
2679 if (!tb.childNodes[row.i].isEqualNode(tr)) {
2680 if (tb.childNodes[row.i].childNodes[1].colSpan !== 1)
2681 tb.childNodes[row.i].childNodes[1].colSpan = "1";
2682 for (let i = 0; i < 8; i++) {
2683 let yellowBg = false;
2685 // check for data change in value column
2687 let oldElem = tb.childNodes[row.i].childNodes[i];
2688 if (oldElem === undefined)
2691 while (oldElem.childNodes[0] !== undefined &&
2692 (oldElem.tagName === 'TD' || oldElem.tagName === 'A' || oldElem.tagName === 'DIV'))
2693 oldElem = oldElem.childNodes[0]; // get into <td> <a> <div> elements
2694 oldElem = oldElem.parentNode.innerHTML;
2696 let newElem = tr.childNodes[i];
2697 if (newElem === undefined)
2700 while (newElem.childNodes[0] !== undefined &&
2701 (newElem.tagName === 'TD' || newElem.tagName === 'A' || newElem.tagName === 'DIV'))
2702 newElem = newElem.childNodes[0]; // get into <td> <a> <div> elements
2703 newElem = newElem.parentNode.innerHTML;
2706 if (oldElem !== newElem && // value changed
2707 !odb.skip_yellow && // skip if globally disabled
2708 // skip if edit just finished
2709 !(oldElem.indexOf('(') == -1 && newElem.indexOf('(') !== -1) &&
2710 // skip '*' of arrays
2711 newElem.indexOf('>*</a>') === -1)
2717 // check for edit mode
2718 let inlineEdit = find_input_element(tb.childNodes[row.i].childNodes[i]);
2720 if (tb.childNodes[row.i].childNodes[i] === undefined)
2721 tb.childNodes[row.i].appendChild(tr.childNodes[i].cloneNode(true));
2722 else if (!inlineEdit) {
2723 let e = tb.childNodes[row.i].childNodes[i];
2725 if (tb.childNodes[row.i].childNodes[i].innerHTML !== tr.childNodes[i].innerHTML)
2726 tb.childNodes[row.i].childNodes[i].innerHTML = tr.childNodes[i].innerHTML;
2729 e.style.backgroundColor = 'var(--myellow)';
2730 e.style.setProperty("-webkit-transition", "", "");
2731 e.style.setProperty("transition", "", "");
2732 e.age = new Date() / 1000;
2735 if (e.age !== undefined && new Date() / 1000 > e.age + 1) {
2736 e.style.setProperty("-webkit-transition", "background-color 1s", "");
2737 e.style.setProperty("transition", "background-color 1s", "");
2738 e.style.backgroundColor = "";
2745 // Install mouse click handler
2746 if (tb.childNodes[row.i].childNodes[2] !== undefined) {
2747 tb.childNodes[row.i].childNodes[2].removeEventListener('mousedown', select_array_element);
2748 tb.childNodes[row.i].childNodes[2].removeEventListener('mousemove', select_array_element);
2750 tb.childNodes[row.i].addEventListener('mousedown', select_key);
2752 tb.childNodes[row.i].addEventListener('mousemove', select_key);
2753 tb.childNodes[row.i].addEventListener('contextmenu', context_menu);
2756 // Remember ODB path and key in row
2757 tb.childNodes[row.i].odbPath = keyPath;
2758 tb.childNodes[row.i].key = key;
2760 // Copy visibility for odbOptions
2761 tb.childNodes[row.i].style.display = tr.style.display;
2763 // Print array values
2764 if (Array.isArray(key.value)) {
2765 if (key.expanded === false) {
2767 } else for (let i=0 ; i<key.value.length ; i++) {
2770 // return if in edit mode
2771 if (tb.childNodes[row.i] !== undefined &&
2772 tb.childNodes[row.i].childNodes[2] !== undefined &&
2773 tb.childNodes[row.i].childNodes[2].inEdit)
2777 let tr = document.createElement('TR');
2780 tr.odbPath = keyPath + '[' + i + ']';
2782 // hide option keys if not in detailed column mode
2783 if (key.options && !odb.detailsColumn)
2784 tr.style.display = 'none';
2786 // Handle column (empty for array values)
2787 let td = document.createElement('TD');
2788 td.setAttribute('name', 'odbHandle');
2789 td.style.display = odb.handleColumn ? 'table-cell' : 'none';
2790 td.style.width = "10px";
2794 td = document.createElement('TD');
2798 td = document.createElement('TD');
2800 let p = (path === '/' ? '/' + key.name : path + '/' + key.name);
2803 if (key.type === TID_BOOL) {
2805 let h = "<select onchange='odb_setoption(this," + i + ")' " +
2806 " onfocus='option_edit(event, this, true)'" +
2807 " onblur='option_edit(event, this, false)'" +
2810 if (key.value[i] === false) {
2811 h += "<option selected value='" + 0 + "'>No</option>";
2812 h += "<option value='" + 1 + "'>Yes</option>";
2814 h += "<option value='" + 0 + "'>No</option>";
2815 h += "<option selected value='" + 1 + "'>Yes</option>";
2818 td.innerHTML = "[" + i + "]" + h;
2820 } else if (options) {
2822 let oc = options.slice(); // make copy of all elements
2824 let h = "<select onchange='odb_setoption(this," + i + ")' " +
2825 " onfocus='option_edit(event, this, true)'" +
2826 " onblur='option_edit(event, this, false)'" +
2829 // check if current value is in options, add it if not
2830 if (!oc.includes(key.value[i]))
2831 oc.unshift(key.value[i]);
2833 for (const o of oc) {
2834 if (key.value[i] === o)
2835 h += "<option selected value='" + o + "'>" + o + "</option>";
2837 h += "<option value='" + o + "'>" + o + "</option>";
2843 let edit = '<a href="#" onclick="ODBInlineEdit(this.parentNode, \'' + p + '\');return false;" ' +
2844 'onfocus="ODBInlineEdit(this.parentNode, \'' + p + '\')" title="Change array element">';
2846 let v = escapeHTML(key.value[i].toString());
2847 if (key.type === TID_STRING && v === "")
2849 else if (key.type === TID_STRING && v.trim() === "")
2851 else if (key.type === TID_LINK)
2852 v = '<span style="color:red;background-color:yellow">(cannot resolve link)</span>';
2853 else if (v.substring(0, 2) === "0x") {
2854 v = "0x" + v.substring(2).toUpperCase();
2855 if (key.type === TID_UINT64)
2856 v += " (" + BigInt(key.value[i]).toString() + ')';
2858 v += " (" + parseInt(key.value[i]) + ')';
2859 } else if (key.type !== TID_STRING && key.type !== TID_LINK && key.type !== TID_FLOAT && key.type !== TID_DOUBLE) {
2860 if (key.type === TID_INT8)
2861 v += " (0x" + (key.value[i] & 0xFF).toString(16).toUpperCase() + ')';
2862 else if (key.type === TID_INT16)
2863 v += " (0x" + (key.value[i] & 0xFFFF).toString(16).toUpperCase() + ')';
2864 else if (key.type === TID_INT32)
2865 v += " (0x" + (key.value[i] & 0xFFFFFFFF).toString(16).toUpperCase() + ')';
2866 else if (key.type === TID_INT64)
2867 v += " (0x" + bnToHex(BigInt(key.value[i])) + ')';
2869 v += " (0x" + key.value[i].toString(16).toUpperCase() + ')';
2873 td.innerHTML = '[' + i + '] ' + v;
2874 else if (odb.handlecolumn)
2877 td.innerHTML = '[' + i + '] ' + edit + v + '</a>';
2881 for (let i=0 ; i<5 ; i++) {
2882 td = document.createElement('TD');
2883 td.setAttribute('name', 'odbExt');
2884 td.style.display = odb.detailsColumn ? 'table-cell' : 'none';
2888 if (row.i >= tb.childNodes.length) {
2891 if (!tb.childNodes[row.i].isEqualNode(tr)) {
2892 if (tr.childNodes.length === 1) { // Subdir
2893 tb.childNodes[row.i].replaceWith(tr);
2894 } else if (tr.childNodes.length === 8) { // Key
2895 let odbSelected = tb.childNodes[row.i].odbSelected;
2896 let odbLastSelected = tb.childNodes[row.i].odbLastSelected;
2897 if (tb.childNodes[row.i].childNodes[1].colSpan !== 1)
2898 tb.childNodes[row.i].childNodes[1].colSpan = "1";
2899 for (let i = 0; i < 8; i++) {
2900 let changed = false;
2905 let select = (tr.childNodes[2].childNodes[0] &&
2906 tr.childNodes[2].childNodes[0].tagName == 'SELECT');
2908 if (tb.childNodes[row.i].childNodes[2] !== undefined) {
2910 oldValue = tb.childNodes[row.i].childNodes[2].childNodes[0].value;
2911 else if (tb.childNodes[row.i].childNodes[2].childNodes[1] !== undefined)
2912 oldValue = tb.childNodes[row.i].childNodes[2].childNodes[1].innerHTML;
2913 else if (tb.childNodes[row.i].childNodes[2].childNodes[0] !== undefined)
2914 oldValue = tb.childNodes[row.i].childNodes[2].childNodes[0].innerHTML
2916 oldValue = tb.childNodes[row.i].childNodes[2].innerHTML;
2919 newValue = tr.childNodes[2].childNodes[0].value;
2921 if (tr.childNodes[2].childNodes[1] === undefined)
2922 newValue = tr.childNodes[2].childNodes[0].data;
2924 newValue = tr.childNodes[2].childNodes[1].innerHTML;
2928 if (oldValue !== undefined &&
2929 oldValue !== newValue && // value changed
2930 i === 2 && // we are in value column
2931 !odb.skip_yellow && // skip if globally disabled
2932 // skip if edit just finished
2933 !(oldValue.indexOf('(') == -1 && newValue.indexOf('(') !== -1) &&
2934 // skip '*' of arrays
2935 newValue.indexOf('>*</a>') === -1 &&
2936 tb.childNodes[row.i].odbSelected !== true)
2939 if (tb.childNodes[row.i].childNodes[i] === undefined)
2940 tb.childNodes[row.i].appendChild(tr.childNodes[i].cloneNode(true));
2942 // preserve color if key is selected
2943 let c = tb.childNodes[row.i].childNodes[i].style.color;
2944 tb.childNodes[row.i].childNodes[i].innerHTML = tr.childNodes[i].innerHTML;
2945 change_color(tb.childNodes[row.i].childNodes[i], c);
2948 let e = tb.childNodes[row.i].childNodes[i];
2950 e.style.backgroundColor = 'var(--myellow)';
2951 e.style.setProperty("-webkit-transition", "", "");
2952 e.style.setProperty("transition", "", "");
2953 e.age = new Date() / 1000;
2956 if (e.age !== undefined && new Date() / 1000 > e.age + 1) {
2957 e.style.setProperty("-webkit-transition", "background-color 1s", "");
2958 e.style.setProperty("transition", "background-color 1s", "");
2959 e.style.backgroundColor = "";
2961 if (e.age !== undefined && new Date() / 1000 > e.age + 2) {
2962 e.style.cssText = "";
2963 e.removeAttribute("style");
2968 tb.childNodes[row.i].odbSelected = odbSelected;
2969 tb.childNodes[row.i].odbLastSelected = odbLastSelected;
2974 // remove/install mouse click handlers
2975 tb.childNodes[row.i].removeEventListener('mousedown', select_key);
2977 tb.childNodes[row.i].removeEventListener('mousemove', select_key);
2978 tb.childNodes[row.i].removeEventListener('contextmenu', context_menu);
2981 tb.childNodes[row.i].childNodes[2].addEventListener('mousedown', select_array_element);
2982 tb.childNodes[row.i].childNodes[2].addEventListener('mousemove', select_array_element);
2984 // catch all mouseup events to stop dragging
2985 document.addEventListener('mouseup', mouseStopDragging);
2986 tb.childNodes[row.i].odbPath = tr.odbPath;
2987 tb.childNodes[row.i].key = undefined;
2989 tb.childNodes[row.i].style.display = tr.style.display;
2995function resize_array(event, path, n) {
2996 event.stopPropagation(); // do not select row
2997 dlgQuery("Enter size of array:", n, do_resize_array, path);
3000function do_resize_array(n, path) {
3001 mjsonrpc_db_resize([path],[parseInt(n)]).then(rpc => {
3002 }).catch(error => mjsonrpc_error_alert(error));
3005function resize_string(event, path, size, num_values) {
3006 event.stopPropagation(); // do not select row
3007 dlgQuery("Enter new string size:", size, do_resize_string, { "path": path, "num_values": num_values });
3010function do_resize_string(size, p) {
3011 mjsonrpc_call("db_resize_string",
3012 { "paths": [p.path],
3013 "new_lengths": [parseInt(p.num_values)],
3014 "new_string_lengths": [parseInt(size)]}).then(rpc => {
3015 }).catch(error => mjsonrpc_error_alert(error));
3018function dlgCreateKeyDown(event, inp) {
3019 let keyCode = ('which' in event) ? event.which : event.keyCode;
3021 if (keyCode === 27) {
3023 dlgHide('dlgCreate');
3027 if (keyCode === 13) {
3029 if (do_new_key(event.target))
3030 dlgHide('dlgCreate');
3037function dlgCreateLinkKeyDown(event, inp) {
3038 let keyCode = ('which' in event) ? event.which : event.keyCode;
3040 if (keyCode === 27) {
3042 dlgHide('dlgCreateLink');
3046 if (keyCode === 13) {
3048 if (do_new_link(this))
3049 dlgHide('dlgCreateLink');
3056function drag_start(event) {
3057 let tr = event.target;
3058 while (tr.tagName !== 'TR')
3060 let tb = getOdbTb(tr);
3062 tb.dragTargetRow = Array.from(tb.children).indexOf(tr);
3063 tb.dragSourceRow = tb.dragTargetRow;
3064 tb.dragSource = tr.odbPath;
3065 tb.dragRowContent = tr.cloneNode(true);
3067 window.setTimeout(() => {
3068 window.clearTimeout(tb.odb.updateTimer);
3069 let w = window.getComputedStyle(tr).width;
3070 let h = window.getComputedStyle(tr).height;
3071 tr.innerHTML = '<td colspan="8" style="background-color:white; border: 2px dashed #6bb28c"></td>';
3072 tr.style.height = parseInt(h) + "px";
3073 tr.style.width = parseInt(w) + "px";
3078function drag_move(event, td) {
3079 event.preventDefault();
3081 let tr = event.target;
3082 while (tr.tagName !== 'TR')
3084 let tb = getOdbTb(tr);
3085 let children = Array.from(tb.children);
3087 if (children.indexOf(tr) > tb.dragTargetRow)
3088 tr.after(tb.childNodes[tb.dragTargetRow]);
3089 else if (children.indexOf(tr) < tb.dragTargetRow)
3090 tr.before(tb.childNodes[tb.dragTargetRow]);
3092 tb.dragTargetRow = children.indexOf(tr);
3095function drag_end(event, td) {
3096 let tr = event.target;
3097 while (tr.tagName !== 'TR')
3099 let tb = getOdbTb(tr);
3101 let ttr = tb.childNodes[tb.dragTargetRow];
3102 ttr.innerHTML = tb.dragRowContent.innerHTML;
3103 ttr.style.height = "";
3104 ttr.style.width = "";
3106 if (tb.dragSourceRow !== tb.dragTargetRow) {
3107 mjsonrpc_call("db_reorder", {"paths": [tb.dragSource], "indices": [tb.dragTargetRow - 4]}).then(rpc => {
3108 tb.odb.updateTimer = window.setTimeout(odb_update, 10, tb);
3109 }).catch(error => mjsonrpc_error_alert(error));
3111 tb.odb.updateTimer = window.setTimeout(odb_update, 10, tb);
3114function show_open_records(odb) {
3115 let title = "ODB open records under \"" + odb.path + "\"";
3117 let d = document.createElement("div");
3118 d.className = "dlgFrame";
3119 d.style.zIndex = "30";
3120 d.style.minWidth = "400px";
3121 d.shouldDestroy = true;
3123 d.innerHTML = "<div class=\"dlgTitlebar\" id=\"dlgMessageTitle\">" + title + "</div>" +
3124 "<div class=\"dlgPanel\" style=\"padding: 2px;\">" +
3125 "<div id=\"dlgSOR\"></div>" +
3126 "<button class=\"dlgButton\" id=\"dlgMessageButton\" +" +
3127 "type=\"button\" " +
3128 " onClick=\"dlgMessageDestroy(this)\">Close</button>" +
3131 document.body.appendChild(d);
3133 update_open_records(odb);
3137function update_open_records(odb) {
3138 let path = odb.path;
3139 mjsonrpc_call("db_sor", {"path": path}).then (rpc => {
3140 let sor = rpc.result.sor;
3142 for (const s of sor) {
3144 paths[s.path] += ', ' + s.name;
3146 paths[s.path] = s.name;
3148 let sorted_paths = Object.keys(paths).sort();
3149 let html = '<table class="mtable" style="width: 100%;margin-top: 0;margin-bottom:0"><tbody>';
3150 html += '<tr><th>ODB Path</th><th>Open by</th></tr>';
3151 for (const p of sorted_paths)
3152 html += '<tr><td style="padding: 4px">' + p + '</td>' +
3153 '<td style="padding: 4px">' + paths[p] + '</td></tr>';
3154 html += '</tbody></table>';
3156 let d = document.getElementById('dlgSOR');
3158 return; // dialog has been closed
3159 if (d.innerHTML !== html) {
3161 dlgCenter(d.parentElement.parentElement);
3163 window.setTimeout(update_open_records, 1000, odb);
3165 }).catch( error => mjsonrpc_error_alert(error));
3168function show_open_clients(e) {
3169 let odb = getOdbTb(e).odb;
3170 let title = "ODB clients";
3172 let d = document.createElement("div");
3173 d.className = "dlgFrame";
3174 d.style.zIndex = "30";
3175 d.style.minWidth = "400px";
3176 d.shouldDestroy = true;
3178 d.innerHTML = "<div class=\"dlgTitlebar\" id=\"dlgMessageTitle\">" + title + "</div>" +
3179 "<div class=\"dlgPanel\" style=\"padding: 2px;\">" +
3180 "<div id=\"dlgSCL\"></div>" +
3181 "<button class=\"dlgButton\" id=\"dlgMessageButton\" +" +
3182 "type=\"button\" " +
3183 " onClick=\"dlgMessageDestroy(this)\">Close</button>" +
3186 document.body.appendChild(d);
3188 update_open_clients(odb);
3192function update_open_clients(odb) {
3193 let path = odb.path;
3194 mjsonrpc_call("db_scl").then (rpc => {
3195 let scl = rpc.result.scl.clients;
3197 let html = '<table class="mtable" style="width: 100%;margin-top: 0;margin-bottom:0"><tbody>';
3198 html += '<tr><th>Name</th><th>Host</th><th>Slot</th><th>PID</th><th>Timeout</th><th>Last active [ms]</th></tr>';
3199 for (const s of scl) {
3200 html += '<tr><td style="padding: 4px"><a href="?cmd=odb&odb_path=/System/Clients/"'
3201 + s.pid + '">' + s.name + '</td>';
3202 html += '<td style="padding: 4px">'+ s.host + '</td>';
3203 html += '<td style="padding: 4px">'+ s.slot + '</td>';
3204 html += '<td style="padding: 4px">'+ s.pid + '</td>';
3205 html += '<td style="padding: 4px">'+ s.watchdog_timeout_millisec + '</td>';
3206 html += '<td style="padding: 4px">'+ s.last_activity_millisec + '</td>';
3209 html += '</tbody></table>';
3211 let d = document.getElementById('dlgSCL');
3213 return; // dialog has been closed
3214 if (d.innerHTML !== html) {
3216 dlgCenter(d.parentElement.parentElement);
3218 window.setTimeout(update_open_clients, 1000, odb);
3220 }).catch( error => mjsonrpc_error_alert(error));