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" maxlength="31"
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 odb_picker("/", setLinkTarget);
731function setLinkTarget(flag, path) {
733 document.getElementById('odbCreateLinkTarget').value = path;
736function do_new_link(e) {
737 e = e.parentElement.parentElement.param;
738 let tb = getOdbTb(e);
740 let path = document.getElementById('odbCreateLinkDir').innerHTML;
741 if (path === '/') path = "";
742 let name = document.getElementById('odbCreateLinkName').value.trim();
743 let target = document.getElementById('odbCreateLinkTarget').value.trim();
745 if (name.length < 1) {
746 dlgAlert("No name specified");
750 if (target.length < 1) {
751 dlgAlert("No link target specified");
755 if (target[0] !== '/') {
756 dlgAlert("Link target must be absolute (start with a \"/\")");
760 path = path + "/" + name;
762 mjsonrpc_call("db_link", {"new_links": [path], "target_paths":[target]}).then(rpc => {
763 let status = rpc.result.status[0];
764 if (status === 311) {
765 dlgMessage("Error", "ODB entry \"" + name + "\" exists already");
766 } else if (status !== 1) {
767 dlgMessage("Error", "db_create_key() error " + status + ", see MIDAS messages");
771 }).catch(error => mjsonrpc_error_alert(error));
776function new_subdir(e) {
777 dlgQuery('Subdirectory name:', "", do_new_subdir, e);
780function do_new_subdir(subdir, e) {
781 if (subdir === false)
784 // remove any leading or trailing spaces
785 subdir = subdir.trim();
787 if (subdir.length < 1) {
788 dlgAlert("No name specified");
792 let tb = getOdbTb(e);
793 let path = tb.odb.path;
797 param.path = path + "/" + subdir;
798 param.type = TID_KEY;
800 mjsonrpc_db_create([param]).then(rpc => {
801 let status = rpc.result.status[0];
802 if (status === 311) {
803 dlgMessage("Error", "ODB key \"" + subdir + "\" exists already");
804 } else if (status !== 1) {
805 dlgMessage("Error", "db_create_key() error " + status + ", see MIDAS messages");
808 }).catch(error => mjsonrpc_error_alert(error));
811function more_menu(event) {
813 event.stopPropagation(); // don't send click to select_key()
815 let odb = getOdbTb(event.target).odb;
817 let d = document.getElementById('moreMenu');
821 d = document.createElement("div");
823 d.style.display = "none";
824 d.style.position = "absolute";
825 d.className = "msidenav";
826 d.style.borderRadius = "0";
827 d.style.border = "2px solid #808080";
828 d.style.margin = "0";
829 d.style.backgroundColor = "#F0F0F0";
831 let cm = document.createElement("div");
833 // Show open records ----------
835 let mDiv = document.createElement("div");
836 mDiv.className = 'mmenuitem mmenulink';
837 mDiv.innerHTML = "<nobr>Show open records...</nobr>";
838 mDiv.title = "Show ODB keys which are open by other programs";
839 mDiv.onclick = function () {
840 d.style.display = 'none';
841 // window.location.href = "?cmd=odb_sor&odb_path=" + encodeURIComponent(odb.path);
842 show_open_records(event.target);
845 cm.appendChild(mDiv);
847 // Show ODB clients ----------
849 mDiv = document.createElement("div");
850 mDiv.className = 'mmenuitem mmenulink';
851 mDiv.innerHTML = "<nobr>Show ODB clients...</nobr>";
852 mDiv.title = "Show clients currently attached to ODB";
853 mDiv.onclick = function () {
854 d.style.display = 'none';
855 // window.location.href = "?cmd=odb_scl";
856 show_open_clients(event.target);
859 cm.appendChild(mDiv);
861 document.body.appendChild(d);
864 let rect = event.target.getBoundingClientRect();
866 d.style.display = 'block';
867 d.style.left = (rect.left + window.scrollX) + 'px';
868 d.style.top = (rect.bottom + 4 + window.scrollY) + 'px';
870 if (parseInt(d.style.left) + d.offsetWidth > document.body.clientWidth)
871 d.style.left = (document.body.clientWidth - d.offsetWidth) + 'px';
874function change_color(e, color) {
875 if (e.style !== undefined && (e.odb === undefined || !e.odb.inEdit))
876 e.style.color = color;
877 if (e.childNodes && (e.odb === undefined || !e.odb.inEdit))
878 for (const c of e.childNodes)
879 if (c.tagName !== 'SELECT')
880 change_color(c, color);
883function unselect_all_keys(tb) {
884 for (let i=4 ; i<tb.childNodes.length ; i++) {
885 let tr = tb.childNodes[i];
888 tr.odbSelected = false;
889 tr.odbLastSelected = false;
890 tr.style.backgroundColor = '';
891 change_color(tr, '');
895function get_selected_keys(tb) {
897 for (let i=4 ; i<tb.childNodes.length ; i++) {
898 let tr = tb.childNodes[i];
900 paths.push({"path": tr.odbPath, "key": tr.key});
905function unselect_all_array_elements(tb) {
906 for (let i=4 ; i<tb.childNodes.length ; i++) {
907 let tr = tb.childNodes[i];
909 // selected array elements
910 if (tr.childNodes.length > 2) {
911 tr.childNodes[2].style.backgroundColor = '';
912 change_color(tr.childNodes[2], '');
917function select_key(event) {
919 console.log("Type: " + event.type + "Target: " + event.target.tagName);
921 // stay off anchors, input boxes and dragging handles
922 if (event.target.tagName === 'A' || event.target.tagName === 'SELECT' ||
923 event.target.tagName === "INPUT" || event.target.parentNode.getAttribute('name') === 'odbHandle')
926 event.preventDefault();
927 event.stopPropagation();
929 let tr = event.target;
930 let tb = getOdbTb(tr);
931 if (tb === undefined)
936 // hide submenu if visible
937 let m = document.getElementById('moreMenu');
938 if (m !== null && m.style.display === 'block')
939 m.style.display = 'none';
941 // hide context menu if visible
942 m = document.getElementById('contextMenu');
943 if (m !== null && m.style.display === 'block')
944 m.style.display = 'none';
946 // un-select all array elements
947 unselect_all_array_elements(tb);
949 // don't select key when we are in edit mode
950 while (tr.tagName !== 'TR') {
955 if (find_input_element(tr))
960 for (let i = 4; i < tb.childNodes.length; i++)
961 tb.childNodes[i].odbSelected = false;
963 // don't select array values
964 if (tr.childNodes[1].innerHTML !== "")
965 tr.odbSelected = true;
967 odb.selectedKey = tr.odbPath;
969 // check if click is on header row, if so remove selection further down
970 let headerRow = false;
971 for (let i = 0; i < 4; i++)
972 if (tb.childNodes[i] === tr) {
977 if (event.type === "mousemove") {
979 if (event.buttons === 1 && !headerRow) {
980 // search last selected row
982 for (i1 = 4; i1 < tb.childNodes.length; i1++)
983 if (tb.childNodes[i1].odbLastSelected)
985 if (i1 === tb.childNodes.length)
986 i1 = 4; // none selected, so use first one
989 for (i2 = 4; i2 < tb.childNodes.length; i2++)
990 if (tb.childNodes[i2] === tr)
993 if (i2 < tb.childNodes.length) {
997 for (let i = i1; i <= i2; i++) {
998 // don't select arrays
999 if (tb.childNodes[i].childNodes[1].innerHTML !== "")
1000 tb.childNodes[i].odbSelected = true;
1005 } else { // mousedown
1007 if (event.shiftKey && !headerRow) {
1008 // search last selected row
1010 for (i1 = 4; i1 < tb.childNodes.length; i1++)
1011 if (tb.childNodes[i1].odbLastSelected)
1013 if (i1 === tb.childNodes.length)
1014 i1 = 4; // none selected, so use first one
1017 for (i2 = 4; i2 < tb.childNodes.length; i2++)
1018 if (tb.childNodes[i2] === tr)
1021 if (i2 < tb.childNodes.length) {
1023 [i1, i2] = [i2, i1];
1025 for (let i = i1; i <= i2; i++) {
1026 // don't select arrays
1027 if (tb.childNodes[i].childNodes[1].innerHTML !== "")
1028 tb.childNodes[i].odbSelected = true;
1032 } else if ((event.metaKey || event.ctrlKey) && !headerRow) {
1034 // command key just toggles current selection
1035 tr.odbSelected = !tr.odbSelected;
1037 for (let i = 4; i < tb.childNodes.length; i++)
1038 tb.childNodes[i].odbLastSelected = false;
1040 tr.odbLastSelected = true;
1044 // no key pressed -> un-select all but current
1045 for (let i = 4; i < tb.childNodes.length; i++) {
1046 tb.childNodes[i].odbSelected = false;
1047 tb.childNodes[i].odbLastSelected = false;
1050 // don't select header row and array values
1051 if (!headerRow && tr.childNodes[1].innerHTML !== "") {
1052 tr.odbSelected = true;
1053 tr.odbLastSelected = true;
1054 tr.odbSelectTime = new Date().getTime();
1060 // change color of all rows according to selection
1061 for (let i=4 ; i<tb.childNodes.length ; i++) {
1062 let tr = tb.childNodes[i];
1064 tr.style.backgroundColor = tr.odbSelected ? '#004CBD' : '';
1065 change_color(tr, tr.odbSelected ? '#FFFFFF' : '');
1069function getArrayTr(tr) {
1070 // collect all rows belonging to "tr" in an array
1071 let tb = getOdbTb(tr);
1074 for (i = 0; i < tb.childNodes.length; i++)
1075 if (tb.childNodes[i] === tr)
1077 while (tb.childNodes[i].odbPath.indexOf('[') !== -1)
1079 i++; // first element
1081 atr.push(tb.childNodes[i++]);
1082 } while (i < tb.childNodes.length && tb.childNodes[i].odbPath.indexOf('[') !== undefined);
1086let mouseDragged = false;
1088function mouseStopDragging() {
1089 mouseDragged = false;
1092function select_array_element(event) {
1094 if (event.type === 'mousemove' && !mouseDragged)
1097 // keep off secondary mouse buttons
1098 if (event.button !== 0)
1101 // keep off drop-down boxes
1102 if (event.target.tagName === "SELECT")
1105 event.preventDefault();
1106 event.stopPropagation();
1108 let tr = event.target;
1109 let tb = getOdbTb(tr);
1110 if (tb === undefined)
1113 // don't select key when we are in edit mode
1114 while (tr.tagName !== 'TR') {
1119 if (find_input_element(tr))
1124 // hide submenu if visible
1125 let m = document.getElementById('moreMenu');
1126 if (m !== null && m.style.display === 'block')
1127 m.style.display = 'none';
1129 // hide context menu if visible
1130 m = document.getElementById('contextMenu');
1131 if (m !== null && m.style.display === 'block')
1132 m.style.display = 'none';
1134 // remove selection from all non-array keys
1135 for (let i=0 ; i<tb.childNodes.length ; i++) {
1136 if (tb.childNodes[i].childNodes.length > 1 &&
1137 tb.childNodes[i].childNodes[1].innerHTML !== "" &&
1138 tb.childNodes[i].odbSelected) {
1139 tb.childNodes[i].odbSelected = false;
1140 tb.childNodes[i].style.backgroundColor = '';
1141 change_color(tb.childNodes[i], '');
1145 // create array of all array elements
1146 let atr = getArrayTr(tr);
1148 if (event.type === 'mousedown') {
1149 mouseDragged = true;
1152 // un-select all but current
1153 for (const tr of atr) {
1154 tr.odbSelected = false;
1155 tr.odbLastSelected = false;
1157 tr.odbSelected = true;
1158 tr.odbLastSelected = true;
1159 odb.selectedKey = tr.odbPath;
1163 if (event.shiftKey) {
1164 // search last selected row
1166 for (i1 = 0; i1 < atr.length; i1++)
1167 if (atr[i1].odbLastSelected)
1169 if (i1 === atr.length)
1170 i1 = 0; // non selected, so use first one
1173 for (i2 = 0; i2 < atr.length; i2++)
1177 if (i2 < atr.length) {
1179 [i1, i2] = [i2, i1];
1181 for (let i = i1; i <= i2; i++)
1182 atr[i].odbSelected = true;
1185 } else if (event.metaKey || event.ctrlKey) {
1187 // command key just toggles current selection
1188 tr.odbSelected = !tr.odbSelected;
1190 for (const tr of atr)
1191 tr.odbLastSelected = false;
1193 tr.odbLastSelected = true;
1197 // no key pressed -> un-select all but current
1198 for (const tr of atr) {
1199 tr.odbSelected = false;
1200 tr.odbLastSelected = false;
1202 tr.odbSelected = true;
1203 tr.odbLastSelected = true;
1208 if (event.type === 'mousemove' && mouseDragged) {
1210 if (!event.shiftKey && !event.metaKey && !event.ctrlKey) {
1212 for (const tr of atr)
1213 tr.odbSelected = false;
1215 // search last selected row
1216 for (i1 = 0; i1 < atr.length; i1++)
1217 if (atr[i1].odbLastSelected)
1219 if (i1 === atr.length)
1220 i1 = 0; // non selected, so use first one
1223 for (i2 = 0; i2 < atr.length; i2++)
1227 if (i2 < atr.length) {
1229 [i1, i2] = [i2, i1];
1231 for (let i = i1; i <= i2; i++)
1232 atr[i].odbSelected = true;
1237 // change color of all rows according to selection
1238 for (const e of atr) {
1239 if (e.childNodes.length >= 3) {
1240 e.childNodes[2].style.backgroundColor = e.odbSelected ? '#004CBD' : '';
1241 change_color(e.childNodes[2], e.odbSelected ? '#FFFFFF' : '');
1246function context_menu(event) {
1247 // don't show context menu if we are in input box (user wants traditional copy/paste)
1248 if (event.target.tagName === 'INPUT')
1251 event.preventDefault();
1252 event.stopPropagation();
1254 let tr = event.target;
1255 while (tr.tagName !== 'TR')
1257 let tb = getOdbTb(tr);
1259 let path = tr.odbPath;
1261 if (!tr.odbSelected)
1264 let d = document.getElementById('contextMenu');
1266 // create menu on the first call
1268 d = document.createElement("div");
1269 d.id = "contextMenu";
1270 d.style.display = "none";
1271 d.style.position = "absolute";
1272 d.className = "msidenav";
1273 d.style.borderRadius = "0";
1274 d.style.border = "2px solid #808080";
1275 d.style.margin = "0";
1276 d.style.backgroundColor = "#F0F0F0";
1278 let cm = document.createElement("div");
1280 let menu = ["Copy key", "Copy plain text", "Delete key", "Rename key...",
1281 "Open in new tab...", "Open in new window..."];
1284 for (const m of menu) {
1285 mDiv = document.createElement("div");
1286 mDiv.className = 'mmenuitem mmenulink';
1287 mDiv.innerHTML = '<nobr>' + m + '</nobr>';
1289 cm.appendChild(mDiv);
1293 document.body.appendChild(d);
1296 // set event handler for copy menu
1297 d.childNodes[0].childNodes[0].onmousedown = function () {
1298 d.style.display = 'none';
1302 // set event handler for copy plain text menu
1303 d.childNodes[0].childNodes[1].onmousedown = function () {
1304 d.style.display = 'none';
1305 let selKeys = get_selected_keys(tb);
1307 for (const k of selKeys)
1310 mjsonrpc_db_copy(paths).then(rpc => {
1312 let needPath = false;
1313 for (let i=0 ; i<rpc.result.data.length ; i++) {
1314 if (selKeys[i].key.type === TID_KEY) {
1315 text += odbASCII(rpc.result.data[i], paths[i]);
1319 text += odbASCII(rpc.result.data[i], tb.odb.path);
1321 text += odbASCII(rpc.result.data[i]);
1325 if (navigator.clipboard && navigator.clipboard.writeText) {
1327 navigator.clipboard.writeText(text).then( () => {
1328 if (paths.length === 1)
1329 dlgAlert("ODB key \"" + paths + "\" copied to clipboard as plain text");
1331 dlgAlert(paths.length + " ODB keys copied to clipboard as plain text");
1332 }).catch(e => dlgAlert(e));
1337 }).catch(error => mjsonrpc_error_alert(error));
1340 // set event handler for delete key menu
1341 d.childNodes[0].childNodes[2].onmousedown = function () {
1342 d.style.display = 'none';
1346 // set event handler for rename menu
1347 d.childNodes[0].childNodes[3].onmousedown = function () {
1348 d.style.display = 'none';
1351 if (tr.key.type === TID_KEY)
1352 elem = tr.childNodes[1].childNodes[2];
1354 elem = tr.childNodes[1].childNodes[1];
1356 let key = elem.innerHTML.trim();
1367 // set event handler for open in new tab
1368 d.childNodes[0].childNodes[4].onmousedown = function () {
1369 d.style.display = 'none';
1371 let url = window.location.href;
1372 if (url.search("&odb_path") !== -1)
1373 url = url.slice(0, url.search("&odb_path"));
1374 url += "&odb_path=" + encodeURIComponent(tr.odbPath); // convert spaces to %20 etc
1376 window.open(url, '_blank').focus();
1379 // set event handler for open in new window
1380 d.childNodes[0].childNodes[5].onmousedown = function () {
1381 d.style.display = 'none';
1383 let url = window.location.href;
1384 if (url.search("&odb_path") !== -1)
1385 url = url.slice(0, url.search("&odb_path"));
1386 url += "&odb_path=" + encodeURIComponent(tr.odbPath); // convert spaces to %20 etc
1388 window.open(url, "", "menubar=yes,location=yes,status=yes");
1391 let rect = event.target.getBoundingClientRect();
1393 d.style.display = 'block';
1394 d.style.left = (event.offsetX + rect.left + window.scrollX) + 'px';
1395 d.style.top = (event.offsetY+rect.top + window.scrollY) + 'px';
1397 if (parseInt(d.style.left) + d.offsetWidth > document.body.clientWidth)
1398 d.style.left = (document.body.clientWidth - d.offsetWidth) + 'px';
1404 let tb = getOdbTb(e);
1407 let rarr = document.getElementById('expRight');
1408 let cells = document.getElementsByName('odbExt');
1409 let table = document.getElementById('odbTable');
1410 odb.detailsColumn = !odb.detailsColumn;
1412 if (odb.detailsColumn) {
1413 for (const d of cells)
1414 d.style.display = 'table-cell';
1415 rarr.style.display = 'none';
1416 table.style.minWidth = '800px';
1418 for (const d of cells)
1419 d.style.display = 'none';
1420 rarr.style.display = 'inline';
1421 table.style.minWidth = '600px';
1423 odb.skip_yellow = true;
1427function clear_expanded(odb) {
1428 for (const key of odb.key) {
1429 if (key.type === TID_KEY)
1430 key.subdir_open = false;
1432 key.expanded = false;
1436function toggle_handles(e) {
1437 let tb = getOdbTb(e);
1440 // clear all expanded flags
1441 clear_expanded(odb);
1444 let n = document.getElementsByName('odbHandle');
1445 odb.handleColumn = !odb.handleColumn;
1447 if (odb.handleColumn) {
1448 odb.subdirFirst = false;
1449 for (const d of n) {
1450 let tr = d.parentNode;
1451 if (tr.firstChild.className !== 'colHeader') {
1452 tr.setAttribute('draggable', true);
1453 tr.addEventListener('dragstart', drag_start);
1454 tr.addEventListener('dragover', drag_move);
1455 tr.addEventListener('dragend', drag_end);
1458 d.style.display = 'table-cell';
1461 odb.subdirFirst = true;
1462 for (const d of n) {
1463 let tr = d.parentNode;
1464 if (tr.firstChild.className !== 'colHeader') {
1465 tr.removeAttribute('draggable');
1466 tr.removeEventListener('dragstart', drag_start);
1467 tr.removeEventListener('dragover', drag_move);
1468 tr.removeEventListener('dragend', drag_end);
1471 d.style.display = 'none';
1478function toggle_expanded(e) {
1480 while (tr.tagName !== 'TR')
1483 if (tr.key.num_values >= 5000 && !tr.key.expanded) {
1484 dlgConfirm("ODB key \""+ tr.key.name + "\" has " + tr.key.num_values +
1485 " array elements. This can take very long. Do you want to continue?", do_toggle_expanded, tr);
1487 do_toggle_expanded(true, tr);
1490function do_toggle_expanded(flag, tr) {
1492 let tb = getOdbTb(tr);
1493 tr.key.expanded = !tr.key.expanded;
1494 unselect_all_keys(tb);
1495 unselect_all_array_elements(tb);
1500function odb_delete(e) {
1502 let tb = getOdbTb(e);
1503 let currentPath = tb.odb.path;
1505 for (const tr of tb.childNodes) {
1507 paths.push(tr.odbPath);
1510 if (paths.length == 0) {
1511 // strip last directory from current path
1512 let newPath = currentPath;
1513 if (newPath.lastIndexOf('/') !== -1)
1514 newPath = newPath.substring(0, newPath.lastIndexOf('/'));
1518 dlgConfirm('Are you sure to delete odb directory \"' + currentPath + '\" ?',
1519 do_odb_delete, {"e": e, "paths": [currentPath], "newPath": newPath} );
1520 } else if (paths.length == 1)
1521 dlgConfirm('Are you sure to delete odb key \"' + paths[0] + '\" ?',
1522 do_odb_delete, {"e": e, "paths": paths});
1524 dlgConfirm('Are you sure to delete ' + paths.length + ' keys ?',
1525 do_odb_delete, {"e": e, "paths": paths});
1528function do_odb_delete(flag, param) {
1530 mjsonrpc_db_delete(param.paths).then((rpc) => {
1532 if (rpc.result.status[0] === 318) {
1533 dlgAlert("ODB key is write protected and cannot be deleted");
1534 } else if (rpc.result.status[0] === 320) {
1535 dlgAlert("ODB key is locked by other program and cannot be deleted");
1536 } else if (rpc.result.status[0] !== 1) {
1537 dlgAlert("ODB error: " + rpc.result.status[0]);
1541 subdir_goto(param.e, param.newPath);
1543 let tb = getOdbTb(param.e);
1547 }).catch(error => mjsonrpc_error_alert(error));
1551function rename_key(element) {
1552 let tb = getOdbTb(element);
1555 for (const t of tb.childNodes) {
1556 if (t.odbSelected) {
1563 dlgAlert("Please select key to be renamed");
1565 dlgAlert("Please select only single key to be renamed");
1567 tr.odbSelected = false;
1568 tr.style.backgroundColor = '';
1569 change_color(tr, '');
1573 if (tr.key.type === TID_KEY)
1574 elem = tr.childNodes[1].childNodes[2];
1576 elem = tr.childNodes[1].childNodes[1];
1578 let key = elem.innerHTML;
1581 inline_edit(undefined,
1590function do_rename_key(p, str, path) {
1592 dlgAlert("Empty name not allowed");
1596 mjsonrpc_call("db_rename", { "paths": [path], "new_names": [str]})
1598 .catch(error => mjsonrpc_error_alert(error));
1600 let old = p.parentElement.parentElement.oldKey;
1602 p.innerHTML = p.innerHTML.replace(old, str);
1605function search_key() {
1606 dlgQuery("Enter key name (substring case insensitive):", "", do_search_key);
1609function do_search_key(str) {
1611 window.location.href = "?cmd=Find&value=" + str;
1614function odb_copy(e) {
1615 let tb = getOdbTb(e);
1616 let selKeys = get_selected_keys(tb);
1618 let dirFlag = false;
1619 if (selKeys.length === 0) {
1620 paths.push(tb.odb.path);
1623 for (const k of selKeys)
1626 mjsonrpc_db_copy(paths).then(rpc => {
1629 // generate text from objects, add top directoy which is missing from db_copy
1630 for (const [i,d] of rpc.result.data.entries()) {
1633 let dir = tb.odb.path.substring(tb.odb.path.lastIndexOf('/')+1);
1636 text += '{ \"'+ dir +
1637 '\": '+JSON.stringify(d, null, ' ')+'}';
1638 } else if (selKeys[i].key.type === TID_KEY) {
1639 text += '{ \"'+ selKeys[i].key.name +'\": '+JSON.stringify(d, null, ' ')+'}';
1641 text += JSON.stringify(d, null, ' ');
1643 text += JSON.stringify(d, null, ' ').substring(1); // strip first '{'
1644 if (i < rpc.result.data.length - 1)
1645 text = text.substring(0, text.lastIndexOf('}')) + ','; // replace last '}' by ','
1648 // put text to clipboard if possible
1649 if (navigator.clipboard && navigator.clipboard.writeText) {
1651 navigator.clipboard.writeText(text).then( () => {
1653 dlgAlert("ODB directory \"" + paths + "\" copied to clipboard");
1655 if (paths.length === 1)
1656 dlgAlert("ODB key \"" + paths + "\" copied to clipboard");
1658 dlgAlert("ODB keys \"" + paths + "\" copied to clipboard");
1660 }).catch(e => dlgAlert(e));
1666 // store text locally as a backup
1668 sessionStorage.setItem('odbclip', text);
1673 }).catch(error => mjsonrpc_error_alert(error));
1676function odb_paste(e) {
1677 let tb = getOdbTb(e);
1680 let text = sessionStorage.getItem('odbclip');
1682 let keys = JSON.parse(text);
1683 odb_paste_keys(tb, keys);
1687 dlgAlert("Invalid JSON format in clipboard:<br /><br />" + error);
1691 if (navigator.clipboard && navigator.clipboard.readText) {
1692 navigator.clipboard.readText().then(text => {
1695 let keys = JSON.parse(text);
1696 odb_paste_keys(tb, keys);
1699 dlgAlert("Nothing stored in clipboard");
1702 dlgAlert("Invalid JSON format in clipboard:<br /><br />" + error);
1706 dlgAlert("Paste not possible in this browser");
1709function odb_paste_keys(tb, newKeys) {
1712 for (const key in newKeys) {
1713 if (key.indexOf('/') !== -1)
1716 // check for existing key in current keys
1717 let cKeyMaxIndex = 0;
1722 for (const cKey of odb.key) {
1723 if (cKey.name.indexOf(' copy') === -1)
1726 ckb = cKey.name.substring(0, cKey.name.indexOf(' copy'));
1731 if (cKey.name.indexOf(' copy') !== -1) {
1732 cKeyIndex = parseInt(cKey.name.substring(cKey.name.indexOf(' copy')+5));
1733 if (isNaN(cKeyIndex))
1735 if (cKeyIndex > cKeyMaxIndex)
1736 cKeyMaxIndex = cKeyIndex;
1741 // rename new key if name exists
1743 let newName = cKeyBare + ' copy';
1745 newName += ' ' + (cKeyIndex+1).toString();
1747 newKeys[newName] = newKeys[key];
1748 delete newKeys[key];
1750 newKeys[newName + '/key'] = newKeys[key + '/key'];
1751 delete newKeys[key + '/key'];
1755 mjsonrpc_db_paste([odb.path], [newKeys]).then(rpc =>
1756 odb_update(tb)).catch(error =>
1761function odbASCII(o, path) {
1762 // convert ODB keys to plain ASCII representation
1764 let need_path = true;
1765 if (path === undefined)
1767 for (const key in o) {
1768 if (key.indexOf('/') !== -1)
1771 if (o[key+'/key'] === undefined && path) {
1772 t += odbASCII(o[key], path + '/' + key);
1779 t += '\n[' + path + ']\n';
1782 let tid = o[key+'/key'].type;
1783 let num_values = o[key+'/key'].num_values;
1786 if (num_values > 1) {
1788 for (let i=0 ; i<num_values ; i++) {
1789 t += '[' + i + ']\t';
1790 t += mie_to_string(tid, o[key][i]);
1795 t += mie_to_string(tid, o[key]);
1802function odb_export(e) {
1803 let tb = getOdbTb(e);
1804 mjsonrpc_db_copy([tb.odb.path]).then(rpc => {
1806 let dirs = tb.odb.path.split('/');
1807 let filename = dirs[dirs.length - 1];
1808 if (filename === '')
1810 filename += ".json";
1813 "/MIDAS version": "2.1",
1814 "/filename": filename,
1815 "/ODB path": tb.odb.path
1817 header = JSON.stringify(header, null, ' ');
1818 header = header.substring(0, header.length-2) + ','; // strip trailing '}'
1820 let odbJson = JSON.stringify(rpc.result.data[0], null, ' ');
1821 if (odbJson.indexOf('{') === 0)
1822 odbJson = odbJson.substring(1); // strip leading '{'
1824 odbJson = header + odbJson;
1826 // use trick from FileSaver.js
1827 let a = document.getElementById('downloadHook');
1829 a = document.createElement("a");
1830 a.style.display = "none";
1831 a.id = "downloadHook";
1832 document.body.appendChild(a);
1835 let blob = new Blob([odbJson], {type: "text/json"});
1836 let url = window.URL.createObjectURL(blob);
1839 a.download = filename;
1841 window.URL.revokeObjectURL(url);
1842 dlgAlert("ODB subtree \"" + tb.odb.path +
1843 "\" downloaded to file \"" + filename + "\"");
1845 }).catch(error => mjsonrpc_error_alert(error));
1849function odb_save_picker(e) {
1850 let tb = getOdbTb(e);
1851 _odb_path = tb.odb.path;
1852 file_picker("", "*.json", odb_save, true);
1854function odb_save(filename) {
1855 mjsonrpc_db_copy([_odb_path]).then(rpc => {
1857 if (filename.indexOf('.json') === -1)
1858 filename += '.json';
1861 "/MIDAS version": "2.1",
1862 "/filename": filename,
1863 "/ODB path": _odb_path
1865 header = JSON.stringify(header, null, ' ');
1866 header = header.substring(0, header.length-2) + ','; // strip trailing '}'
1868 let odbJson = JSON.stringify(rpc.result.data[0], null, ' ');
1869 if (odbJson.indexOf('{') === 0)
1870 odbJson = odbJson.substring(1); // strip leading '{'
1872 odbJson = header + odbJson;
1874 file_save_ascii(filename, odbJson, "ODB subtree \"" + _odb_path +
1875 "\" saved to file \"" + filename + "\"");
1877 }).catch(error => mjsonrpc_error_alert(error));
1880async function odb_import(e) {
1882 // Check if showOpenFilePicker is available in the browser
1883 // The "else" is more of a JS standard
1884 if (window.showOpenFilePicker) {
1886 [fileHandle] = await window.showOpenFilePicker();
1887 const file = await fileHandle.getFile();
1888 const text = await file.text();
1889 pasteBuffer(e, text, file.name);
1891 // Show a traditional file input element
1892 const fileInput = document.createElement('input');
1893 fileInput.type = 'file';
1894 fileInput.accept = 'application/JSON';
1895 fileInput.addEventListener('change', (event) => {
1896 const file = event.target.files[0];
1898 const reader = new FileReader();
1899 reader.onload = async (evObj) => {
1900 const text = evObj.target.result;
1901 pasteBuffer(e, text, file.name);
1903 reader.readAsText(file);
1906 // Trigger the file input element
1910 if (error.name !== 'AbortError') {
1911 // Handle errors as needed
1917function loadFileFromSelector(e) {
1919 let input = document.getElementById('fileSelector');
1920 let file = input.files[0];
1921 if (file !== undefined) {
1922 let reader = new FileReader();
1923 reader.readAsText(file);
1925 reader.onerror = function () {
1926 dlgAlert('File read error: ' + reader.error);
1929 reader.onload = function () {
1930 pasteBuffer(e.param, reader.result, file.name);
1935function odb_load(e) {
1936 let tb = getOdbTb(e);
1937 let path = tb.odb.path;
1938 _odb_path = tb.odb.path;
1939 file_picker("", "*.json", loadFile);
1942function loadFile(filename) {
1943 file_load_ascii(filename, (text) => pasteBuffer(undefined, text, filename));
1946function pasteBuffer(e, text, filename) {
1950 if (e === undefined)
1959 odbJson = JSON.parse(text);
1964 // delete /MIDAS version, /filename etc.
1965 for (let [name, value] of Object.entries(odbJson)) {
1966 if (name[0] === '/')
1967 delete odbJson[name];
1969 mjsonrpc_db_paste([path], [odbJson]).then(rpc =>
1970 dlgAlert("File \"" + filename + "\" successfully loaded to ODB at " + path)).catch(error =>
1971 mjsonrpc_error_alert(error));
1974function make_dialog_scroll(tb) {
1975 let d = tb.parentElement.parentElement.parentElement.parentElement;
1978 d.style.position = "absolute"; // remove "fixed"
1981function subdir_open_click(event, e) {
1982 event.stopPropagation();
1983 let tr = e.parentNode;
1984 while (tr.tagName !== 'TR')
1986 let path = tr.odbPath;
1988 subdir_open(event.target, path);
1991// user clicks on subdirectory, so open it
1992function subdir_open(tr, path) {
1993 while (tr.tagName !== 'TR')
1995 let tb = getOdbTb(tr);
1998 // don't open subdirs if in reorder mode
1999 if (odb.handleColumn)
2002 unselect_all_keys(tb);
2003 unselect_all_array_elements(tb);
2004 make_dialog_scroll(tb);
2006 // find key belonging to 'path'
2007 tr.key.subdir_open = !tr.key.subdir_open;
2008 tb.odb.skip_yellow = true;
2012function subdir_goto_click(event, e) {
2013 event.stopPropagation();
2014 let tr = e.parentNode;
2015 while (tr.tagName !== 'TR')
2017 let path = tr.odbPath;
2019 // don't open subdir if we are in rename mode
2020 if (tr.childNodes[1].childNodes[2].odbParam &&
2021 tr.childNodes[1].childNodes[2].odbParam.inEdit)
2024 subdir_goto(event.target, path);
2027function subdir_goto(e, path) {
2028 let tb = getOdbTb(e);
2031 unselect_all_keys(tb);
2032 unselect_all_array_elements(tb);
2035 if (odb.updateTimer !== undefined)
2036 window.clearTimeout(odb.updateTimer);
2040 let url = window.location.href;
2041 if (url.search("&odb_path") !== -1)
2042 url = url.slice(0, url.search("&odb_path"));
2043 url += "&odb_path=" + encodeURIComponent(path); // convert spaces to %20 etc
2046 // "cmd=ODB" vs. "cmd=ODB&odb_path=/"
2047 if (window.location.href.indexOf('&') === -1 &&
2051 if (url !== window.location.href && !skip)
2052 window.history.pushState({'path': path}, '', url);
2056 if (document.title.indexOf("ODB") !== -1) {
2058 document.title = "ODB";
2060 document.title = "ODB " + path;
2063 // update ODB object
2068 odb.skip_yellow = true;
2073// push all paths of open subdirectories recursively for a following db_ls
2074function push_paths(paths, odb, path) {
2075 odb.dataIndex = paths.length;
2077 for (let i=0 ; i< odb.key.length ; i++) {
2078 if (odb.key[i].type === TID_KEY && odb.key[i].subdir_open) {
2079 if (odb.key[i].value.id === undefined) {
2080 odb.key[i].value = {
2082 path: odb.path === '/' ? '/' + odb.key[i].name : odb.path + '/' + odb.key[i].name,
2083 level: odb.level + 1,
2084 dataIndex: paths.length,
2088 push_paths(paths, odb.key[i].value, odb.key[i].value.path);
2093// update overall ODB display
2094function odb_update(tb) {
2097 if (odb.updateTimer !== undefined)
2098 window.clearTimeout(odb.updateTimer);
2099 let row = { i:4, nvk:0 };
2101 // show clickable ODB path in top row
2103 if (odb.path === '/')
2106 dirs = odb.path.split('/');
2108 let s = "<a href=\"#\" onclick=\"subdir_goto(this, '/');return false;\" title=\"Root directory\">" +
2109 "<img src=\"icons/slash-square.svg\" style=\"border:none; height: 16px; vertical-align: middle;margin-bottom: 2px;\">" +
2111 for (let i=1 ; i<dirs.length ; i++) {
2112 path += "/" + dirs[i];
2113 s += "<a href=\"#\" onclick=\"subdir_goto(this, '" + escapeHTML(path) + "');return false;\">"+escapeHTML(dirs[i])+"</a>";
2114 if (i < dirs.length - 1)
2115 s += " / ";
2117 if (s !== tb.childNodes[1].firstChild.innerHTML)
2118 tb.childNodes[1].firstChild.innerHTML = s;
2122 push_paths(paths, odb, odb.path);
2124 mjsonrpc_db_ls(paths).then(rpc => {
2125 odb_extract(odb, rpc.result.data);
2126 odb_print(tb, row, tb.odb);
2127 odb.skip_yellow = false;
2129 // call timer in one second to update again
2130 odb.updateTimer = window.setTimeout(odb_update, 1000, tb);
2134// extract keys and put them into odb tree from db_ls result in dataArray
2135function odb_extract(odb, dataArray) {
2136 let data = dataArray[odb.dataIndex];
2138 for (const item in data) {
2139 if (item.indexOf('/') !== -1)
2144 key.value = data[item];
2145 let linkFlag = data[item + '/key'] !== undefined && data[item + '/key'].link !== undefined;
2146 if (!linkFlag && typeof key.value === 'object' && Object.keys(key.value).length === 0) {
2148 key.subdir_open = false;
2151 key.last_written = 0;
2152 key.access_mode = 0;
2154 key.type = data[item + '/key'].type;
2155 key.link = data[item + '/key'].link;
2156 key.num_values = data[item + '/key'].num_values;
2157 if (key.num_values === undefined)
2159 if (data[item + '/key'].item_size !== undefined)
2160 key.item_size = data[item + '/key'].item_size;
2162 key.item_size = tid_size[key.type];
2163 key.last_written = data[item + '/key'].last_written;
2164 key.access_mode = data[item + '/key'].access_mode;
2166 key.options = (key.name.substring(0, 7) === "Options");
2168 if (odb.key.length <= n) {
2169 key.expanded = (key.num_values > 1 && key.num_values <= 10);
2171 key.expanded = false;
2174 if (odb.key[n].subdir_open) {
2175 key.subdir_open = true;
2176 key.value = odb.key[n].value;
2178 if (odb.key[n].expanded !== undefined) {
2179 key.expanded = odb.key[n].expanded;
2185 // if current key is a subdirectory, call us recursively
2186 if (key.type === TID_KEY && key.subdir_open) {
2187 odb.key[n-1].value.picker = odb.picker;
2188 odb_extract(odb.key[n-1].value, dataArray);
2192 // if local data remaining, ODB key must have been deleted, so delete also local data
2193 if (odb.key.length > n) {
2194 odb.key = odb.key.slice(0, n);
2195 odb.skip_yellow = true;
2199function odb_print_all(tb) {
2201 let row = { i:4, nvk:0 };
2203 odb.skip_yellow = true;
2204 odb_print(tb, row, tb.odb);
2207function odb_print(tb, row, odb) {
2209 // first print all directories
2210 if (odb.subdirFirst) {
2211 for (const key of odb.key) {
2212 if (key.type !== TID_KEY)
2215 // Print current key
2216 odb_print_key(tb, row, odb.path, key, odb.level);
2219 // Print whole subdirectory if open
2220 if (key.subdir_open) {
2222 // Propagate flags to all subkeys
2223 key.value.skip_yellow = odb.skip_yellow;
2224 key.value.subdirFirst = odb.subdirFirst;
2226 // Print whole subdirectory
2227 odb_print(tb, row, key.value);
2232 // now print all keys
2233 for (const key of odb.key) {
2235 if (odb.subdirFirst)
2236 if (key.type === TID_KEY)
2239 // search for "Options <key>"
2240 let options = odb.key.find(o => o.name === 'Options ' + key.name);
2242 // Print current key
2243 odb_print_key(tb, row, odb.path, key, odb.level, options ? options.value : undefined);
2248 // Hide 'value' if only directories are listed
2249 let ds = document.getElementsByName('valueHeader');
2252 d.style.display = row.nvk > 0 ? 'table-cell' : 'none';
2254 // At the end, remove old rows if subdirectory has been closed
2255 if (odb.level === 0)
2256 while (tb.childNodes.length > row.i)
2257 tb.removeChild(tb.childNodes[tb.childNodes.length-1]);
2260function find_input_element(e) {
2261 if (e === undefined)
2263 for (const c of e.childNodes) {
2264 if (c.tagName === 'INPUT')
2266 if (c.tagName === 'SELECT' && e.inEdit)
2268 if (c.childNodes.length > 0)
2269 if (find_input_element(c))
2275function bnToHex(val) {
2278 hex = ((1n << 64n) + BigInt(val)).toString(16).toUpperCase();
2279 hex = hex.padStart(16, '0');
2281 hex = val.toString(16).toUpperCase();
2282 if (hex.length % 2) {
2290function odb_print_key(tb, row, path, key, level, options) {
2294 let keyPath = (path === '/' ? '/' + key.name : path + '/' + key.name);
2297 let tr = document.createElement('TR');
2300 let td = document.createElement('TD');
2301 td.setAttribute('name', 'odbHandle');
2302 td.style.display = odb.handleColumn ? 'table-cell' : 'none';
2303 td.style.width = "10px";
2305 td.innerHTML = '<img style="cursor: all-scroll; height: 13px; padding: 0; border: 0" src="icons/menu.svg">';
2307 td.childNodes[0].setAttribute('draggable', false);
2310 td = document.createElement('TD');
2313 // Add three spaces of indent for each level
2314 let indent = "<span>";
2315 for (let i = 0; i < level; i++)
2316 indent += " ";
2317 indent += "</span>";
2319 // Remember indent level for key rename
2320 tr.level = odb.level;
2322 // Remember key (need to toggle expanded flag)
2325 // Remember key path
2326 tr.odbPath = keyPath;
2328 // Set special class for "Options ..." keys
2329 if (key.options && !odb.detailsColumn)
2330 tr.style.display = 'none';
2332 // Print subdir with open subdir handler
2333 if (key.type === TID_KEY) {
2334 if (odb.handleColumn) {
2335 // do not show any links in re-order mode
2336 td.innerHTML = indent + " \u25B8 " + escapeHTML(key.name);
2338 let handler = "onclick=\"subdir_open_click(event, this);return false;\" ";
2340 if (key.subdir_open)
2341 td.innerHTML = indent + "<a href='#' " +
2343 "> \u25BE </a>";
2345 td.innerHTML = indent + "<a href='#' " +
2347 "> \u25B8 </a>";
2349 handler = "onclick=\"subdir_goto_click(event, this);return false;\" ";
2350 td.innerHTML += "<a href='#' " + handler + ">" + escapeHTML(key.name) + "</a>";
2355 td.innerHTML += ' → <span>' + key.link + '</span>';
2357 td.innerHTML += ' → <span>'+ '<a href="#" ' +
2358 'onclick="inline_edit(event, this.parentNode, \'' +
2359 key.link + '\', odb_setlink, undefined, \'' + keyPath +
2360 '\'); return false;" ' +
2361 ' title="Change value">' + key.link +
2365 } else if (key.link === undefined) {
2366 td.innerHTML = indent + '<span>' + escapeHTML(key.name) + '</span>';
2368 td.innerHTML = indent + '<span>' + escapeHTML(key.name) + '</span>';
2370 td.innerHTML += ' → <span>' + escapeHTML(key.link) + '</span>';
2372 td.innerHTML += ' → <span>'+ '<a href="#" ' +
2373 'onclick="inline_edit(event, this.parentNode, \'' +
2374 key.link + '\', odb_setlink, undefined, \'' + keyPath +
2375 '\'); return false;" ' +
2376 ' title="Change value">' + escapeHTML(key.link) +
2380 // Subdirs occupy all 8 columns
2381 if (key.type === TID_KEY) {
2384 if (tb.childNodes.length > row.i) {
2385 let selected = tb.childNodes[row.i].odbSelected;
2387 tr.style.backgroundColor = '#004CBD';
2388 change_color(tr, '#FFFFFF');
2391 if (!tb.childNodes[row.i].isEqualNode(tr)) {
2392 // check for edit mode
2393 let inlineEdit = find_input_element(tb.childNodes[row.i]);
2395 if (tb.childNodes[row.i].innerHTML != tr.innerHTML)
2396 tb.childNodes[row.i].innerHTML = tr.innerHTML;
2402 // Remove and install mouse click handlers
2403 if (tb.childNodes[row.i].childNodes[2] !== undefined) {
2404 tb.childNodes[row.i].childNodes[2].removeEventListener('mousedown', select_array_element);
2405 tb.childNodes[row.i].childNodes[2].removeEventListener('mousemove', select_array_element);
2407 tb.childNodes[row.i].addEventListener('mousedown', select_key);
2408 tb.childNodes[row.i].addEventListener('mousemove', select_key);
2410 tb.childNodes[row.i].addEventListener('contextmenu', context_menu);
2412 // Remember ODB path in row
2413 tb.childNodes[row.i].odbPath = keyPath;
2414 tb.childNodes[row.i].key = key;
2416 // Remove and odbOptions class or invisibility
2417 tb.childNodes[row.i].className = "";
2418 tb.childNodes[row.i].style.display = 'table-row';
2424 td = document.createElement('TD');
2427 if (Array.isArray(key.value)) {
2430 '<div style="display: inline;"></div>';
2432 if (key.type === TID_BOOL) {
2434 '<div style="display: inline;">' +
2435 '<select title="Set array elements to same value" ' +
2436 'onchange="odb_setall_key(this, \'' + keyPath + '\', this.value);">' +
2437 '<option value=""></option>' +
2438 '<option value="0">No</option>' +
2439 '<option value="1">Yes</option>' +
2444 '<div style="display: inline;"><a href="#" ' +
2445 'onclick="inline_edit(event, this.parentNode, \'' +
2446 key.value[0] + '\', odb_setall, 10, \'' + keyPath +
2447 '\');return false;" ' +
2448 'title="Set array elements to same value">*</a></div>';
2451 if (key.expanded === false)
2453 '<div style="display: inline;float: right">' +
2455 'onclick="toggle_expanded(this.parentNode);return false;" ' +
2456 'title="Show array elements">\u25B8</a>' +
2460 '<div style="display: inline;float: right">' +
2462 'onclick="toggle_expanded(this.parentNode);return false;" ' +
2463 'title="Hide array elements">\u25BE</a>' +
2467 if (key.type === TID_BOOL) {
2469 let h = "<select onchange='odb_setoption(this)' " +
2470 " onfocus='option_edit(event, this, true)'" +
2471 " onblur='option_edit(event, this, false)'" +
2474 if (key.value === false) {
2475 h += "<option selected value='" + 0 + "'>No</option>";
2476 h += "<option value='" + 1 + "'>Yes</option>";
2478 h += "<option value='" + 0 + "'>No</option>";
2479 h += "<option selected value='" + 1 + "'>Yes</option>";
2484 } else if (options) {
2486 let oc = options.slice(); // make copy of all elements
2488 let h = "<select onchange='odb_setoption(this)' " +
2489 " onfocus='option_edit(event, this, true)'" +
2490 " onblur='option_edit(event, this, false)'" +
2493 // check if current value is in options, add it if not
2494 if (!oc.includes(key.value))
2495 oc.unshift(key.value);
2497 for (const o of oc) {
2498 if (key.value === o)
2499 h += "<option selected value='" + o + "'>" + o + "</option>";
2501 h += "<option value='" + o + "'>" + o + "</option>";
2508 let edit = '<a href="#" onclick="ODBInlineEdit(this.parentNode, \'' + keyPath + '\');return false;" ' +
2509 'onfocus="ODBInlineEdit(this.parentNode, \'' + keyPath + '\')" title="Change value">';
2511 let v = escapeHTML(key.value.toString());
2512 if (key.type === TID_STRING && v === "")
2514 else if (key.type === TID_STRING && v.trim() === "")
2516 else if (key.type === TID_LINK)
2517 v = '<span style="color:red;background-color:yellow">(cannot resolve link)</span>';
2518 else if (v.substring(0, 2) === "0x") {
2519 v = "0x" + v.substring(2).toUpperCase();
2520 if (key.type === TID_UINT64)
2521 v += " (" + BigInt(key.value).toString() + ')';
2523 v += " (" + parseInt(key.value) + ')';
2524 } else if (key.type !== TID_STRING && key.type !== TID_LINK && key.type !== TID_FLOAT && key.type !== TID_DOUBLE) {
2525 if (key.type === TID_INT8)
2526 v += " (0x" + (key.value & 0xFF).toString(16).toUpperCase() + ')';
2527 else if (key.type === TID_INT16)
2528 v += " (0x" + (key.value & 0xFFFF).toString(16).toUpperCase() + ')';
2529 else if (key.type === TID_INT32)
2530 v += " (0x" + (key.value >>> 0).toString(16).toUpperCase() + ')';
2531 else if (key.type === TID_INT64)
2532 v += " (0x" + bnToHex(BigInt(key.value)) + ')';
2534 v += " (0x" + key.value.toString(16).toUpperCase() + ')';
2536 if (odb.picker || odb.handleColumn)
2539 td.innerHTML = edit + v + '</a>';
2544 td = document.createElement('TD');
2545 td.setAttribute('name', 'odbExt');
2546 td.style.display = odb.detailsColumn ? 'table-cell' : 'none';
2548 td.appendChild(document.createTextNode(tid_name[key.type]));
2551 td = document.createElement('TD');
2552 td.setAttribute('name', 'odbExt');
2553 td.style.display = odb.detailsColumn ? 'table-cell' : 'none';
2555 td.innerHTML = '<a href="#" onclick="resize_array(event, \''+keyPath+'\','+key.num_values+');return false;">'
2556 + key.num_values + '</a>';
2559 td = document.createElement('TD');
2560 td.setAttribute('name', 'odbExt');
2561 td.style.display = odb.detailsColumn ? 'table-cell' : 'none';
2563 if (key.type === TID_STRING)
2564 td.innerHTML = '<a href="#" onclick="resize_string(event, \''+keyPath+'\','+key.item_size+','+key.num_values+');return false;">'
2565 + key.item_size + '</a>';
2567 td.innerHTML = key.item_size;
2570 td = document.createElement('TD');
2571 td.setAttribute('name', 'odbExt');
2572 td.style.display = odb.detailsColumn ? 'table-cell' : 'none';
2574 let s = Math.floor(0.5 + new Date().getTime() / 1000 - key.last_written);
2578 else if (s < 60 * 60)
2579 t = Math.floor(0.5 + s / 60) + 'm';
2580 else if (s < 60 * 60 * 24)
2581 t = Math.floor(0.5 + s / 60 / 60) + 'h';
2582 else if (s < 60 * 60 * 24 * 99)
2583 t = Math.floor(0.5 + s / 60 / 60 / 24) + 'd';
2586 td.appendChild(document.createTextNode(t));
2589 td = document.createElement('TD');
2590 td.setAttribute('name', 'odbExt');
2591 td.style.display = odb.detailsColumn ? 'table-cell' : 'none';
2593 let mode = key.access_mode;
2595 if (mode & MODE_READ)
2597 if (mode & MODE_WRITE)
2599 if (mode & MODE_DELETE)
2601 if (mode & MODE_EXCLUSIVE)
2603 if (mode & MODE_WATCH)
2605 td.appendChild(document.createTextNode(m));
2607 // invert color if selected
2608 if (tb.childNodes.length > row.i) {
2609 let selected = tb.childNodes[row.i].odbSelected;
2611 tr.style.backgroundColor = '#004CBD';
2612 change_color(tr, '#FFFFFF');
2616 if (row.i >= tb.childNodes.length) {
2617 // append new row if nothing exists
2620 // replace current row if it differs
2621 if (!tb.childNodes[row.i].isEqualNode(tr)) {
2622 if (tb.childNodes[row.i].childNodes[1].colSpan !== 1)
2623 tb.childNodes[row.i].childNodes[1].colSpan = "1";
2624 for (let i = 0; i < 8; i++) {
2625 let yellowBg = false;
2627 // check for data change in value column
2629 let oldElem = tb.childNodes[row.i].childNodes[i];
2630 if (oldElem === undefined)
2633 while (oldElem.childNodes[0] !== undefined &&
2634 (oldElem.tagName === 'TD' || oldElem.tagName === 'A' || oldElem.tagName === 'DIV'))
2635 oldElem = oldElem.childNodes[0]; // get into <td> <a> <div> elements
2636 oldElem = oldElem.parentNode.innerHTML;
2638 let newElem = tr.childNodes[i];
2639 if (newElem === undefined)
2642 while (newElem.childNodes[0] !== undefined &&
2643 (newElem.tagName === 'TD' || newElem.tagName === 'A' || newElem.tagName === 'DIV'))
2644 newElem = newElem.childNodes[0]; // get into <td> <a> <div> elements
2645 newElem = newElem.parentNode.innerHTML;
2648 if (oldElem !== newElem && // value changed
2649 !odb.skip_yellow && // skip if globally disabled
2650 // skip if edit just finished
2651 !(oldElem.indexOf('(') == -1 && newElem.indexOf('(') !== -1) &&
2652 // skip '*' of arrays
2653 newElem.indexOf('>*</a>') === -1)
2659 // check for edit mode
2660 let inlineEdit = find_input_element(tb.childNodes[row.i].childNodes[i]);
2662 if (tb.childNodes[row.i].childNodes[i] === undefined)
2663 tb.childNodes[row.i].appendChild(tr.childNodes[i].cloneNode(true));
2664 else if (!inlineEdit) {
2665 let e = tb.childNodes[row.i].childNodes[i];
2667 if (tb.childNodes[row.i].childNodes[i].innerHTML !== tr.childNodes[i].innerHTML)
2668 tb.childNodes[row.i].childNodes[i].innerHTML = tr.childNodes[i].innerHTML;
2671 e.style.backgroundColor = 'var(--myellow)';
2672 e.style.setProperty("-webkit-transition", "", "");
2673 e.style.setProperty("transition", "", "");
2674 e.age = new Date() / 1000;
2677 if (e.age !== undefined && new Date() / 1000 > e.age + 1) {
2678 e.style.setProperty("-webkit-transition", "background-color 1s", "");
2679 e.style.setProperty("transition", "background-color 1s", "");
2680 e.style.backgroundColor = "";
2687 // Install mouse click handler
2688 if (tb.childNodes[row.i].childNodes[2] !== undefined) {
2689 tb.childNodes[row.i].childNodes[2].removeEventListener('mousedown', select_array_element);
2690 tb.childNodes[row.i].childNodes[2].removeEventListener('mousemove', select_array_element);
2692 tb.childNodes[row.i].addEventListener('mousedown', select_key);
2693 tb.childNodes[row.i].addEventListener('mousemove', select_key);
2695 tb.childNodes[row.i].addEventListener('contextmenu', context_menu);
2697 // Remember ODB path and key in row
2698 tb.childNodes[row.i].odbPath = keyPath;
2699 tb.childNodes[row.i].key = key;
2701 // Copy visibility for odbOptions
2702 tb.childNodes[row.i].style.display = tr.style.display;
2704 // Print array values
2705 if (Array.isArray(key.value)) {
2706 if (key.expanded === false) {
2708 } else for (let i=0 ; i<key.value.length ; i++) {
2711 // return if in edit mode
2712 if (tb.childNodes[row.i] !== undefined &&
2713 tb.childNodes[row.i].childNodes[2] !== undefined &&
2714 tb.childNodes[row.i].childNodes[2].inEdit)
2718 let tr = document.createElement('TR');
2721 tr.odbPath = keyPath + '[' + i + ']';
2723 // hide option keys if not in detailed column mode
2724 if (key.options && !odb.detailsColumn)
2725 tr.style.display = 'none';
2727 // Handle column (empty for array values)
2728 let td = document.createElement('TD');
2729 td.setAttribute('name', 'odbHandle');
2730 td.style.display = odb.handleColumn ? 'table-cell' : 'none';
2731 td.style.width = "10px";
2735 td = document.createElement('TD');
2739 td = document.createElement('TD');
2741 let p = (path === '/' ? '/' + key.name : path + '/' + key.name);
2744 if (key.type === TID_BOOL) {
2746 let h = "<select onchange='odb_setoption(this," + i + ")' " +
2747 " onfocus='option_edit(event, this, true)'" +
2748 " onblur='option_edit(event, this, false)'" +
2751 if (key.value[i] === false) {
2752 h += "<option selected value='" + 0 + "'>No</option>";
2753 h += "<option value='" + 1 + "'>Yes</option>";
2755 h += "<option value='" + 0 + "'>No</option>";
2756 h += "<option selected value='" + 1 + "'>Yes</option>";
2759 td.innerHTML = "[" + i + "]" + h;
2761 } else if (options) {
2763 let oc = options.slice(); // make copy of all elements
2765 let h = "<select onchange='odb_setoption(this," + i + ")' " +
2766 " onfocus='option_edit(event, this, true)'" +
2767 " onblur='option_edit(event, this, false)'" +
2770 // check if current value is in options, add it if not
2771 if (!oc.includes(key.value[i]))
2772 oc.unshift(key.value[i]);
2774 for (const o of oc) {
2775 if (key.value[i] === o)
2776 h += "<option selected value='" + o + "'>" + o + "</option>";
2778 h += "<option value='" + o + "'>" + o + "</option>";
2784 let edit = '<a href="#" onclick="ODBInlineEdit(this.parentNode, \'' + p + '\');return false;" ' +
2785 'onfocus="ODBInlineEdit(this.parentNode, \'' + p + '\')" title="Change array element">';
2787 let v = escapeHTML(key.value[i].toString());
2788 if (key.type === TID_STRING && v === "")
2790 else if (key.type === TID_STRING && v.trim() === "")
2792 else if (key.type === TID_LINK)
2793 v = '<span style="color:red;background-color:yellow">(cannot resolve link)</span>';
2794 else if (v.substring(0, 2) === "0x") {
2795 v = "0x" + v.substring(2).toUpperCase();
2796 if (key.type === TID_UINT64)
2797 v += " (" + BigInt(key.value[i]).toString() + ')';
2799 v += " (" + parseInt(key.value[i]) + ')';
2800 } else if (key.type !== TID_STRING && key.type !== TID_LINK && key.type !== TID_FLOAT && key.type !== TID_DOUBLE) {
2801 if (key.type === TID_INT8)
2802 v += " (0x" + (key.value[i] & 0xFF).toString(16).toUpperCase() + ')';
2803 else if (key.type === TID_INT16)
2804 v += " (0x" + (key.value[i] & 0xFFFF).toString(16).toUpperCase() + ')';
2805 else if (key.type === TID_INT32)
2806 v += " (0x" + (key.value[i] & 0xFFFFFFFF).toString(16).toUpperCase() + ')';
2807 else if (key.type === TID_INT64)
2808 v += " (0x" + bnToHex(BigInt(key.value[i])) + ')';
2810 v += " (0x" + key.value[i].toString(16).toUpperCase() + ')';
2814 td.innerHTML = '[' + i + '] ' + v;
2815 else if (odb.handlecolumn)
2818 td.innerHTML = '[' + i + '] ' + edit + v + '</a>';
2822 for (let i=0 ; i<5 ; i++) {
2823 td = document.createElement('TD');
2824 td.setAttribute('name', 'odbExt');
2825 td.style.display = odb.detailsColumn ? 'table-cell' : 'none';
2829 if (row.i >= tb.childNodes.length) {
2832 if (!tb.childNodes[row.i].isEqualNode(tr)) {
2833 if (tr.childNodes.length === 1) { // Subdir
2834 tb.childNodes[row.i].replaceWith(tr);
2835 } else if (tr.childNodes.length === 8) { // Key
2836 let odbSelected = tb.childNodes[row.i].odbSelected;
2837 let odbLastSelected = tb.childNodes[row.i].odbLastSelected;
2838 if (tb.childNodes[row.i].childNodes[1].colSpan !== 1)
2839 tb.childNodes[row.i].childNodes[1].colSpan = "1";
2840 for (let i = 0; i < 8; i++) {
2841 let changed = false;
2846 let select = (tr.childNodes[2].childNodes[0] &&
2847 tr.childNodes[2].childNodes[0].tagName == 'SELECT');
2849 if (tb.childNodes[row.i].childNodes[2] !== undefined) {
2851 oldValue = tb.childNodes[row.i].childNodes[2].childNodes[0].value;
2852 else if (tb.childNodes[row.i].childNodes[2].childNodes[1] !== undefined)
2853 oldValue = tb.childNodes[row.i].childNodes[2].childNodes[1].innerHTML;
2854 else if (tb.childNodes[row.i].childNodes[2].childNodes[0] !== undefined)
2855 oldValue = tb.childNodes[row.i].childNodes[2].childNodes[0].innerHTML
2857 oldValue = tb.childNodes[row.i].childNodes[2].innerHTML;
2860 newValue = tr.childNodes[2].childNodes[0].value;
2862 if (tr.childNodes[2].childNodes[1] === undefined)
2863 newValue = tr.childNodes[2].childNodes[0].data;
2865 newValue = tr.childNodes[2].childNodes[1].innerHTML;
2869 if (oldValue !== undefined &&
2870 oldValue !== newValue && // value changed
2871 i === 2 && // we are in value column
2872 !odb.skip_yellow && // skip if globally disabled
2873 // skip if edit just finished
2874 !(oldValue.indexOf('(') == -1 && newValue.indexOf('(') !== -1) &&
2875 // skip '*' of arrays
2876 newValue.indexOf('>*</a>') === -1 &&
2877 tb.childNodes[row.i].odbSelected !== true)
2880 if (tb.childNodes[row.i].childNodes[i] === undefined)
2881 tb.childNodes[row.i].appendChild(tr.childNodes[i].cloneNode(true));
2883 // preserve color if key is selected
2884 let c = tb.childNodes[row.i].childNodes[i].style.color;
2885 tb.childNodes[row.i].childNodes[i].innerHTML = tr.childNodes[i].innerHTML;
2886 change_color(tb.childNodes[row.i].childNodes[i], c);
2889 let e = tb.childNodes[row.i].childNodes[i];
2891 e.style.backgroundColor = 'var(--myellow)';
2892 e.style.setProperty("-webkit-transition", "", "");
2893 e.style.setProperty("transition", "", "");
2894 e.age = new Date() / 1000;
2897 if (e.age !== undefined && new Date() / 1000 > e.age + 1) {
2898 e.style.setProperty("-webkit-transition", "background-color 1s", "");
2899 e.style.setProperty("transition", "background-color 1s", "");
2900 e.style.backgroundColor = "";
2902 if (e.age !== undefined && new Date() / 1000 > e.age + 2) {
2903 e.style.cssText = "";
2904 e.removeAttribute("style");
2909 tb.childNodes[row.i].odbSelected = odbSelected;
2910 tb.childNodes[row.i].odbLastSelected = odbLastSelected;
2915 // remove/install mouse click handlers
2916 tb.childNodes[row.i].removeEventListener('mousedown', select_key);
2917 tb.childNodes[row.i].removeEventListener('mousemove', select_key);
2919 tb.childNodes[row.i].removeEventListener('contextmenu', context_menu);
2921 tb.childNodes[row.i].childNodes[2].addEventListener('mousedown', select_array_element);
2922 tb.childNodes[row.i].childNodes[2].addEventListener('mousemove', select_array_element);
2924 // catch all mouseup events to stop dragging
2925 document.addEventListener('mouseup', mouseStopDragging);
2926 tb.childNodes[row.i].odbPath = tr.odbPath;
2927 tb.childNodes[row.i].key = undefined;
2929 tb.childNodes[row.i].style.display = tr.style.display;
2935function resize_array(event, path, n) {
2936 event.stopPropagation(); // do not select row
2937 dlgQuery("Enter size of array:", n, do_resize_array, path);
2940function do_resize_array(n, path) {
2941 mjsonrpc_db_resize([path],[parseInt(n)]).then(rpc => {
2942 }).catch(error => mjsonrpc_error_alert(error));
2945function resize_string(event, path, size, num_values) {
2946 event.stopPropagation(); // do not select row
2947 dlgQuery("Enter new string size:", size, do_resize_string, { "path": path, "num_values": num_values });
2950function do_resize_string(size, p) {
2951 mjsonrpc_call("db_resize_string",
2952 { "paths": [p.path],
2953 "new_lengths": [parseInt(p.num_values)],
2954 "new_string_lengths": [parseInt(size)]}).then(rpc => {
2955 }).catch(error => mjsonrpc_error_alert(error));
2958function dlgCreateKeyDown(event, inp) {
2959 let keyCode = ('which' in event) ? event.which : event.keyCode;
2961 if (keyCode === 27) {
2963 dlgHide('dlgCreate');
2967 if (keyCode === 13) {
2969 if (do_new_key(event.target))
2970 dlgHide('dlgCreate');
2977function dlgCreateLinkKeyDown(event, inp) {
2978 let keyCode = ('which' in event) ? event.which : event.keyCode;
2980 if (keyCode === 27) {
2982 dlgHide('dlgCreateLink');
2986 if (keyCode === 13) {
2988 if (do_new_link(this))
2989 dlgHide('dlgCreateLink');
2996function drag_start(event) {
2997 let tr = event.target;
2998 while (tr.tagName !== 'TR')
3000 let tb = getOdbTb(tr);
3002 tb.dragTargetRow = Array.from(tb.children).indexOf(tr);
3003 tb.dragSourceRow = tb.dragTargetRow;
3004 tb.dragSource = tr.odbPath;
3005 tb.dragRowContent = tr.cloneNode(true);
3007 window.setTimeout(() => {
3008 window.clearTimeout(tb.odb.updateTimer);
3009 let w = window.getComputedStyle(tr).width;
3010 let h = window.getComputedStyle(tr).height;
3011 tr.innerHTML = '<td colspan="8" style="background-color:white; border: 2px dashed #6bb28c"></td>';
3012 tr.style.height = parseInt(h) + "px";
3013 tr.style.width = parseInt(w) + "px";
3018function drag_move(event, td) {
3019 event.preventDefault();
3021 let tr = event.target;
3022 while (tr.tagName !== 'TR')
3024 let tb = getOdbTb(tr);
3025 let children = Array.from(tb.children);
3027 if (children.indexOf(tr) > tb.dragTargetRow)
3028 tr.after(tb.childNodes[tb.dragTargetRow]);
3029 else if (children.indexOf(tr) < tb.dragTargetRow)
3030 tr.before(tb.childNodes[tb.dragTargetRow]);
3032 tb.dragTargetRow = children.indexOf(tr);
3035function drag_end(event, td) {
3036 let tr = event.target;
3037 while (tr.tagName !== 'TR')
3039 let tb = getOdbTb(tr);
3041 let ttr = tb.childNodes[tb.dragTargetRow];
3042 ttr.innerHTML = tb.dragRowContent.innerHTML;
3043 ttr.style.height = "";
3044 ttr.style.width = "";
3046 if (tb.dragSourceRow !== tb.dragTargetRow) {
3047 mjsonrpc_call("db_reorder", {"paths": [tb.dragSource], "indices": [tb.dragTargetRow - 4]}).then(rpc => {
3048 tb.odb.updateTimer = window.setTimeout(odb_update, 10, tb);
3049 }).catch(error => mjsonrpc_error_alert(error));
3051 tb.odb.updateTimer = window.setTimeout(odb_update, 10, tb);
3054function show_open_records(odb) {
3055 let title = "ODB open records under \"" + odb.path + "\"";
3057 let d = document.createElement("div");
3058 d.className = "dlgFrame";
3059 d.style.zIndex = "30";
3060 d.style.minWidth = "400px";
3061 d.shouldDestroy = true;
3063 d.innerHTML = "<div class=\"dlgTitlebar\" id=\"dlgMessageTitle\">" + title + "</div>" +
3064 "<div class=\"dlgPanel\" style=\"padding: 2px;\">" +
3065 "<div id=\"dlgSOR\"></div>" +
3066 "<button class=\"dlgButton\" id=\"dlgMessageButton\" +" +
3067 "type=\"button\" " +
3068 " onClick=\"dlgMessageDestroy(this)\">Close</button>" +
3071 document.body.appendChild(d);
3073 update_open_records(odb);
3077function update_open_records(odb) {
3078 let path = odb.path;
3079 mjsonrpc_call("db_sor", {"path": path}).then (rpc => {
3080 let sor = rpc.result.sor;
3082 for (const s of sor) {
3084 paths[s.path] += ', ' + s.name;
3086 paths[s.path] = s.name;
3088 let sorted_paths = Object.keys(paths).sort();
3089 let html = '<table class="mtable" style="width: 100%;margin-top: 0;margin-bottom:0"><tbody>';
3090 html += '<tr><th>ODB Path</th><th>Open by</th></tr>';
3091 for (const p of sorted_paths)
3092 html += '<tr><td style="padding: 4px">' + p + '</td>' +
3093 '<td style="padding: 4px">' + paths[p] + '</td></tr>';
3094 html += '</tbody></table>';
3096 let d = document.getElementById('dlgSOR');
3098 return; // dialog has been closed
3099 if (d.innerHTML !== html) {
3101 dlgCenter(d.parentElement.parentElement);
3103 window.setTimeout(update_open_records, 1000, odb);
3105 }).catch( error => mjsonrpc_error_alert(error));
3108function show_open_clients(e) {
3109 let odb = getOdbTb(e).odb;
3110 let title = "ODB clients";
3112 let d = document.createElement("div");
3113 d.className = "dlgFrame";
3114 d.style.zIndex = "30";
3115 d.style.minWidth = "400px";
3116 d.shouldDestroy = true;
3118 d.innerHTML = "<div class=\"dlgTitlebar\" id=\"dlgMessageTitle\">" + title + "</div>" +
3119 "<div class=\"dlgPanel\" style=\"padding: 2px;\">" +
3120 "<div id=\"dlgSCL\"></div>" +
3121 "<button class=\"dlgButton\" id=\"dlgMessageButton\" +" +
3122 "type=\"button\" " +
3123 " onClick=\"dlgMessageDestroy(this)\">Close</button>" +
3126 document.body.appendChild(d);
3128 update_open_clients(odb);
3132function update_open_clients(odb) {
3133 let path = odb.path;
3134 mjsonrpc_call("db_scl").then (rpc => {
3135 let scl = rpc.result.scl.clients;
3137 let html = '<table class="mtable" style="width: 100%;margin-top: 0;margin-bottom:0"><tbody>';
3138 html += '<tr><th>Name</th><th>Host</th><th>Slot</th><th>PID</th><th>Timeout</th><th>Last active [ms]</th></tr>';
3139 for (const s of scl) {
3140 html += '<tr><td style="padding: 4px"><a href="?cmd=odb&odb_path=/System/Clients/"'
3141 + s.pid + '">' + s.name + '</td>';
3142 html += '<td style="padding: 4px">'+ s.host + '</td>';
3143 html += '<td style="padding: 4px">'+ s.slot + '</td>';
3144 html += '<td style="padding: 4px">'+ s.pid + '</td>';
3145 html += '<td style="padding: 4px">'+ s.watchdog_timeout_millisec + '</td>';
3146 html += '<td style="padding: 4px">'+ s.last_activity_millisec + '</td>';
3149 html += '</tbody></table>';
3151 let d = document.getElementById('dlgSCL');
3153 return; // dialog has been closed
3154 if (d.innerHTML !== html) {
3156 dlgCenter(d.parentElement.parentElement);
3158 window.setTimeout(update_open_clients, 1000, odb);
3160 }).catch( error => mjsonrpc_error_alert(error));