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 let m = document.getElementById('moreMenu');
941 if (m !== null && m.style.display === 'block')
942 m.style.display = 'none';
944 // hide context menu if visible
945 m = document.getElementById('contextMenu');
946 if (m !== null && m.style.display === 'block')
947 m.style.display = 'none';
949 // un-select all array elements
950 unselect_all_array_elements(tb);
952 // don't select key when we are in edit mode
953 while (tr.tagName !== 'TR') {
958 if (find_input_element(tr))
963 for (let i = 4; i < tb.childNodes.length; i++)
964 tb.childNodes[i].odbSelected = false;
966 // don't select array values
967 if (tr.childNodes[1] && tr.childNodes[1].innerHTML !== "")
968 tr.odbSelected = true;
970 odb.selectedKey = tr.odbPath;
972 // check if click is on header row, if so remove selection further down
973 let headerRow = false;
974 for (let i = 0; i < 4; i++)
975 if (tb.childNodes[i] === tr) {
980 if (event.type === "mousemove") {
982 if (event.buttons === 1 && !headerRow) {
983 // search last selected row
985 for (i1 = 4; i1 < tb.childNodes.length; i1++)
986 if (tb.childNodes[i1].odbLastSelected)
988 if (i1 === tb.childNodes.length)
989 i1 = 4; // none selected, so use first one
992 for (i2 = 4; i2 < tb.childNodes.length; i2++)
993 if (tb.childNodes[i2] === tr)
996 if (i2 < tb.childNodes.length) {
1000 for (let i = i1; i <= i2; i++) {
1001 // don't select arrays
1002 if (tb.childNodes[i].childNodes[1].innerHTML !== "")
1003 tb.childNodes[i].odbSelected = true;
1008 } else { // mousedown
1010 if (event.shiftKey && !headerRow) {
1011 // search last selected row
1013 for (i1 = 4; i1 < tb.childNodes.length; i1++)
1014 if (tb.childNodes[i1].odbLastSelected)
1016 if (i1 === tb.childNodes.length)
1017 i1 = 4; // none selected, so use first one
1020 for (i2 = 4; i2 < tb.childNodes.length; i2++)
1021 if (tb.childNodes[i2] === tr)
1024 if (i2 < tb.childNodes.length) {
1026 [i1, i2] = [i2, i1];
1028 for (let i = i1; i <= i2; i++) {
1029 // don't select arrays
1030 if (tb.childNodes[i].childNodes[1].innerHTML !== "")
1031 tb.childNodes[i].odbSelected = true;
1035 } else if ((event.metaKey || event.ctrlKey) && !headerRow) {
1037 // command key just toggles current selection
1038 tr.odbSelected = !tr.odbSelected;
1040 for (let i = 4; i < tb.childNodes.length; i++)
1041 tb.childNodes[i].odbLastSelected = false;
1043 tr.odbLastSelected = true;
1047 // no key pressed -> un-select all but current
1048 for (let i = 4; i < tb.childNodes.length; i++) {
1049 tb.childNodes[i].odbSelected = false;
1050 tb.childNodes[i].odbLastSelected = false;
1053 // don't select header row and array values
1054 if (!headerRow && tr.childNodes[1].innerHTML !== "") {
1055 tr.odbSelected = true;
1056 tr.odbLastSelected = true;
1057 tr.odbSelectTime = new Date().getTime();
1063 // change color of all rows according to selection
1064 for (let i=4 ; i<tb.childNodes.length ; i++) {
1065 let tr = tb.childNodes[i];
1067 tr.style.backgroundColor = tr.odbSelected ? '#004CBD' : '';
1068 change_color(tr, tr.odbSelected ? '#FFFFFF' : '');
1072function getArrayTr(tr) {
1073 // collect all rows belonging to "tr" in an array
1074 let tb = getOdbTb(tr);
1077 for (i = 0; i < tb.childNodes.length; i++)
1078 if (tb.childNodes[i] === tr)
1080 while (tb.childNodes[i].odbPath.indexOf('[') !== -1)
1082 i++; // first element
1084 atr.push(tb.childNodes[i++]);
1085 } while (i < tb.childNodes.length && tb.childNodes[i].odbPath.indexOf('[') !== undefined);
1089let mouseDragged = false;
1091function mouseStopDragging() {
1092 mouseDragged = false;
1095function select_array_element(event) {
1097 if (event.type === 'mousemove' && !mouseDragged)
1100 // keep off secondary mouse buttons
1101 if (event.button !== 0)
1104 // keep off drop-down boxes
1105 if (event.target.tagName === "SELECT")
1108 event.preventDefault();
1109 event.stopPropagation();
1111 let tr = event.target;
1112 let tb = getOdbTb(tr);
1113 if (tb === undefined)
1116 // don't select key when we are in edit mode
1117 while (tr.tagName !== 'TR') {
1122 if (find_input_element(tr))
1127 // hide submenu if visible
1128 let m = document.getElementById('moreMenu');
1129 if (m !== null && m.style.display === 'block')
1130 m.style.display = 'none';
1132 // hide context menu if visible
1133 m = document.getElementById('contextMenu');
1134 if (m !== null && m.style.display === 'block')
1135 m.style.display = 'none';
1137 // remove selection from all non-array keys
1138 for (let i=0 ; i<tb.childNodes.length ; i++) {
1139 if (tb.childNodes[i].childNodes.length > 1 &&
1140 tb.childNodes[i].childNodes[1].innerHTML !== "" &&
1141 tb.childNodes[i].odbSelected) {
1142 tb.childNodes[i].odbSelected = false;
1143 tb.childNodes[i].style.backgroundColor = '';
1144 change_color(tb.childNodes[i], '');
1148 // create array of all array elements
1149 let atr = getArrayTr(tr);
1151 if (event.type === 'mousedown') {
1152 mouseDragged = true;
1155 // un-select all but current
1156 for (const tr of atr) {
1157 tr.odbSelected = false;
1158 tr.odbLastSelected = false;
1160 tr.odbSelected = true;
1161 tr.odbLastSelected = true;
1162 odb.selectedKey = tr.odbPath;
1166 if (event.shiftKey) {
1167 // search last selected row
1169 for (i1 = 0; i1 < atr.length; i1++)
1170 if (atr[i1].odbLastSelected)
1172 if (i1 === atr.length)
1173 i1 = 0; // non selected, so use first one
1176 for (i2 = 0; i2 < atr.length; i2++)
1180 if (i2 < atr.length) {
1182 [i1, i2] = [i2, i1];
1184 for (let i = i1; i <= i2; i++)
1185 atr[i].odbSelected = true;
1188 } else if (event.metaKey || event.ctrlKey) {
1190 // command key just toggles current selection
1191 tr.odbSelected = !tr.odbSelected;
1193 for (const tr of atr)
1194 tr.odbLastSelected = false;
1196 tr.odbLastSelected = true;
1200 // no key pressed -> un-select all but current
1201 for (const tr of atr) {
1202 tr.odbSelected = false;
1203 tr.odbLastSelected = false;
1205 tr.odbSelected = true;
1206 tr.odbLastSelected = true;
1211 if (event.type === 'mousemove' && mouseDragged) {
1213 if (!event.shiftKey && !event.metaKey && !event.ctrlKey) {
1215 for (const tr of atr)
1216 tr.odbSelected = false;
1218 // search last selected row
1219 for (i1 = 0; i1 < atr.length; i1++)
1220 if (atr[i1].odbLastSelected)
1222 if (i1 === atr.length)
1223 i1 = 0; // non selected, so use first one
1226 for (i2 = 0; i2 < atr.length; i2++)
1230 if (i2 < atr.length) {
1232 [i1, i2] = [i2, i1];
1234 for (let i = i1; i <= i2; i++)
1235 atr[i].odbSelected = true;
1240 // change color of all rows according to selection
1241 for (const e of atr) {
1242 if (e.childNodes.length >= 3) {
1243 e.childNodes[2].style.backgroundColor = e.odbSelected ? '#004CBD' : '';
1244 change_color(e.childNodes[2], e.odbSelected ? '#FFFFFF' : '');
1249function context_menu(event) {
1250 // don't show context menu if we are in input box (user wants traditional copy/paste)
1251 if (event.target.tagName === 'INPUT')
1254 event.preventDefault();
1255 event.stopPropagation();
1257 let tr = event.target;
1258 while (tr.tagName !== 'TR')
1260 let tb = getOdbTb(tr);
1262 let path = tr.odbPath;
1264 if (!tr.odbSelected)
1267 let d = document.getElementById('contextMenu');
1269 // create menu on the first call
1271 d = document.createElement("div");
1272 d.id = "contextMenu";
1273 d.style.display = "none";
1274 d.style.position = "absolute";
1275 d.className = "msidenav";
1276 d.style.borderRadius = "0";
1277 d.style.border = "2px solid #808080";
1278 d.style.margin = "0";
1279 d.style.backgroundColor = "#F0F0F0";
1281 let cm = document.createElement("div");
1283 let menu = ["Copy key", "Copy plain text", "Delete key", "Rename key...",
1284 "Open in new tab...", "Open in new window..."];
1287 for (const m of menu) {
1288 mDiv = document.createElement("div");
1289 mDiv.className = 'mmenuitem mmenulink';
1290 mDiv.innerHTML = '<nobr>' + m + '</nobr>';
1292 cm.appendChild(mDiv);
1296 document.body.appendChild(d);
1299 // set event handler for copy menu
1300 d.childNodes[0].childNodes[0].onmousedown = function () {
1301 d.style.display = 'none';
1305 // set event handler for copy plain text menu
1306 d.childNodes[0].childNodes[1].onmousedown = function () {
1307 d.style.display = 'none';
1308 let selKeys = get_selected_keys(tb);
1310 for (const k of selKeys)
1313 mjsonrpc_db_copy(paths).then(rpc => {
1315 let needPath = false;
1316 for (let i=0 ; i<rpc.result.data.length ; i++) {
1317 if (selKeys[i].key.type === TID_KEY) {
1318 text += odbASCII(rpc.result.data[i], paths[i]);
1322 text += odbASCII(rpc.result.data[i], tb.odb.path);
1324 text += odbASCII(rpc.result.data[i]);
1328 if (navigator.clipboard && navigator.clipboard.writeText) {
1330 navigator.clipboard.writeText(text).then( () => {
1331 if (paths.length === 1)
1332 dlgAlert("ODB key \"" + paths + "\" copied to clipboard as plain text");
1334 dlgAlert(paths.length + " ODB keys copied to clipboard as plain text");
1335 }).catch(e => dlgAlert(e));
1340 }).catch(error => mjsonrpc_error_alert(error));
1343 // set event handler for delete key menu
1344 d.childNodes[0].childNodes[2].onmousedown = function () {
1345 d.style.display = 'none';
1349 // set event handler for rename menu
1350 d.childNodes[0].childNodes[3].onmousedown = function () {
1351 d.style.display = 'none';
1354 if (tr.key.type === TID_KEY)
1355 elem = tr.childNodes[1].childNodes[2];
1357 elem = tr.childNodes[1].childNodes[1];
1359 let key = elem.innerHTML.trim();
1370 // set event handler for open in new tab
1371 d.childNodes[0].childNodes[4].onmousedown = function () {
1372 d.style.display = 'none';
1374 let url = window.location.href;
1375 if (url.search("&odb_path") !== -1)
1376 url = url.slice(0, url.search("&odb_path"));
1377 url += "&odb_path=" + encodeURIComponent(tr.odbPath); // convert spaces to %20 etc
1379 window.open(url, '_blank').focus();
1382 // set event handler for open in new window
1383 d.childNodes[0].childNodes[5].onmousedown = function () {
1384 d.style.display = 'none';
1386 let url = window.location.href;
1387 if (url.search("&odb_path") !== -1)
1388 url = url.slice(0, url.search("&odb_path"));
1389 url += "&odb_path=" + encodeURIComponent(tr.odbPath); // convert spaces to %20 etc
1391 window.open(url, "", "menubar=yes,location=yes,status=yes");
1394 let rect = event.target.getBoundingClientRect();
1396 d.style.display = 'block';
1397 d.style.left = (event.offsetX + rect.left + window.scrollX) + 'px';
1398 d.style.top = (event.offsetY+rect.top + window.scrollY) + 'px';
1400 if (parseInt(d.style.left) + d.offsetWidth > document.body.clientWidth)
1401 d.style.left = (document.body.clientWidth - d.offsetWidth) + 'px';
1407 let tb = getOdbTb(e);
1410 let rarr = document.getElementById('expRight');
1411 let cells = document.getElementsByName('odbExt');
1412 let table = document.getElementById('odbTable');
1413 odb.detailsColumn = !odb.detailsColumn;
1415 if (odb.detailsColumn) {
1416 for (const d of cells)
1417 d.style.display = 'table-cell';
1418 rarr.style.display = 'none';
1419 table.style.minWidth = '800px';
1421 for (const d of cells)
1422 d.style.display = 'none';
1423 rarr.style.display = 'inline';
1424 table.style.minWidth = '600px';
1426 odb.skip_yellow = true;
1430function clear_expanded(odb) {
1431 for (const key of odb.key) {
1432 if (key.type === TID_KEY)
1433 key.subdir_open = false;
1435 key.expanded = false;
1439function toggle_handles(e) {
1440 let tb = getOdbTb(e);
1443 // clear all expanded flags
1444 clear_expanded(odb);
1447 let n = document.getElementsByName('odbHandle');
1448 odb.handleColumn = !odb.handleColumn;
1450 if (odb.handleColumn) {
1451 odb.subdirFirst = false;
1452 for (const d of n) {
1453 let tr = d.parentNode;
1454 if (tr.firstChild.className !== 'colHeader') {
1455 tr.setAttribute('draggable', true);
1456 tr.addEventListener('dragstart', drag_start);
1457 tr.addEventListener('dragover', drag_move);
1458 tr.addEventListener('dragend', drag_end);
1461 d.style.display = 'table-cell';
1464 odb.subdirFirst = true;
1465 for (const d of n) {
1466 let tr = d.parentNode;
1467 if (tr.firstChild.className !== 'colHeader') {
1468 tr.removeAttribute('draggable');
1469 tr.removeEventListener('dragstart', drag_start);
1470 tr.removeEventListener('dragover', drag_move);
1471 tr.removeEventListener('dragend', drag_end);
1474 d.style.display = 'none';
1481function toggle_expanded(e) {
1483 while (tr.tagName !== 'TR')
1486 if (tr.key.num_values >= 5000 && !tr.key.expanded) {
1487 dlgConfirm("ODB key \""+ tr.key.name + "\" has " + tr.key.num_values +
1488 " array elements. This can take very long. Do you want to continue?", do_toggle_expanded, tr);
1490 do_toggle_expanded(true, tr);
1493function do_toggle_expanded(flag, tr) {
1495 let tb = getOdbTb(tr);
1496 tr.key.expanded = !tr.key.expanded;
1497 unselect_all_keys(tb);
1498 unselect_all_array_elements(tb);
1503function odb_delete(e) {
1505 let tb = getOdbTb(e);
1506 let currentPath = tb.odb.path;
1508 for (const tr of tb.childNodes) {
1510 paths.push(tr.odbPath);
1513 if (paths.length == 0) {
1514 // strip last directory from current path
1515 let newPath = currentPath;
1516 if (newPath.lastIndexOf('/') !== -1)
1517 newPath = newPath.substring(0, newPath.lastIndexOf('/'));
1521 dlgConfirm('Are you sure to delete odb directory \"' + currentPath + '\" ?',
1522 do_odb_delete, {"e": e, "paths": [currentPath], "newPath": newPath} );
1523 } else if (paths.length == 1)
1524 dlgConfirm('Are you sure to delete odb key \"' + paths[0] + '\" ?',
1525 do_odb_delete, {"e": e, "paths": paths});
1527 dlgConfirm('Are you sure to delete ' + paths.length + ' keys ?',
1528 do_odb_delete, {"e": e, "paths": paths});
1531function do_odb_delete(flag, param) {
1533 mjsonrpc_db_delete(param.paths).then((rpc) => {
1535 if (rpc.result.status[0] === 318) {
1536 dlgAlert("ODB key is write protected and cannot be deleted");
1537 } else if (rpc.result.status[0] === 320) {
1538 dlgAlert("ODB key is locked by other program and cannot be deleted");
1539 } else if (rpc.result.status[0] !== 1) {
1540 dlgAlert("ODB error: " + rpc.result.status[0]);
1544 subdir_goto(param.e, param.newPath);
1546 let tb = getOdbTb(param.e);
1550 }).catch(error => mjsonrpc_error_alert(error));
1554function rename_key(element) {
1555 let tb = getOdbTb(element);
1558 for (const t of tb.childNodes) {
1559 if (t.odbSelected) {
1566 dlgAlert("Please select key to be renamed");
1568 dlgAlert("Please select only single key to be renamed");
1570 tr.odbSelected = false;
1571 tr.style.backgroundColor = '';
1572 change_color(tr, '');
1576 if (tr.key.type === TID_KEY)
1577 elem = tr.childNodes[1].childNodes[2];
1579 elem = tr.childNodes[1].childNodes[1];
1581 let key = elem.innerHTML;
1584 inline_edit(undefined,
1593function do_rename_key(p, str, path) {
1595 dlgAlert("Empty name not allowed");
1599 mjsonrpc_call("db_rename", { "paths": [path], "new_names": [str]})
1601 .catch(error => mjsonrpc_error_alert(error));
1603 let old = p.parentElement.parentElement.oldKey;
1605 p.innerHTML = p.innerHTML.replace(old, str);
1608function search_key() {
1609 dlgQuery("Enter key name (substring case insensitive):", "", do_search_key);
1612function do_search_key(str) {
1614 window.location.href = "?cmd=Find&value=" + str;
1617function odb_copy(e) {
1618 let tb = getOdbTb(e);
1619 let selKeys = get_selected_keys(tb);
1621 let dirFlag = false;
1622 if (selKeys.length === 0) {
1623 paths.push(tb.odb.path);
1626 for (const k of selKeys)
1629 mjsonrpc_db_copy(paths).then(rpc => {
1632 // generate text from objects, add top directoy which is missing from db_copy
1633 for (const [i,d] of rpc.result.data.entries()) {
1636 let dir = tb.odb.path.substring(tb.odb.path.lastIndexOf('/')+1);
1639 text += '{ \"'+ dir +
1640 '\": '+JSON.stringify(d, null, ' ')+'}';
1641 } else if (selKeys[i].key.type === TID_KEY) {
1642 text += '{ \"'+ selKeys[i].key.name +'\": '+JSON.stringify(d, null, ' ')+'}';
1644 text += JSON.stringify(d, null, ' ');
1646 text += JSON.stringify(d, null, ' ').substring(1); // strip first '{'
1647 if (i < rpc.result.data.length - 1)
1648 text = text.substring(0, text.lastIndexOf('}')) + ','; // replace last '}' by ','
1651 // put text to clipboard if possible
1652 if (navigator.clipboard && navigator.clipboard.writeText) {
1654 navigator.clipboard.writeText(text).then( () => {
1656 dlgAlert("ODB directory \"" + paths + "\" copied to clipboard");
1658 if (paths.length === 1)
1659 dlgAlert("ODB key \"" + paths + "\" copied to clipboard");
1661 dlgAlert("ODB keys \"" + paths + "\" copied to clipboard");
1663 }).catch(e => dlgAlert(e));
1669 // store text locally as a backup
1671 sessionStorage.setItem('odbclip', text);
1676 }).catch(error => mjsonrpc_error_alert(error));
1679function odb_paste(e) {
1680 let tb = getOdbTb(e);
1683 let text = sessionStorage.getItem('odbclip');
1685 let keys = JSON.parse(text);
1686 odb_paste_keys(tb, keys);
1690 dlgAlert("Invalid JSON format in clipboard:<br /><br />" + error);
1694 if (navigator.clipboard && navigator.clipboard.readText) {
1695 navigator.clipboard.readText().then(text => {
1698 let keys = JSON.parse(text);
1699 odb_paste_keys(tb, keys);
1702 dlgAlert("Nothing stored in clipboard");
1705 dlgAlert("Invalid JSON format in clipboard:<br /><br />" + error);
1709 dlgAlert("Paste not possible in this browser");
1712function odb_paste_keys(tb, newKeys) {
1715 for (const key in newKeys) {
1716 if (key.indexOf('/') !== -1)
1719 // check for existing key in current keys
1720 let cKeyMaxIndex = 0;
1725 for (const cKey of odb.key) {
1726 if (cKey.name.indexOf(' copy') === -1)
1729 ckb = cKey.name.substring(0, cKey.name.indexOf(' copy'));
1734 if (cKey.name.indexOf(' copy') !== -1) {
1735 cKeyIndex = parseInt(cKey.name.substring(cKey.name.indexOf(' copy')+5));
1736 if (isNaN(cKeyIndex))
1738 if (cKeyIndex > cKeyMaxIndex)
1739 cKeyMaxIndex = cKeyIndex;
1744 // rename new key if name exists
1746 let newName = cKeyBare + ' copy';
1748 newName += ' ' + (cKeyIndex+1).toString();
1750 newKeys[newName] = newKeys[key];
1751 delete newKeys[key];
1753 newKeys[newName + '/key'] = newKeys[key + '/key'];
1754 delete newKeys[key + '/key'];
1758 mjsonrpc_db_paste([odb.path], [newKeys]).then(rpc =>
1759 odb_update(tb)).catch(error =>
1764function odbASCII(o, path) {
1765 // convert ODB keys to plain ASCII representation
1767 let need_path = true;
1768 if (path === undefined)
1770 for (const key in o) {
1771 if (key.indexOf('/') !== -1)
1774 if (o[key+'/key'] === undefined && path) {
1775 t += odbASCII(o[key], path + '/' + key);
1782 t += '\n[' + path + ']\n';
1785 let tid = o[key+'/key'].type;
1786 let num_values = o[key+'/key'].num_values;
1789 if (num_values > 1) {
1791 for (let i=0 ; i<num_values ; i++) {
1792 t += '[' + i + ']\t';
1793 t += mie_to_string(tid, o[key][i]);
1798 t += mie_to_string(tid, o[key]);
1805function odb_export(e) {
1806 let tb = getOdbTb(e);
1807 mjsonrpc_db_copy([tb.odb.path]).then(rpc => {
1809 let dirs = tb.odb.path.split('/');
1810 let filename = dirs[dirs.length - 1];
1811 if (filename === '')
1813 filename += ".json";
1816 "/MIDAS version": "2.1",
1817 "/filename": filename,
1818 "/ODB path": tb.odb.path
1820 header = JSON.stringify(header, null, ' ');
1821 header = header.substring(0, header.length-2) + ','; // strip trailing '}'
1823 let odbJson = JSON.stringify(rpc.result.data[0], null, ' ');
1824 if (odbJson.indexOf('{') === 0)
1825 odbJson = odbJson.substring(1); // strip leading '{'
1827 odbJson = header + odbJson;
1829 // use trick from FileSaver.js
1830 let a = document.getElementById('downloadHook');
1832 a = document.createElement("a");
1833 a.style.display = "none";
1834 a.id = "downloadHook";
1835 document.body.appendChild(a);
1838 let blob = new Blob([odbJson], {type: "text/json"});
1839 let url = window.URL.createObjectURL(blob);
1842 a.download = filename;
1844 window.URL.revokeObjectURL(url);
1845 dlgAlert("ODB subtree \"" + tb.odb.path +
1846 "\" downloaded to file \"" + filename + "\"");
1848 }).catch(error => mjsonrpc_error_alert(error));
1852function odb_save_picker(e) {
1853 let tb = getOdbTb(e);
1854 _odb_path = tb.odb.path;
1855 file_picker("", "*.json", odb_save, true);
1857function odb_save(filename) {
1858 mjsonrpc_db_copy([_odb_path]).then(rpc => {
1860 if (filename.indexOf('.json') === -1)
1861 filename += '.json';
1864 "/MIDAS version": "2.1",
1865 "/filename": filename,
1866 "/ODB path": _odb_path
1868 header = JSON.stringify(header, null, ' ');
1869 header = header.substring(0, header.length-2) + ','; // strip trailing '}'
1871 let odbJson = JSON.stringify(rpc.result.data[0], null, ' ');
1872 if (odbJson.indexOf('{') === 0)
1873 odbJson = odbJson.substring(1); // strip leading '{'
1875 odbJson = header + odbJson;
1877 file_save_ascii(filename, odbJson, "ODB subtree \"" + _odb_path +
1878 "\" saved to file \"" + filename + "\"");
1880 }).catch(error => mjsonrpc_error_alert(error));
1883async function odb_import(e) {
1885 // Check if showOpenFilePicker is available in the browser
1886 // The "else" is more of a JS standard
1887 if (window.showOpenFilePicker) {
1889 [fileHandle] = await window.showOpenFilePicker();
1890 const file = await fileHandle.getFile();
1891 const text = await file.text();
1892 pasteBuffer(e, text, file.name);
1894 // Show a traditional file input element
1895 const fileInput = document.createElement('input');
1896 fileInput.type = 'file';
1897 fileInput.accept = 'application/JSON';
1898 fileInput.addEventListener('change', (event) => {
1899 const file = event.target.files[0];
1901 const reader = new FileReader();
1902 reader.onload = async (evObj) => {
1903 const text = evObj.target.result;
1904 pasteBuffer(e, text, file.name);
1906 reader.readAsText(file);
1909 // Trigger the file input element
1913 if (error.name !== 'AbortError') {
1914 // Handle errors as needed
1920function loadFileFromSelector(e) {
1922 let input = document.getElementById('fileSelector');
1923 let file = input.files[0];
1924 if (file !== undefined) {
1925 let reader = new FileReader();
1926 reader.readAsText(file);
1928 reader.onerror = function () {
1929 dlgAlert('File read error: ' + reader.error);
1932 reader.onload = function () {
1933 pasteBuffer(e.param, reader.result, file.name);
1938function odb_load(e) {
1939 let tb = getOdbTb(e);
1940 let path = tb.odb.path;
1941 _odb_path = tb.odb.path;
1942 file_picker("", "*.json", loadFile);
1945function loadFile(filename) {
1946 file_load_ascii(filename, (text) => pasteBuffer(undefined, text, filename));
1949function pasteBuffer(e, text, filename) {
1953 if (e === undefined)
1962 odbJson = JSON.parse(text);
1967 // delete /MIDAS version, /filename etc.
1968 for (let [name, value] of Object.entries(odbJson)) {
1969 if (name[0] === '/')
1970 delete odbJson[name];
1972 mjsonrpc_db_paste([path], [odbJson]).then(rpc =>
1973 dlgAlert("File \"" + filename + "\" successfully loaded to ODB at " + path)).catch(error =>
1974 mjsonrpc_error_alert(error));
1977function make_dialog_scroll(tb) {
1978 let d = tb.parentElement.parentElement.parentElement.parentElement;
1981 d.style.position = "absolute"; // remove "fixed"
1984function subdir_open_click(event, e) {
1985 event.stopPropagation();
1986 let tr = e.parentNode;
1987 while (tr.tagName !== 'TR')
1989 let path = tr.odbPath;
1991 subdir_open(event.target, path);
1994// user clicks on subdirectory, so open it
1995function subdir_open(tr, path) {
1996 while (tr.tagName !== 'TR')
1998 let tb = getOdbTb(tr);
2001 // don't open subdirs if in reorder mode
2002 if (odb.handleColumn)
2005 unselect_all_keys(tb);
2006 unselect_all_array_elements(tb);
2007 make_dialog_scroll(tb);
2009 // find key belonging to 'path'
2010 tr.key.subdir_open = !tr.key.subdir_open;
2011 tb.odb.skip_yellow = true;
2015function subdir_goto_click(event, e) {
2016 event.stopPropagation();
2017 let tr = e.parentNode;
2018 while (tr.tagName !== 'TR')
2020 let path = tr.odbPath;
2022 // don't open subdir if we are in rename mode
2023 if (tr.childNodes[1].childNodes[2].odbParam &&
2024 tr.childNodes[1].childNodes[2].odbParam.inEdit)
2027 subdir_goto(event.target, path);
2030function subdir_goto(e, path) {
2031 let tb = getOdbTb(e);
2034 unselect_all_keys(tb);
2035 unselect_all_array_elements(tb);
2038 if (odb.updateTimer !== undefined)
2039 window.clearTimeout(odb.updateTimer);
2043 let url = window.location.href;
2044 if (url.search("&odb_path") !== -1)
2045 url = url.slice(0, url.search("&odb_path"));
2046 url += "&odb_path=" + encodeURIComponent(path); // convert spaces to %20 etc
2049 // "cmd=ODB" vs. "cmd=ODB&odb_path=/"
2050 if (window.location.href.indexOf('&') === -1 &&
2054 if (url !== window.location.href && !skip)
2055 window.history.pushState({'path': path}, '', url);
2059 if (document.title.indexOf("ODB") !== -1) {
2061 document.title = "ODB";
2063 document.title = "ODB " + path;
2066 // update ODB object
2071 odb.skip_yellow = true;
2076// push all paths of open subdirectories recursively for a following db_ls
2077function push_paths(paths, odb, path) {
2078 odb.dataIndex = paths.length;
2080 for (let i=0 ; i< odb.key.length ; i++) {
2081 if (odb.key[i].type === TID_KEY && odb.key[i].subdir_open) {
2082 if (odb.key[i].value.id === undefined) {
2083 odb.key[i].value = {
2085 path: odb.path === '/' ? '/' + odb.key[i].name : odb.path + '/' + odb.key[i].name,
2086 level: odb.level + 1,
2087 dataIndex: paths.length,
2091 push_paths(paths, odb.key[i].value, odb.key[i].value.path);
2096// update overall ODB display
2097function odb_update(tb) {
2099 // console.log("odb_update" + new Date());
2101 if (tb === undefined)
2102 tb = document.getElementById("odbTable").firstChild;
2105 if (odb.updateTimer !== undefined)
2106 window.clearTimeout(odb.updateTimer);
2107 let row = { i:4, nvk:0 };
2109 // show clickable ODB path in top row
2111 if (odb.path === '/')
2114 dirs = odb.path.split('/');
2116 let s = "<a href=\"#\" onclick=\"subdir_goto(this, '/');return false;\" title=\"Root directory\">" +
2117 "<img src=\"icons/slash-square.svg\" style=\"border:none; height: 16px; vertical-align: middle;margin-bottom: 2px;\">" +
2119 for (let i=1 ; i<dirs.length ; i++) {
2120 path += "/" + dirs[i];
2121 s += "<a href=\"#\" onclick=\"subdir_goto(this, '" + escapeHTML(path) + "');return false;\">"+escapeHTML(dirs[i])+"</a>";
2122 if (i < dirs.length - 1)
2123 s += " / ";
2125 if (s !== tb.childNodes[1].firstChild.innerHTML)
2126 tb.childNodes[1].firstChild.innerHTML = s;
2130 push_paths(paths, odb, odb.path);
2132 mjsonrpc_db_ls(paths).then(rpc => {
2133 odb_extract(odb, rpc.result.data);
2134 odb_print(tb, row, tb.odb);
2135 odb.skip_yellow = false;
2137 // call timer in one second to update again
2138 odb.updateTimer = window.setTimeout(odb_update, 1000, tb);
2142// extract keys and put them into odb tree from db_ls result in dataArray
2143function odb_extract(odb, dataArray) {
2144 let data = dataArray[odb.dataIndex];
2146 for (const item in data) {
2147 if (item.indexOf('/') !== -1)
2152 key.value = data[item];
2153 let linkFlag = data[item + '/key'] !== undefined && data[item + '/key'].link !== undefined;
2154 if (!linkFlag && typeof key.value === 'object' && Object.keys(key.value).length === 0) {
2156 key.subdir_open = false;
2159 key.last_written = 0;
2160 key.access_mode = 0;
2162 key.type = data[item + '/key'].type;
2163 key.link = data[item + '/key'].link;
2164 key.num_values = data[item + '/key'].num_values;
2165 if (key.num_values === undefined)
2167 if (data[item + '/key'].item_size !== undefined)
2168 key.item_size = data[item + '/key'].item_size;
2170 key.item_size = tid_size[key.type];
2171 key.last_written = data[item + '/key'].last_written;
2172 key.access_mode = data[item + '/key'].access_mode;
2174 key.options = (key.name.substring(0, 7) === "Options");
2176 if (odb.key.length <= n) {
2177 key.expanded = (key.num_values > 1 && key.num_values <= 10);
2179 key.expanded = false;
2182 if (odb.key[n].subdir_open) {
2183 key.subdir_open = true;
2184 key.value = odb.key[n].value;
2186 if (odb.key[n].expanded !== undefined) {
2187 key.expanded = odb.key[n].expanded;
2193 // if current key is a subdirectory, call us recursively
2194 if (key.type === TID_KEY && key.subdir_open) {
2195 odb.key[n-1].value.picker = odb.picker;
2196 odb_extract(odb.key[n-1].value, dataArray);
2200 // if local data remaining, ODB key must have been deleted, so delete also local data
2201 if (odb.key.length > n) {
2202 odb.key = odb.key.slice(0, n);
2203 odb.skip_yellow = true;
2207function odb_print_all(tb) {
2209 let row = { i:4, nvk:0 };
2211 odb.skip_yellow = true;
2212 odb_print(tb, row, tb.odb);
2215function odb_print(tb, row, odb) {
2217 // first print all directories
2218 if (odb.subdirFirst) {
2219 for (const key of odb.key) {
2220 if (key.type !== TID_KEY)
2223 // Print current key
2224 odb_print_key(tb, row, odb.path, key, odb.level);
2227 // Print whole subdirectory if open
2228 if (key.subdir_open) {
2230 // Propagate flags to all subkeys
2231 key.value.skip_yellow = odb.skip_yellow;
2232 key.value.subdirFirst = odb.subdirFirst;
2234 // Print whole subdirectory
2235 odb_print(tb, row, key.value);
2240 // now print all keys
2241 for (const key of odb.key) {
2243 if (odb.subdirFirst)
2244 if (key.type === TID_KEY)
2247 // search for "Options <key>"
2248 let options = odb.key.find(o => o.name === 'Options ' + key.name);
2250 // Print current key
2251 odb_print_key(tb, row, odb.path, key, odb.level, options ? options.value : undefined);
2256 // Hide 'value' if only directories are listed
2257 let ds = document.getElementsByName('valueHeader');
2260 d.style.display = row.nvk > 0 ? 'table-cell' : 'none';
2262 // At the end, remove old rows if subdirectory has been closed
2263 if (odb.level === 0)
2264 while (tb.childNodes.length > row.i)
2265 tb.removeChild(tb.childNodes[tb.childNodes.length-1]);
2268function find_input_element(e) {
2269 if (e === undefined)
2271 for (const c of e.childNodes) {
2272 if (c.tagName === 'INPUT')
2274 if (c.tagName === 'SELECT' && e.inEdit)
2276 if (c.childNodes.length > 0)
2277 if (find_input_element(c))
2283function bnToHex(val) {
2286 hex = ((1n << 64n) + BigInt(val)).toString(16).toUpperCase();
2287 hex = hex.padStart(16, '0');
2289 hex = val.toString(16).toUpperCase();
2290 if (hex.length % 2) {
2298function odb_print_key(tb, row, path, key, level, options) {
2302 let keyPath = (path === '/' ? '/' + key.name : path + '/' + key.name);
2305 let tr = document.createElement('TR');
2308 let td = document.createElement('TD');
2309 td.setAttribute('name', 'odbHandle');
2310 td.style.display = odb.handleColumn ? 'table-cell' : 'none';
2311 td.style.width = "10px";
2313 td.innerHTML = '<img style="cursor: all-scroll; height: 13px; padding: 0; border: 0" src="icons/menu.svg">';
2315 td.childNodes[0].setAttribute('draggable', false);
2318 td = document.createElement('TD');
2321 // Add three spaces of indent for each level
2322 let indent = "<span>";
2323 for (let i = 0; i < level; i++)
2324 indent += " ";
2325 indent += "</span>";
2327 // Remember indent level for key rename
2328 tr.level = odb.level;
2330 // Remember key (need to toggle expanded flag)
2333 // Remember key path
2334 tr.odbPath = keyPath;
2336 // Set special class for "Options ..." keys
2337 if (key.options && !odb.detailsColumn)
2338 tr.style.display = 'none';
2340 // Print subdir with open subdir handler
2341 if (key.type === TID_KEY) {
2342 if (odb.handleColumn) {
2343 // do not show any links in re-order mode
2344 td.innerHTML = indent + " \u25B8 " + escapeHTML(key.name);
2346 let handler = "onclick=\"subdir_open_click(event, this);return false;\" ";
2348 if (key.subdir_open)
2349 td.innerHTML = indent + "<a href='#' " +
2351 "> \u25BE </a>";
2353 td.innerHTML = indent + "<a href='#' " +
2355 "> \u25B8 </a>";
2357 handler = "onclick=\"subdir_goto_click(event, this);return false;\" ";
2358 td.innerHTML += "<a href='#' " + handler + ">" + escapeHTML(key.name) + "</a>";
2363 td.innerHTML += ' → <span>' + key.link + '</span>';
2365 td.innerHTML += ' → <span>'+ '<a href="#" ' +
2366 'onclick="inline_edit(event, this.parentNode, \'' +
2367 key.link + '\', odb_setlink, undefined, \'' + keyPath +
2368 '\'); return false;" ' +
2369 ' title="Change value">' + key.link +
2373 } else if (key.link === undefined) {
2374 td.innerHTML = indent + '<span>' + escapeHTML(key.name) + '</span>';
2376 td.innerHTML = indent + '<span>' + escapeHTML(key.name) + '</span>';
2378 td.innerHTML += ' → <span>' + escapeHTML(key.link) + '</span>';
2380 td.innerHTML += ' → <span>'+ '<a href="#" ' +
2381 'onclick="inline_edit(event, this.parentNode, \'' +
2382 key.link + '\', odb_setlink, undefined, \'' + keyPath +
2383 '\'); return false;" ' +
2384 ' title="Change value">' + escapeHTML(key.link) +
2388 // Subdirs occupy all 8 columns
2389 if (key.type === TID_KEY) {
2392 if (tb.childNodes.length > row.i) {
2393 let selected = tb.childNodes[row.i].odbSelected;
2395 tr.style.backgroundColor = '#004CBD';
2396 change_color(tr, '#FFFFFF');
2399 if (!tb.childNodes[row.i].isEqualNode(tr)) {
2400 // check for edit mode
2401 let inlineEdit = find_input_element(tb.childNodes[row.i]);
2403 if (tb.childNodes[row.i].innerHTML != tr.innerHTML)
2404 tb.childNodes[row.i].innerHTML = tr.innerHTML;
2410 // Remove and install mouse click handlers
2411 if (tb.childNodes[row.i].childNodes[2] !== undefined) {
2412 tb.childNodes[row.i].childNodes[2].removeEventListener('mousedown', select_array_element);
2413 tb.childNodes[row.i].childNodes[2].removeEventListener('mousemove', select_array_element);
2415 tb.childNodes[row.i].addEventListener('mousedown', select_key);
2417 tb.childNodes[row.i].addEventListener('mousemove', select_key);
2418 tb.childNodes[row.i].addEventListener('contextmenu', context_menu);
2421 // Remember ODB path in row
2422 tb.childNodes[row.i].odbPath = keyPath;
2423 tb.childNodes[row.i].key = key;
2425 // Remove and odbOptions class or invisibility
2426 tb.childNodes[row.i].className = "";
2427 tb.childNodes[row.i].style.display = 'table-row';
2433 td = document.createElement('TD');
2436 if (Array.isArray(key.value)) {
2439 '<div style="display: inline;"></div>';
2441 if (key.type === TID_BOOL) {
2443 '<div style="display: inline;">' +
2444 '<select title="Set array elements to same value" ' +
2445 'onchange="odb_setall_key(this, \'' + keyPath + '\', this.value);">' +
2446 '<option value=""></option>' +
2447 '<option value="0">No</option>' +
2448 '<option value="1">Yes</option>' +
2453 '<div style="display: inline;"><a href="#" ' +
2454 'onclick="inline_edit(event, this.parentNode, \'' +
2455 key.value[0] + '\', odb_setall, 10, \'' + keyPath +
2456 '\');return false;" ' +
2457 'title="Set array elements to same value">*</a></div>';
2460 if (key.expanded === false)
2462 '<div style="display: inline;float: right">' +
2464 'onclick="toggle_expanded(this.parentNode);return false;" ' +
2465 'title="Show array elements">\u25B8</a>' +
2469 '<div style="display: inline;float: right">' +
2471 'onclick="toggle_expanded(this.parentNode);return false;" ' +
2472 'title="Hide array elements">\u25BE</a>' +
2476 if (key.type === TID_BOOL) {
2478 let h = "<select onchange='odb_setoption(this)' " +
2479 " onfocus='option_edit(event, this, true)'" +
2480 " onblur='option_edit(event, this, false)'" +
2483 if (key.value === false) {
2484 h += "<option selected value='" + 0 + "'>No</option>";
2485 h += "<option value='" + 1 + "'>Yes</option>";
2487 h += "<option value='" + 0 + "'>No</option>";
2488 h += "<option selected value='" + 1 + "'>Yes</option>";
2493 } else if (options) {
2495 let oc = options.slice(); // make copy of all elements
2497 let h = "<select onchange='odb_setoption(this)' " +
2498 " onfocus='option_edit(event, this, true)'" +
2499 " onblur='option_edit(event, this, false)'" +
2502 // check if current value is in options, add it if not
2503 if (!oc.includes(key.value))
2504 oc.unshift(key.value);
2506 for (const o of oc) {
2507 if (key.value === o)
2508 h += "<option selected value='" + o + "'>" + o + "</option>";
2510 h += "<option value='" + o + "'>" + o + "</option>";
2517 let edit = '<a href="#" onclick="this.parentNode.dataset.format=\'\';' +
2518 'this.parentNode.refreshPage=odb_update;' +
2519 'ODBInlineEdit(this.parentNode, \'' + keyPath + '\');return false;" ' +
2520 'onfocus="ODBInlineEdit(this.parentNode, \'' + keyPath + '\')" title="Change value">';
2521 let editHex = '<a href="#" onclick="this.parentNode.dataset.format=\'x\';' +
2522 'this.parentNode.refreshPage=odb_update;' +
2523 'ODBInlineEdit(this.parentNode, \'' + keyPath + '\');return false;" ' +
2524 'onfocus="ODBInlineEdit(this.parentNode, \'' + keyPath + '\')" title="Change hex value">';
2526 let v = escapeHTML(key.value.toString());
2528 let hexValue = false;
2529 if (key.type === TID_STRING && v === "")
2531 else if (key.type === TID_STRING && v.trim() === "")
2533 else if (key.type === TID_LINK)
2534 v = '<span style="color:red;background-color:yellow">(cannot resolve link)</span>';
2535 else if (v.substring(0, 2) === "0x") {
2537 vHex = "0x" + v.substring(2).toUpperCase();
2538 if (key.type === TID_UINT64)
2539 v = BigInt(key.value).toString();
2541 v = parseInt(key.value);
2542 } else if (key.type !== TID_STRING && key.type !== TID_LINK && key.type !== TID_FLOAT && key.type !== TID_DOUBLE) {
2543 if (key.type === TID_INT8)
2544 vHex = "0x" + (key.value & 0xFF).toString(16).toUpperCase();
2545 else if (key.type === TID_INT16)
2546 vHex = "0x" + (key.value & 0xFFFF).toString(16).toUpperCase();
2547 else if (key.type === TID_INT32)
2548 vHex = "0x" + (key.value >>> 0).toString(16).toUpperCase();
2549 else if (key.type === TID_INT64)
2550 vHex = "0x" + bnToHex(BigInt(key.value));
2552 vHex = "0x" + key.value.toString(16).toUpperCase();
2554 if (odb.picker || odb.handleColumn)
2555 td.innerHTML = v + vHex;
2558 td.innerHTML = edit + v + '</a>';
2561 td.innerHTML = editHex + vHex + '</a> ' + ' (' + edit + v + '</a>)';
2563 td.innerHTML = edit + v + '</a> ' + ' (' + editHex + vHex + '</a>)';
2570 td = document.createElement('TD');
2571 td.setAttribute('name', 'odbExt');
2572 td.style.display = odb.detailsColumn ? 'table-cell' : 'none';
2574 td.appendChild(document.createTextNode(tid_name[key.type]));
2577 td = document.createElement('TD');
2578 td.setAttribute('name', 'odbExt');
2579 td.style.display = odb.detailsColumn ? 'table-cell' : 'none';
2581 td.innerHTML = '<a href="#" onclick="resize_array(event, \''+keyPath+'\','+key.num_values+');return false;">'
2582 + key.num_values + '</a>';
2585 td = document.createElement('TD');
2586 td.setAttribute('name', 'odbExt');
2587 td.style.display = odb.detailsColumn ? 'table-cell' : 'none';
2589 if (key.type === TID_STRING)
2590 td.innerHTML = '<a href="#" onclick="resize_string(event, \''+keyPath+'\','+key.item_size+','+key.num_values+');return false;">'
2591 + key.item_size + '</a>';
2593 td.innerHTML = key.item_size;
2596 td = document.createElement('TD');
2597 td.setAttribute('name', 'odbExt');
2598 td.style.display = odb.detailsColumn ? 'table-cell' : 'none';
2600 let s = Math.floor(0.5 + new Date().getTime() / 1000 - key.last_written);
2604 else if (s < 60 * 60)
2605 t = Math.floor(0.5 + s / 60) + 'm';
2606 else if (s < 60 * 60 * 24)
2607 t = Math.floor(0.5 + s / 60 / 60) + 'h';
2608 else if (s < 60 * 60 * 24 * 99)
2609 t = Math.floor(0.5 + s / 60 / 60 / 24) + 'd';
2612 td.appendChild(document.createTextNode(t));
2615 td = document.createElement('TD');
2616 td.setAttribute('name', 'odbExt');
2617 td.style.display = odb.detailsColumn ? 'table-cell' : 'none';
2619 let mode = key.access_mode;
2621 if (mode & MODE_READ)
2623 if (mode & MODE_WRITE)
2625 if (mode & MODE_DELETE)
2627 if (mode & MODE_EXCLUSIVE)
2629 if (mode & MODE_WATCH)
2631 td.appendChild(document.createTextNode(m));
2633 // invert color if selected
2634 if (tb.childNodes.length > row.i) {
2635 let selected = tb.childNodes[row.i].odbSelected;
2637 tr.style.backgroundColor = '#004CBD';
2638 change_color(tr, '#FFFFFF');
2642 if (row.i >= tb.childNodes.length) {
2643 // append new row if nothing exists
2646 // replace current row if it differs
2647 if (!tb.childNodes[row.i].isEqualNode(tr)) {
2648 if (tb.childNodes[row.i].childNodes[1].colSpan !== 1)
2649 tb.childNodes[row.i].childNodes[1].colSpan = "1";
2650 for (let i = 0; i < 8; i++) {
2651 let yellowBg = false;
2653 // check for data change in value column
2655 let oldElem = tb.childNodes[row.i].childNodes[i];
2656 if (oldElem === undefined)
2659 while (oldElem.childNodes[0] !== undefined &&
2660 (oldElem.tagName === 'TD' || oldElem.tagName === 'A' || oldElem.tagName === 'DIV'))
2661 oldElem = oldElem.childNodes[0]; // get into <td> <a> <div> elements
2662 oldElem = oldElem.parentNode.innerHTML;
2664 let newElem = tr.childNodes[i];
2665 if (newElem === undefined)
2668 while (newElem.childNodes[0] !== undefined &&
2669 (newElem.tagName === 'TD' || newElem.tagName === 'A' || newElem.tagName === 'DIV'))
2670 newElem = newElem.childNodes[0]; // get into <td> <a> <div> elements
2671 newElem = newElem.parentNode.innerHTML;
2674 if (oldElem !== newElem && // value changed
2675 !odb.skip_yellow && // skip if globally disabled
2676 // skip if edit just finished
2677 !(oldElem.indexOf('(') == -1 && newElem.indexOf('(') !== -1) &&
2678 // skip '*' of arrays
2679 newElem.indexOf('>*</a>') === -1)
2685 // check for edit mode
2686 let inlineEdit = find_input_element(tb.childNodes[row.i].childNodes[i]);
2688 if (tb.childNodes[row.i].childNodes[i] === undefined)
2689 tb.childNodes[row.i].appendChild(tr.childNodes[i].cloneNode(true));
2690 else if (!inlineEdit) {
2691 let e = tb.childNodes[row.i].childNodes[i];
2693 if (tb.childNodes[row.i].childNodes[i].innerHTML !== tr.childNodes[i].innerHTML)
2694 tb.childNodes[row.i].childNodes[i].innerHTML = tr.childNodes[i].innerHTML;
2697 e.style.backgroundColor = 'var(--myellow)';
2698 e.style.setProperty("-webkit-transition", "", "");
2699 e.style.setProperty("transition", "", "");
2700 e.age = new Date() / 1000;
2703 if (e.age !== undefined && new Date() / 1000 > e.age + 1) {
2704 e.style.setProperty("-webkit-transition", "background-color 1s", "");
2705 e.style.setProperty("transition", "background-color 1s", "");
2706 e.style.backgroundColor = "";
2713 // Install mouse click handler
2714 if (tb.childNodes[row.i].childNodes[2] !== undefined) {
2715 tb.childNodes[row.i].childNodes[2].removeEventListener('mousedown', select_array_element);
2716 tb.childNodes[row.i].childNodes[2].removeEventListener('mousemove', select_array_element);
2718 tb.childNodes[row.i].addEventListener('mousedown', select_key);
2720 tb.childNodes[row.i].addEventListener('mousemove', select_key);
2721 tb.childNodes[row.i].addEventListener('contextmenu', context_menu);
2724 // Remember ODB path and key in row
2725 tb.childNodes[row.i].odbPath = keyPath;
2726 tb.childNodes[row.i].key = key;
2728 // Copy visibility for odbOptions
2729 tb.childNodes[row.i].style.display = tr.style.display;
2731 // Print array values
2732 if (Array.isArray(key.value)) {
2733 if (key.expanded === false) {
2735 } else for (let i=0 ; i<key.value.length ; i++) {
2738 // return if in edit mode
2739 if (tb.childNodes[row.i] !== undefined &&
2740 tb.childNodes[row.i].childNodes[2] !== undefined &&
2741 tb.childNodes[row.i].childNodes[2].inEdit)
2745 let tr = document.createElement('TR');
2748 tr.odbPath = keyPath + '[' + i + ']';
2750 // hide option keys if not in detailed column mode
2751 if (key.options && !odb.detailsColumn)
2752 tr.style.display = 'none';
2754 // Handle column (empty for array values)
2755 let td = document.createElement('TD');
2756 td.setAttribute('name', 'odbHandle');
2757 td.style.display = odb.handleColumn ? 'table-cell' : 'none';
2758 td.style.width = "10px";
2762 td = document.createElement('TD');
2766 td = document.createElement('TD');
2768 let p = (path === '/' ? '/' + key.name : path + '/' + key.name);
2771 if (key.type === TID_BOOL) {
2773 let h = "<select onchange='odb_setoption(this," + i + ")' " +
2774 " onfocus='option_edit(event, this, true)'" +
2775 " onblur='option_edit(event, this, false)'" +
2778 if (key.value[i] === false) {
2779 h += "<option selected value='" + 0 + "'>No</option>";
2780 h += "<option value='" + 1 + "'>Yes</option>";
2782 h += "<option value='" + 0 + "'>No</option>";
2783 h += "<option selected value='" + 1 + "'>Yes</option>";
2786 td.innerHTML = "[" + i + "]" + h;
2788 } else if (options) {
2790 let oc = options.slice(); // make copy of all elements
2792 let h = "<select onchange='odb_setoption(this," + i + ")' " +
2793 " onfocus='option_edit(event, this, true)'" +
2794 " onblur='option_edit(event, this, false)'" +
2797 // check if current value is in options, add it if not
2798 if (!oc.includes(key.value[i]))
2799 oc.unshift(key.value[i]);
2801 for (const o of oc) {
2802 if (key.value[i] === o)
2803 h += "<option selected value='" + o + "'>" + o + "</option>";
2805 h += "<option value='" + o + "'>" + o + "</option>";
2811 let edit = '<a href="#" onclick="ODBInlineEdit(this.parentNode, \'' + p + '\');return false;" ' +
2812 'onfocus="ODBInlineEdit(this.parentNode, \'' + p + '\')" title="Change array element">';
2814 let v = escapeHTML(key.value[i].toString());
2815 if (key.type === TID_STRING && v === "")
2817 else if (key.type === TID_STRING && v.trim() === "")
2819 else if (key.type === TID_LINK)
2820 v = '<span style="color:red;background-color:yellow">(cannot resolve link)</span>';
2821 else if (v.substring(0, 2) === "0x") {
2822 v = "0x" + v.substring(2).toUpperCase();
2823 if (key.type === TID_UINT64)
2824 v += " (" + BigInt(key.value[i]).toString() + ')';
2826 v += " (" + parseInt(key.value[i]) + ')';
2827 } else if (key.type !== TID_STRING && key.type !== TID_LINK && key.type !== TID_FLOAT && key.type !== TID_DOUBLE) {
2828 if (key.type === TID_INT8)
2829 v += " (0x" + (key.value[i] & 0xFF).toString(16).toUpperCase() + ')';
2830 else if (key.type === TID_INT16)
2831 v += " (0x" + (key.value[i] & 0xFFFF).toString(16).toUpperCase() + ')';
2832 else if (key.type === TID_INT32)
2833 v += " (0x" + (key.value[i] & 0xFFFFFFFF).toString(16).toUpperCase() + ')';
2834 else if (key.type === TID_INT64)
2835 v += " (0x" + bnToHex(BigInt(key.value[i])) + ')';
2837 v += " (0x" + key.value[i].toString(16).toUpperCase() + ')';
2841 td.innerHTML = '[' + i + '] ' + v;
2842 else if (odb.handlecolumn)
2845 td.innerHTML = '[' + i + '] ' + edit + v + '</a>';
2849 for (let i=0 ; i<5 ; i++) {
2850 td = document.createElement('TD');
2851 td.setAttribute('name', 'odbExt');
2852 td.style.display = odb.detailsColumn ? 'table-cell' : 'none';
2856 if (row.i >= tb.childNodes.length) {
2859 if (!tb.childNodes[row.i].isEqualNode(tr)) {
2860 if (tr.childNodes.length === 1) { // Subdir
2861 tb.childNodes[row.i].replaceWith(tr);
2862 } else if (tr.childNodes.length === 8) { // Key
2863 let odbSelected = tb.childNodes[row.i].odbSelected;
2864 let odbLastSelected = tb.childNodes[row.i].odbLastSelected;
2865 if (tb.childNodes[row.i].childNodes[1].colSpan !== 1)
2866 tb.childNodes[row.i].childNodes[1].colSpan = "1";
2867 for (let i = 0; i < 8; i++) {
2868 let changed = false;
2873 let select = (tr.childNodes[2].childNodes[0] &&
2874 tr.childNodes[2].childNodes[0].tagName == 'SELECT');
2876 if (tb.childNodes[row.i].childNodes[2] !== undefined) {
2878 oldValue = tb.childNodes[row.i].childNodes[2].childNodes[0].value;
2879 else if (tb.childNodes[row.i].childNodes[2].childNodes[1] !== undefined)
2880 oldValue = tb.childNodes[row.i].childNodes[2].childNodes[1].innerHTML;
2881 else if (tb.childNodes[row.i].childNodes[2].childNodes[0] !== undefined)
2882 oldValue = tb.childNodes[row.i].childNodes[2].childNodes[0].innerHTML
2884 oldValue = tb.childNodes[row.i].childNodes[2].innerHTML;
2887 newValue = tr.childNodes[2].childNodes[0].value;
2889 if (tr.childNodes[2].childNodes[1] === undefined)
2890 newValue = tr.childNodes[2].childNodes[0].data;
2892 newValue = tr.childNodes[2].childNodes[1].innerHTML;
2896 if (oldValue !== undefined &&
2897 oldValue !== newValue && // value changed
2898 i === 2 && // we are in value column
2899 !odb.skip_yellow && // skip if globally disabled
2900 // skip if edit just finished
2901 !(oldValue.indexOf('(') == -1 && newValue.indexOf('(') !== -1) &&
2902 // skip '*' of arrays
2903 newValue.indexOf('>*</a>') === -1 &&
2904 tb.childNodes[row.i].odbSelected !== true)
2907 if (tb.childNodes[row.i].childNodes[i] === undefined)
2908 tb.childNodes[row.i].appendChild(tr.childNodes[i].cloneNode(true));
2910 // preserve color if key is selected
2911 let c = tb.childNodes[row.i].childNodes[i].style.color;
2912 tb.childNodes[row.i].childNodes[i].innerHTML = tr.childNodes[i].innerHTML;
2913 change_color(tb.childNodes[row.i].childNodes[i], c);
2916 let e = tb.childNodes[row.i].childNodes[i];
2918 e.style.backgroundColor = 'var(--myellow)';
2919 e.style.setProperty("-webkit-transition", "", "");
2920 e.style.setProperty("transition", "", "");
2921 e.age = new Date() / 1000;
2924 if (e.age !== undefined && new Date() / 1000 > e.age + 1) {
2925 e.style.setProperty("-webkit-transition", "background-color 1s", "");
2926 e.style.setProperty("transition", "background-color 1s", "");
2927 e.style.backgroundColor = "";
2929 if (e.age !== undefined && new Date() / 1000 > e.age + 2) {
2930 e.style.cssText = "";
2931 e.removeAttribute("style");
2936 tb.childNodes[row.i].odbSelected = odbSelected;
2937 tb.childNodes[row.i].odbLastSelected = odbLastSelected;
2942 // remove/install mouse click handlers
2943 tb.childNodes[row.i].removeEventListener('mousedown', select_key);
2945 tb.childNodes[row.i].removeEventListener('mousemove', select_key);
2946 tb.childNodes[row.i].removeEventListener('contextmenu', context_menu);
2949 tb.childNodes[row.i].childNodes[2].addEventListener('mousedown', select_array_element);
2950 tb.childNodes[row.i].childNodes[2].addEventListener('mousemove', select_array_element);
2952 // catch all mouseup events to stop dragging
2953 document.addEventListener('mouseup', mouseStopDragging);
2954 tb.childNodes[row.i].odbPath = tr.odbPath;
2955 tb.childNodes[row.i].key = undefined;
2957 tb.childNodes[row.i].style.display = tr.style.display;
2963function resize_array(event, path, n) {
2964 event.stopPropagation(); // do not select row
2965 dlgQuery("Enter size of array:", n, do_resize_array, path);
2968function do_resize_array(n, path) {
2969 mjsonrpc_db_resize([path],[parseInt(n)]).then(rpc => {
2970 }).catch(error => mjsonrpc_error_alert(error));
2973function resize_string(event, path, size, num_values) {
2974 event.stopPropagation(); // do not select row
2975 dlgQuery("Enter new string size:", size, do_resize_string, { "path": path, "num_values": num_values });
2978function do_resize_string(size, p) {
2979 mjsonrpc_call("db_resize_string",
2980 { "paths": [p.path],
2981 "new_lengths": [parseInt(p.num_values)],
2982 "new_string_lengths": [parseInt(size)]}).then(rpc => {
2983 }).catch(error => mjsonrpc_error_alert(error));
2986function dlgCreateKeyDown(event, inp) {
2987 let keyCode = ('which' in event) ? event.which : event.keyCode;
2989 if (keyCode === 27) {
2991 dlgHide('dlgCreate');
2995 if (keyCode === 13) {
2997 if (do_new_key(event.target))
2998 dlgHide('dlgCreate');
3005function dlgCreateLinkKeyDown(event, inp) {
3006 let keyCode = ('which' in event) ? event.which : event.keyCode;
3008 if (keyCode === 27) {
3010 dlgHide('dlgCreateLink');
3014 if (keyCode === 13) {
3016 if (do_new_link(this))
3017 dlgHide('dlgCreateLink');
3024function drag_start(event) {
3025 let tr = event.target;
3026 while (tr.tagName !== 'TR')
3028 let tb = getOdbTb(tr);
3030 tb.dragTargetRow = Array.from(tb.children).indexOf(tr);
3031 tb.dragSourceRow = tb.dragTargetRow;
3032 tb.dragSource = tr.odbPath;
3033 tb.dragRowContent = tr.cloneNode(true);
3035 window.setTimeout(() => {
3036 window.clearTimeout(tb.odb.updateTimer);
3037 let w = window.getComputedStyle(tr).width;
3038 let h = window.getComputedStyle(tr).height;
3039 tr.innerHTML = '<td colspan="8" style="background-color:white; border: 2px dashed #6bb28c"></td>';
3040 tr.style.height = parseInt(h) + "px";
3041 tr.style.width = parseInt(w) + "px";
3046function drag_move(event, td) {
3047 event.preventDefault();
3049 let tr = event.target;
3050 while (tr.tagName !== 'TR')
3052 let tb = getOdbTb(tr);
3053 let children = Array.from(tb.children);
3055 if (children.indexOf(tr) > tb.dragTargetRow)
3056 tr.after(tb.childNodes[tb.dragTargetRow]);
3057 else if (children.indexOf(tr) < tb.dragTargetRow)
3058 tr.before(tb.childNodes[tb.dragTargetRow]);
3060 tb.dragTargetRow = children.indexOf(tr);
3063function drag_end(event, td) {
3064 let tr = event.target;
3065 while (tr.tagName !== 'TR')
3067 let tb = getOdbTb(tr);
3069 let ttr = tb.childNodes[tb.dragTargetRow];
3070 ttr.innerHTML = tb.dragRowContent.innerHTML;
3071 ttr.style.height = "";
3072 ttr.style.width = "";
3074 if (tb.dragSourceRow !== tb.dragTargetRow) {
3075 mjsonrpc_call("db_reorder", {"paths": [tb.dragSource], "indices": [tb.dragTargetRow - 4]}).then(rpc => {
3076 tb.odb.updateTimer = window.setTimeout(odb_update, 10, tb);
3077 }).catch(error => mjsonrpc_error_alert(error));
3079 tb.odb.updateTimer = window.setTimeout(odb_update, 10, tb);
3082function show_open_records(odb) {
3083 let title = "ODB open records under \"" + odb.path + "\"";
3085 let d = document.createElement("div");
3086 d.className = "dlgFrame";
3087 d.style.zIndex = "30";
3088 d.style.minWidth = "400px";
3089 d.shouldDestroy = true;
3091 d.innerHTML = "<div class=\"dlgTitlebar\" id=\"dlgMessageTitle\">" + title + "</div>" +
3092 "<div class=\"dlgPanel\" style=\"padding: 2px;\">" +
3093 "<div id=\"dlgSOR\"></div>" +
3094 "<button class=\"dlgButton\" id=\"dlgMessageButton\" +" +
3095 "type=\"button\" " +
3096 " onClick=\"dlgMessageDestroy(this)\">Close</button>" +
3099 document.body.appendChild(d);
3101 update_open_records(odb);
3105function update_open_records(odb) {
3106 let path = odb.path;
3107 mjsonrpc_call("db_sor", {"path": path}).then (rpc => {
3108 let sor = rpc.result.sor;
3110 for (const s of sor) {
3112 paths[s.path] += ', ' + s.name;
3114 paths[s.path] = s.name;
3116 let sorted_paths = Object.keys(paths).sort();
3117 let html = '<table class="mtable" style="width: 100%;margin-top: 0;margin-bottom:0"><tbody>';
3118 html += '<tr><th>ODB Path</th><th>Open by</th></tr>';
3119 for (const p of sorted_paths)
3120 html += '<tr><td style="padding: 4px">' + p + '</td>' +
3121 '<td style="padding: 4px">' + paths[p] + '</td></tr>';
3122 html += '</tbody></table>';
3124 let d = document.getElementById('dlgSOR');
3126 return; // dialog has been closed
3127 if (d.innerHTML !== html) {
3129 dlgCenter(d.parentElement.parentElement);
3131 window.setTimeout(update_open_records, 1000, odb);
3133 }).catch( error => mjsonrpc_error_alert(error));
3136function show_open_clients(e) {
3137 let odb = getOdbTb(e).odb;
3138 let title = "ODB clients";
3140 let d = document.createElement("div");
3141 d.className = "dlgFrame";
3142 d.style.zIndex = "30";
3143 d.style.minWidth = "400px";
3144 d.shouldDestroy = true;
3146 d.innerHTML = "<div class=\"dlgTitlebar\" id=\"dlgMessageTitle\">" + title + "</div>" +
3147 "<div class=\"dlgPanel\" style=\"padding: 2px;\">" +
3148 "<div id=\"dlgSCL\"></div>" +
3149 "<button class=\"dlgButton\" id=\"dlgMessageButton\" +" +
3150 "type=\"button\" " +
3151 " onClick=\"dlgMessageDestroy(this)\">Close</button>" +
3154 document.body.appendChild(d);
3156 update_open_clients(odb);
3160function update_open_clients(odb) {
3161 let path = odb.path;
3162 mjsonrpc_call("db_scl").then (rpc => {
3163 let scl = rpc.result.scl.clients;
3165 let html = '<table class="mtable" style="width: 100%;margin-top: 0;margin-bottom:0"><tbody>';
3166 html += '<tr><th>Name</th><th>Host</th><th>Slot</th><th>PID</th><th>Timeout</th><th>Last active [ms]</th></tr>';
3167 for (const s of scl) {
3168 html += '<tr><td style="padding: 4px"><a href="?cmd=odb&odb_path=/System/Clients/"'
3169 + s.pid + '">' + s.name + '</td>';
3170 html += '<td style="padding: 4px">'+ s.host + '</td>';
3171 html += '<td style="padding: 4px">'+ s.slot + '</td>';
3172 html += '<td style="padding: 4px">'+ s.pid + '</td>';
3173 html += '<td style="padding: 4px">'+ s.watchdog_timeout_millisec + '</td>';
3174 html += '<td style="padding: 4px">'+ s.last_activity_millisec + '</td>';
3177 html += '</tbody></table>';
3179 let d = document.getElementById('dlgSCL');
3181 return; // dialog has been closed
3182 if (d.innerHTML !== html) {
3184 dlgCenter(d.parentElement.parentElement);
3186 window.setTimeout(update_open_clients, 1000, odb);
3188 }).catch( error => mjsonrpc_error_alert(error));