MIDAS
Loading...
Searching...
No Matches
odbbrowser.js
Go to the documentation of this file.
1/*
2
3 Full ODB browser to be used by odb.html as the standard
4 ODB browser or as a key picker with the function
5
6 odb_picker('/', (flag,path) => {
7 if (flag)
8 console.log(path);
9 });
10
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
15 with its full path.
16
17 Created by Stefan Ritt on July 21st, 2022
18
19*/
20
21let odb_css = `<style>
22 .odbBrowser .colHeader {
23 font-weight: bold;
24 }
25 .odbBrowser .mtable {
26 border-spacing: 0;
27 }
28 .odbBrowser td, th {
29 padding: 3px;
30 user-select: none;
31 -webkit-user-select: none;
32 }
33 .odbBrowser img {
34 cursor: pointer;
35 border: 5px solid #E0E0E0;
36 border-radius: 5px;
37 }
38 .odbBrowser img:hover {
39 border: 5px solid #C8C8C8;
40 }
41 </style>`;
42
43let odb_dialogs = `
44
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;">
50 <tr>
51 <td style="text-align: right">Directory:</td>
52 <td style="text-align: left" id="odbCreateDir"></td>
53 </tr>
54 <tr>
55 <td style="text-align: right">Type:</td>
56 <td style="text-align: left">
57 <select id="odbCreateType"
58 onchange="
59 document.getElementById('odbCreateStrLenTR').style.visibility =
60 (this.value==='12') ? 'visible' : 'hidden';
61 ">
62 <option selected value=7>Integer (32-bit)
63 <option value=9>Float (4 Bytes)
64 <option value=12>String
65 <option value=1>Byte
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)
75 </select>
76 </td>
77 </tr>
78 <tr>
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>
82 </tr>
83 <tr>
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>
86 </tr>
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>
90 </tr>
91 </table>
92 <button class="dlgButtonDefault" onclick="if(do_new_key(this))dlgHide('dlgCreate');">Create</button>
93 <button class="dlgButton" onclick="dlgHide('dlgCreate')">Cancel</button>
94 </div>
95</div>
96
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;">
102 <tr>
103 <td style="text-align: right">Directory:</td>
104 <td style="text-align: left" id="odbCreateLinkDir"></td>
105 </tr>
106 <tr>
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>
110 </tr>
111
112 <tr>
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>
119 </td>
120 </tr>
121 </table>
122 <button class="dlgButton" onclick="if(do_new_link(this))dlgHide('dlgCreateLink');">Create</button>
123 <button class="dlgButton" onclick="dlgHide('dlgCreateLink')">Cancel</button>
124 </div>
125</div>
126
127<!-- Select file dialog -->
128<div id="dlgFileSelect" class="dlgFrame">
129 <div class="dlgTitlebar">Select file</div>
130 <div class="dlgPanel">
131 <br />
132 <div style="margin: auto;width:70%">
133 <input type="file" id="fileSelector" accept=".json">
134 </div>
135 <br /><br />
136 <button class="dlgButton" onclick="loadFileFromSelector(this.parentNode.parentNode);dlgHide('dlgFileSelect');">Upload</button>
137 <button class="dlgButton" onclick="dlgHide('dlgFileSelect')">Cancel</button>
138 </div>
139</div>
140`;
141
142function odb_browser(id, path, picker) {
143
144 if (path === undefined) {
145 // get path from URL
146 let url = new URL(window.location.href);
147 path = url.searchParams.get("odb_path");
148 if (path === undefined || path === null || path === "")
149 path = '/';
150 if (path[0] !== '/')
151 path = '/' + path;
152 }
153
154 // add special styles
155 document.head.insertAdjacentHTML("beforeend", odb_css);
156
157 // add special dialogs
158 let d = document.createElement("div");
159 d.innerHTML = odb_dialogs;
160 document.body.appendChild(d);
161
162 if (!picker) {
163 // modify title
164 if (document.title.indexOf("ODB") !== -1) {
165 if (path === '/')
166 document.title = "ODB";
167 else
168 document.title = "ODB " + path;
169 }
170
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"));
175 if (path !== '/')
176 url += "&odb_path=" + encodeURIComponent(path);
177 window.history.pushState({'path': path}, '', url);
178 }
179
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');
187 if (picker) {
188 table.style.width = "100%";
189 table.style.marginTop = "0";
190 table.style.marginBottom = "0";
191 } else
192 table.style.minWidth = "600px";
193
194 table.appendChild(tb);
195
196 // Attach ODB to table body
197 tb.odb = {
198 picker: picker,
199 id: id,
200 updateTimer: 0,
201 path: path,
202 level: 0,
203 dataIndex: 0,
204 subdirFirst: true,
205 handleColumn : false,
206 detailsColumn: false,
207 dragSource: undefined,
208 dragDestination: undefined,
209 dragTargetRow: undefined,
210 dragRowContent : undefined,
211 key: []
212 };
213
214 let tr = document.createElement('TR');
215 tb.appendChild(tr);
216 let th = document.createElement('TH');
217 tr.appendChild(th);
218 th.className = "mtableheader";
219 th.colSpan = "8";
220 th.appendChild(document.createTextNode("Online Database Browser"));
221 if (picker)
222 tr.style.display = 'none';
223
224 // Dynamic ODB path
225 tr = document.createElement('TR');
226 tb.appendChild(tr);
227 let td = document.createElement('TD');
228 td.innerHTML = "";
229 td.colSpan = "8";
230 tr.appendChild(td);
231
232 tr.addEventListener('mousedown', select_key); // used to deselect any kay
233
234 // Toolbar
235 tr = document.createElement('TR');
236 tb.appendChild(tr);
237 td = document.createElement('TD');
238 td.colSpan = "8";
239 tr.appendChild(td);
240 td.innerHTML =
241 '<img src="icons/file-plus.svg" title="Create key CTRL+K" ' +
242 'onmousedown="new_key(this)">&nbsp;&nbsp;' +
243 '<img src="icons/folder-plus.svg" title="Create subdirectory" ' +
244 'onmousedown="new_subdir(this)">&nbsp;&nbsp;' +
245 '<img src="icons/link.svg" title="Create link" ' +
246 'onmousedown="new_link(this)">&nbsp;&nbsp;' +
247 '<img src="icons/edit-3.svg" title="Rename key" ' +
248 'onmousedown="rename_key(this)">&nbsp;&nbsp;' +
249 '<img src="icons/shuffle.svg" title="Reorder keys" ' +
250 'onmousedown="toggle_handles(this)">&nbsp;&nbsp;' +
251 '<img src="icons/copy.svg" title="Copy from ODB Ctrl+C" ' +
252 'onmousedown="odb_copy(this)">&nbsp;&nbsp;' +
253 '<img src="icons/clipboard.svg" title="Paste to ODB Ctrl+V" ' +
254 'onmousedown="odb_paste(this)">&nbsp;&nbsp;' +
255 '<img src="icons/folder-open.svg" title="Load ODB Ctrl+O" ' +
256 'onmousedown="odb_load(this)">&nbsp;&nbsp;' +
257 '<img src="icons/save.svg" title="Save ODBn Ctrl+S" ' +
258 'onmousedown="odb_save_picker(this)">&nbsp;&nbsp;' +
259 '<img src="icons/download.svg" title="Export ODB" ' +
260 'onmousedown="odb_export(this)">&nbsp;&nbsp;' +
261 '<img src="icons/upload.svg" title="Import ODB" ' +
262 'onmousedown="odb_import(this)">&nbsp;&nbsp;' +
263 '<img src="icons/search.svg" title="Search keys" ' +
264 'onmousedown="search_key(this)">&nbsp;&nbsp;' +
265 '<img src="icons/trash-2.svg" title="Delete keys Ctrl+Delete" ' +
266 'onmousedown="odb_delete(this)">&nbsp;&nbsp;' +
267 '<img src="icons/more-vertical.svg" title="More menu commands" ' +
268 'onmousedown="more_menu(event)">&nbsp;&nbsp;' +
269 '';
270
271 tr.addEventListener('mousedown', select_key); // used to deselect any kay
272 if (picker)
273 tr.style.display = 'none';
274
275 // Column header
276 tr = document.createElement('TR');
277 tb.appendChild(tr);
278 let a = [ "Handle", "Key", "Value", "Type", "#Val", "Size", "Written", "Mode" ];
279 for (const t of a) {
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');
288 td.innerHTML = t;
289 } else if (t !== "Key" && t !== "Value") {
290 td.innerHTML = t;
291 td.setAttribute('name', 'odbExt');
292 td.style.display = tb.odb.detailColumn ? 'table-cell' : 'none';
293 } else {
294 td.innerHTML = t;
295 }
296 tr.appendChild(td);
297 }
298
299 if (!picker)
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;'>&#x21E5;</a>"+
303 "</div>";
304
305 tr.childNodes[7].innerHTML +=
306 "&nbsp;<span title='Hide key details' style='display:inline;float:right'>" +
307 "<a id='expRight' href='#' onclick='expand(this);return false;'>&#x21E4;</a>"+
308 "</span>";
309 tr.childNodes[7].style.width = '65px';
310
311 tr.addEventListener('mousedown', select_key); // used to deselect any kay
312
313 d.appendChild(table);
314
315 // install shortcut key handler
316 window.addEventListener('keydown', event => {
317 global_keydown(event, tb);
318 });
319
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);
327 }
328 });
329
330 odb_update(tb);
331
332 // install event handler for browser history popstate events
333 window.addEventListener('popstate', (event) => {
334 if (event.state) {
335 subdir_goto(tb, event.state.path);
336 }
337 });
338
339}
340
341function escapeHTML(text) {
342 let div = document.createElement('div');
343 div.innerText = text;
344 return div.innerHTML;
345}
346
347function global_keydown(event, tb) {
348 if (event.target.tagName === 'INPUT')
349 return true;
350
351 // Ctrl+C
352 if ((event.ctrlKey || event.metaKey) && event.key === 'c') {
353 odb_copy(tb);
354 return false;
355 }
356
357 // Ctrl+V
358 if ((event.ctrlKey || event.metaKey) && event.key === 'v') {
359 odb_paste(tb);
360 return false;
361 }
362
363 // Ctrl+K
364 if ((event.ctrlKey || event.metaKey) && event.key === 'k') {
365 event.preventDefault();
366 new_key(tb);
367 return false;
368 }
369
370 // Ctrl+S
371 if ((event.ctrlKey || event.metaKey) && event.key === 's') {
372 event.preventDefault();
373 odb_save_picker(tb);
374 return false;
375 }
376
377 // Ctrl+O
378 if ((event.ctrlKey || event.metaKey) && event.key === 'o') {
379 event.preventDefault();
380 odb_load(tb);
381 return false;
382 }
383
384 // Ctrl+Backspace
385 if ((event.ctrlKey || event.metaKey) && event.key === 'Backspace') {
386 odb_delete(tb);
387 return false;
388 }
389
390 // Escape
391 if (event.keyCode === 27)
392 return close_submenus();
393
394 return true;
395}
396
397function close_submenus(event) {
398 let flag = false;
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";
404 if (flag)
405 m.style.display = 'none';
406 }
407
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";
413 if (flag)
414 m.style.display = 'none';
415 }
416
417 return flag;
418}
419
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;
428
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>" +
438 "</div>" +
439 "</div>";
440
441 document.body.appendChild(d);
442
443 // remove any trailing '/' from path
444 if (path.length > 2 && path.slice(-1) == '/')
445 path = path.slice(0, -1);
446
447 odb_browser('dlgOdbPicker', path, true);
448
449 dlgShow(d, true);
450 d.style.top = "100px";
451
452 return d;
453}
454
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;
458 if (!path)
459 path = d.childNodes[1].childNodes[0].childNodes[0].childNodes[0].odb.path;
460 d.callback(flag, path);
461 dlgMessageDestroy(e.parentElement);
462}
463
464function getOdbTb(e) {
465 if (e === null)
466 return;
467 while (e.tagName !== 'TBODY') {
468 e = e.parentNode;
469 if (e === null)
470 return;
471 }
472 return e;
473}
474
475
476function inline_edit_keydown(event, p) {
477 let keyCode = ('which' in event) ? event.which : event.keyCode;
478
479 if (keyCode === 27) {
480 // cancel editing
481 p.odbParam.inEdit = false;
482 p.innerHTML = p.odbParam.oldhtml;
483 return false;
484 }
485
486 if (keyCode === 13) {
487 inline_edit_finish(p);
488 return false;
489 }
490
491 return true;
492}
493
494function inline_edit_cancel(p) {
495 // cancel editing
496 if (p.odbParam.inEdit) {
497 p.odbParam.inEdit = false;
498 p.innerHTML = p.odbParam.oldhtml;
499 }
500}
501
502function inline_edit_finish(p) {
503 // finish editing
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);
509}
510
511function inline_edit(event, p, str, callback, size, param) {
512 if (p.odbParam !== undefined && p.odbParam.inEdit)
513 return;
514
515 if (event !== undefined)
516 event.stopPropagation(); // don't propagate to rename_key()
517
518 p.odbParam = {};
519 p.odbParam.param = param;
520 p.odbParam.callback = callback;
521 p.odbParam.oldhtml = p.innerHTML;
522 p.odbParam.inEdit = true;
523
524 if (size === undefined)
525 size = str.length;
526 if (size < 20)
527 size = 20;
528
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);'>";
532
533 // needed for Firefox
534 setTimeout(function () {
535 p.childNodes[0].focus();
536 p.childNodes[0].select();
537 }, 10);
538}
539
540function option_edit(event, e, flag) {
541 event.stopPropagation();
542 e.parentNode.inEdit = flag;
543}
544
545function odb_setoption(p, index) {
546 let tr = p.parentNode;
547 while (tr.tagName !== 'TR')
548 tr = tr.parentNode;
549 let tb = getOdbTb(tr);
550 let path = tr.odbPath;
551 if (index)
552 path += '[' + index + ']';
553 let value = p.value;
554
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=""', '');
560
561 mjsonrpc_db_set_value(path, value).then(() => {
562 tb.odb.skip_yellow = true;
563 p.parentNode.inEdit = false;
564 odb_update(tb);
565 }).catch(error => mjsonrpc_error_alert(error));
566}
567
568function odb_setall(p, value) {
569 // obtain selected array elements
570 let tr = p.parentNode;
571 while (tr.tagName !== 'TR')
572 tr = tr.parentNode;
573 let tb = getOdbTb(tr);
574
575 let atr = getArrayTr(tr);
576
577 let n = 0;
578 if (atr && atr[0]) { // rows could be hidden
579 for (const e of atr)
580 if (e.odbSelected)
581 n++;
582 }
583
584 if (n === 0) {
585 // set all values
586 let path = p.odbParam.param + '[*]';
587 mjsonrpc_db_set_value(path, value).then(() =>
588 odb_update(tb)
589 ).catch(error => mjsonrpc_error_alert(error));
590 } else {
591 let path = p.odbParam.param;
592 let key = p.parentNode.parentNode.key;
593 path = path.substring(0, path.lastIndexOf('/'));
594 path += '/' + key.name + '[';
595 let values = [];
596 for (let i=0 ; i<atr.length ; i++)
597 if (atr[i].odbSelected) {
598 path += i + ',';
599 if (key.type === TID_STRING || key.type === TID_LINK)
600 values.push(value);
601 else
602 values.push(parseFloat(value));
603 }
604 path = path.slice(0, -1) + ']';
605
606 mjsonrpc_db_set_value(path, values).then(() =>
607 odb_update(tb)
608 ).catch(error => mjsonrpc_error_alert(error));
609 }
610}
611
612function odb_setall_key(p, path, value) {
613
614 if (value === "")
615 return;
616
617 // obtain selected array elements
618 let tr = p.parentNode;
619 while (tr.tagName !== 'TR')
620 tr = tr.parentNode;
621 let tb = getOdbTb(tr);
622
623 let atr = getArrayTr(tr);
624
625 let n = 0;
626 if (atr && atr[0]) { // rows could be hidden
627 for (const e of atr)
628 if (e.odbSelected)
629 n++;
630 }
631
632 if (n === 0) {
633 // set all values
634 path += '[*]';
635 mjsonrpc_db_set_value(path, value).then(() =>
636 odb_update(tb)
637 ).catch(error => mjsonrpc_error_alert(error));
638 } else {
639 let key = p.parentNode.parentNode.key;
640 path += '[';
641 let values = [];
642 for (let i=0 ; i<atr.length ; i++)
643 if (atr[i].odbSelected) {
644 path += i + ',';
645 values.push(parseFloat(value));
646 }
647 path = path.slice(0, -1) + ']';
648
649 mjsonrpc_db_set_value(path, values).then(() =>
650 odb_update(tb)
651 ).catch(error => mjsonrpc_error_alert(error));
652 }
653}
654
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); });
658}
659
660function new_key(e) {
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();
666}
667
668function do_new_key(e) {
669 if (e.parentElement.parentElement.param)
670 e = e.parentElement.parentElement.param;
671 else
672 e = e.parentElement.parentElement.parentElement.parentElement.parentElement.parentElement.param;
673
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);
681
682 if (name.length < 1) {
683 dlgAlert("No name specified");
684 return false;
685 }
686
687 if (size < 1) {
688 dlgAlert("Bad array length: " + size);
689 return false;
690 }
691
692 if (strlen < 1) {
693 dlgAlert("Bad string length " + strlen);
694 return false;
695 }
696
697 let param = {};
698 param.path = path + "/" + name;
699 param.type = type;
700 if (size > 1)
701 param.array_length = size;
702 if (strlen > 0)
703 param.string_length = strlen;
704
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");
711 }
712 let tb = getOdbTb(e);
713 odb_update(tb);
714 }).catch(error => mjsonrpc_error_alert(error));
715
716 return true;
717}
718
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();
725}
726
727function pickLinkTarget() {
728 let p = document.getElementById('odbCreateLinkTarget').value;
729 if (p.lastIndexOf('/') > 0)
730 p = p.substring(0, p.lastIndexOf('/'));
731 else
732 p = '/';
733 odb_picker(p, setLinkTarget);
734}
735
736function setLinkTarget(flag, path) {
737 if (flag)
738 document.getElementById('odbCreateLinkTarget').value = path;
739}
740
741function do_new_link(e) {
742 e = e.parentElement.parentElement.param;
743 let tb = getOdbTb(e);
744
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();
749
750 if (name.length < 1) {
751 dlgAlert("No name specified");
752 return false;
753 }
754
755 if (target.length < 1) {
756 dlgAlert("No link target specified");
757 return false;
758 }
759
760 if (target[0] !== '/') {
761 dlgAlert("Link target must be absolute (start with a \"/\")");
762 return false;
763 }
764
765 path = path + "/" + name;
766
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");
773 }
774
775 odb_update(tb);
776 }).catch(error => mjsonrpc_error_alert(error));
777
778 return true;
779}
780
781function new_subdir(e) {
782 dlgQuery('Subdirectory name:', "", do_new_subdir, e);
783}
784
785function do_new_subdir(subdir, e) {
786 if (subdir === false)
787 return;
788
789 // remove any leading or trailing spaces
790 subdir = subdir.trim();
791
792 if (subdir.length < 1) {
793 dlgAlert("No name specified");
794 return;
795 }
796
797 let tb = getOdbTb(e);
798 let path = tb.odb.path;
799 if (path === '/')
800 path = "";
801 let param = {};
802 param.path = path + "/" + subdir;
803 param.type = TID_KEY;
804
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");
811 }
812 odb_update(tb);
813 }).catch(error => mjsonrpc_error_alert(error));
814}
815
816function more_menu(event) {
817
818 event.stopPropagation(); // don't send click to select_key()
819
820 let odb = getOdbTb(event.target).odb;
821
822 let d = document.getElementById('moreMenu');
823 if (d === null) {
824
825 // create menu
826 d = document.createElement("div");
827 d.id = "moreMenu";
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";
835
836 let cm = document.createElement("div");
837
838 // Show open records ----------
839
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);
848 return false;
849 }
850 cm.appendChild(mDiv);
851
852 // Show ODB clients ----------
853
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);
862 return false;
863 }
864 cm.appendChild(mDiv);
865 d.appendChild(cm);
866 document.body.appendChild(d);
867 }
868
869 let rect = event.target.getBoundingClientRect();
870
871 d.style.display = 'block';
872 d.style.left = (rect.left + window.scrollX) + 'px';
873 d.style.top = (rect.bottom + 4 + window.scrollY) + 'px';
874
875 if (parseInt(d.style.left) + d.offsetWidth > document.body.clientWidth)
876 d.style.left = (document.body.clientWidth - d.offsetWidth) + 'px';
877}
878
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);
886}
887
888function unselect_all_keys(tb) {
889 for (let i=4 ; i<tb.childNodes.length ; i++) {
890 let tr = tb.childNodes[i];
891
892 // selected keys
893 tr.odbSelected = false;
894 tr.odbLastSelected = false;
895 tr.style.backgroundColor = '';
896 change_color(tr, '');
897 }
898}
899
900function get_selected_keys(tb) {
901 let paths = [];
902 for (let i=4 ; i<tb.childNodes.length ; i++) {
903 let tr = tb.childNodes[i];
904 if (tr.odbSelected)
905 paths.push({"path": tr.odbPath, "key": tr.key});
906 }
907 return paths;
908}
909
910function unselect_all_array_elements(tb) {
911 for (let i=4 ; i<tb.childNodes.length ; i++) {
912 let tr = tb.childNodes[i];
913
914 // selected array elements
915 if (tr.childNodes.length > 2) {
916 tr.childNodes[2].style.backgroundColor = '';
917 change_color(tr.childNodes[2], '');
918 }
919 }
920}
921
922function select_key(event) {
923
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')
927 return;
928
929 event.preventDefault();
930 event.stopPropagation();
931
932 let tr = event.target;
933 let tb = getOdbTb(tr);
934 if (tb === undefined)
935 return;
936
937 let odb = tb.odb;
938
939 // hide submenu if visible
940 let m = document.getElementById('moreMenu');
941 if (m !== null && m.style.display === 'block')
942 m.style.display = 'none';
943
944 // hide context menu if visible
945 m = document.getElementById('contextMenu');
946 if (m !== null && m.style.display === 'block')
947 m.style.display = 'none';
948
949 // un-select all array elements
950 unselect_all_array_elements(tb);
951
952 // don't select key when we are in edit mode
953 while (tr.tagName !== 'TR') {
954 tr = tr.parentNode;
955 if (tr === null)
956 return;
957 }
958 if (find_input_element(tr))
959 return;
960
961 if (odb.picker) {
962 // unselect all keys
963 for (let i = 4; i < tb.childNodes.length; i++)
964 tb.childNodes[i].odbSelected = false;
965
966 // don't select array values
967 if (tr.childNodes[1] && tr.childNodes[1].innerHTML !== "")
968 tr.odbSelected = true;
969
970 odb.selectedKey = tr.odbPath;
971 } else {
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) {
976 headerRow = true;
977 break;
978 }
979
980 if (event.type === "mousemove") {
981
982 if (event.buttons === 1 && !headerRow) {
983 // search last selected row
984 let i1;
985 for (i1 = 4; i1 < tb.childNodes.length; i1++)
986 if (tb.childNodes[i1].odbLastSelected)
987 break;
988 if (i1 === tb.childNodes.length)
989 i1 = 4; // none selected, so use first one
990
991 let i2;
992 for (i2 = 4; i2 < tb.childNodes.length; i2++)
993 if (tb.childNodes[i2] === tr)
994 break;
995
996 if (i2 < tb.childNodes.length) {
997 if (i1 > i2)
998 [i1, i2] = [i2, i1];
999
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;
1004 }
1005 }
1006 }
1007
1008 } else { // mousedown
1009
1010 if (event.shiftKey && !headerRow) {
1011 // search last selected row
1012 let i1;
1013 for (i1 = 4; i1 < tb.childNodes.length; i1++)
1014 if (tb.childNodes[i1].odbLastSelected)
1015 break;
1016 if (i1 === tb.childNodes.length)
1017 i1 = 4; // none selected, so use first one
1018
1019 let i2;
1020 for (i2 = 4; i2 < tb.childNodes.length; i2++)
1021 if (tb.childNodes[i2] === tr)
1022 break;
1023
1024 if (i2 < tb.childNodes.length) {
1025 if (i1 > i2)
1026 [i1, i2] = [i2, i1];
1027
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;
1032 }
1033 }
1034
1035 } else if ((event.metaKey || event.ctrlKey) && !headerRow) {
1036
1037 // command key just toggles current selection
1038 tr.odbSelected = !tr.odbSelected;
1039
1040 for (let i = 4; i < tb.childNodes.length; i++)
1041 tb.childNodes[i].odbLastSelected = false;
1042 if (tr.odbSelected)
1043 tr.odbLastSelected = true;
1044
1045 } else {
1046
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;
1051 }
1052
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();
1058 }
1059 }
1060 }
1061 }
1062
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];
1066
1067 tr.style.backgroundColor = tr.odbSelected ? '#004CBD' : '';
1068 change_color(tr, tr.odbSelected ? '#FFFFFF' : '');
1069 }
1070}
1071
1072function getArrayTr(tr) {
1073 // collect all rows belonging to "tr" in an array
1074 let tb = getOdbTb(tr);
1075 let atr = [];
1076 let i;
1077 for (i = 0; i < tb.childNodes.length; i++)
1078 if (tb.childNodes[i] === tr)
1079 break;
1080 while (tb.childNodes[i].odbPath.indexOf('[') !== -1)
1081 i--;
1082 i++; // first element
1083 do {
1084 atr.push(tb.childNodes[i++]);
1085 } while (i < tb.childNodes.length && tb.childNodes[i].odbPath.indexOf('[') !== undefined);
1086 return atr;
1087}
1088
1089let mouseDragged = false;
1090
1091function mouseStopDragging() {
1092 mouseDragged = false;
1093}
1094
1095function select_array_element(event) {
1096
1097 if (event.type === 'mousemove' && !mouseDragged)
1098 return;
1099
1100 // keep off secondary mouse buttons
1101 if (event.button !== 0)
1102 return;
1103
1104 // keep off drop-down boxes
1105 if (event.target.tagName === "SELECT")
1106 return;
1107
1108 event.preventDefault();
1109 event.stopPropagation();
1110
1111 let tr = event.target;
1112 let tb = getOdbTb(tr);
1113 if (tb === undefined)
1114 return;
1115
1116 // don't select key when we are in edit mode
1117 while (tr.tagName !== 'TR') {
1118 tr = tr.parentNode;
1119 if (tr === null)
1120 return;
1121 }
1122 if (find_input_element(tr))
1123 return;
1124
1125 let odb = tb.odb;
1126
1127 // hide submenu if visible
1128 let m = document.getElementById('moreMenu');
1129 if (m !== null && m.style.display === 'block')
1130 m.style.display = 'none';
1131
1132 // hide context menu if visible
1133 m = document.getElementById('contextMenu');
1134 if (m !== null && m.style.display === 'block')
1135 m.style.display = 'none';
1136
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], '');
1145 }
1146 }
1147
1148 // create array of all array elements
1149 let atr = getArrayTr(tr);
1150
1151 if (event.type === 'mousedown') {
1152 mouseDragged = true;
1153
1154 if (odb.picker) {
1155 // un-select all but current
1156 for (const tr of atr) {
1157 tr.odbSelected = false;
1158 tr.odbLastSelected = false;
1159 }
1160 tr.odbSelected = true;
1161 tr.odbLastSelected = true;
1162 odb.selectedKey = tr.odbPath;
1163
1164 } else {
1165
1166 if (event.shiftKey) {
1167 // search last selected row
1168 let i1;
1169 for (i1 = 0; i1 < atr.length; i1++)
1170 if (atr[i1].odbLastSelected)
1171 break;
1172 if (i1 === atr.length)
1173 i1 = 0; // non selected, so use first one
1174
1175 let i2;
1176 for (i2 = 0; i2 < atr.length; i2++)
1177 if (atr[i2] === tr)
1178 break;
1179
1180 if (i2 < atr.length) {
1181 if (i1 > i2)
1182 [i1, i2] = [i2, i1];
1183
1184 for (let i = i1; i <= i2; i++)
1185 atr[i].odbSelected = true;
1186 }
1187
1188 } else if (event.metaKey || event.ctrlKey) {
1189
1190 // command key just toggles current selection
1191 tr.odbSelected = !tr.odbSelected;
1192
1193 for (const tr of atr)
1194 tr.odbLastSelected = false;
1195 if (tr.odbSelected)
1196 tr.odbLastSelected = true;
1197
1198 } else {
1199
1200 // no key pressed -> un-select all but current
1201 for (const tr of atr) {
1202 tr.odbSelected = false;
1203 tr.odbLastSelected = false;
1204 }
1205 tr.odbSelected = true;
1206 tr.odbLastSelected = true;
1207 }
1208 }
1209 }
1210
1211 if (event.type === 'mousemove' && mouseDragged) {
1212
1213 if (!event.shiftKey && !event.metaKey && !event.ctrlKey) {
1214 // unselect all
1215 for (const tr of atr)
1216 tr.odbSelected = false;
1217
1218 // search last selected row
1219 for (i1 = 0; i1 < atr.length; i1++)
1220 if (atr[i1].odbLastSelected)
1221 break;
1222 if (i1 === atr.length)
1223 i1 = 0; // non selected, so use first one
1224
1225 let i2;
1226 for (i2 = 0; i2 < atr.length; i2++)
1227 if (atr[i2] === tr)
1228 break;
1229
1230 if (i2 < atr.length) {
1231 if (i1 > i2)
1232 [i1, i2] = [i2, i1];
1233
1234 for (let i = i1; i <= i2; i++)
1235 atr[i].odbSelected = true;
1236 }
1237 }
1238 }
1239
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' : '');
1245 }
1246 }
1247}
1248
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')
1252 return;
1253
1254 event.preventDefault();
1255 event.stopPropagation();
1256
1257 let tr = event.target;
1258 while (tr.tagName !== 'TR')
1259 tr = tr.parentNode;
1260 let tb = getOdbTb(tr);
1261 let odb = tb.odb;
1262 let path = tr.odbPath;
1263
1264 if (!tr.odbSelected)
1265 select_key(event);
1266
1267 let d = document.getElementById('contextMenu');
1268 if (d === null) {
1269 // create menu on the first call
1270
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";
1280
1281 let cm = document.createElement("div");
1282
1283 let menu = ["Copy key", "Copy plain text", "Delete key", "Rename key...",
1284 "Open in new tab...", "Open in new window..."];
1285 let mDiv;
1286
1287 for (const m of menu) {
1288 mDiv = document.createElement("div");
1289 mDiv.className = 'mmenuitem mmenulink';
1290 mDiv.innerHTML = '<nobr>' + m + '</nobr>';
1291 mDiv.title = m;
1292 cm.appendChild(mDiv);
1293 }
1294
1295 d.appendChild(cm);
1296 document.body.appendChild(d);
1297 }
1298
1299 // set event handler for copy menu
1300 d.childNodes[0].childNodes[0].onmousedown = function () {
1301 d.style.display = 'none';
1302 odb_copy(tb);
1303 }
1304
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);
1309 let paths = [];
1310 for (const k of selKeys)
1311 paths.push(k.path);
1312
1313 mjsonrpc_db_copy(paths).then(rpc => {
1314 let text = '';
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]);
1319 needPath = true;
1320 } else {
1321 if (needPath)
1322 text += odbASCII(rpc.result.data[i], tb.odb.path);
1323 else
1324 text += odbASCII(rpc.result.data[i]);
1325 needPath = false;
1326 }
1327 }
1328 if (navigator.clipboard && navigator.clipboard.writeText) {
1329 try {
1330 navigator.clipboard.writeText(text).then( () => {
1331 if (paths.length === 1)
1332 dlgAlert("ODB key \"" + paths + "\" copied to clipboard as plain text");
1333 else
1334 dlgAlert(paths.length + " ODB keys copied to clipboard as plain text");
1335 }).catch(e => dlgAlert(e));
1336 } catch(error) {
1337 dlgAlert(error);
1338 }
1339 }
1340 }).catch(error => mjsonrpc_error_alert(error));
1341 }
1342
1343 // set event handler for delete key menu
1344 d.childNodes[0].childNodes[2].onmousedown = function () {
1345 d.style.display = 'none';
1346 odb_delete(tr);
1347 }
1348
1349 // set event handler for rename menu
1350 d.childNodes[0].childNodes[3].onmousedown = function () {
1351 d.style.display = 'none';
1352
1353 let elem;
1354 if (tr.key.type === TID_KEY)
1355 elem = tr.childNodes[1].childNodes[2];
1356 else
1357 elem = tr.childNodes[1].childNodes[1];
1358
1359 let key = elem.innerHTML.trim();
1360 tr.oldKey = key;
1361
1362 inline_edit(event,
1363 elem,
1364 key,
1365 do_rename_key,
1366 key.length,
1367 tr.odbPath);
1368 }
1369
1370 // set event handler for open in new tab
1371 d.childNodes[0].childNodes[4].onmousedown = function () {
1372 d.style.display = 'none';
1373
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
1378
1379 window.open(url, '_blank').focus();
1380 }
1381
1382 // set event handler for open in new window
1383 d.childNodes[0].childNodes[5].onmousedown = function () {
1384 d.style.display = 'none';
1385
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
1390
1391 window.open(url, "", "menubar=yes,location=yes,status=yes");
1392 }
1393
1394 let rect = event.target.getBoundingClientRect();
1395
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';
1399
1400 if (parseInt(d.style.left) + d.offsetWidth > document.body.clientWidth)
1401 d.style.left = (document.body.clientWidth - d.offsetWidth) + 'px';
1402
1403 return false;
1404}
1405
1406function expand(e) {
1407 let tb = getOdbTb(e);
1408 let odb = tb.odb;
1409
1410 let rarr = document.getElementById('expRight');
1411 let cells = document.getElementsByName('odbExt');
1412 let table = document.getElementById('odbTable');
1413 odb.detailsColumn = !odb.detailsColumn;
1414
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';
1420 } else {
1421 for (const d of cells)
1422 d.style.display = 'none';
1423 rarr.style.display = 'inline';
1424 table.style.minWidth = '600px';
1425 }
1426 odb.skip_yellow = true;
1427 odb_update(tb);
1428}
1429
1430function clear_expanded(odb) {
1431 for (const key of odb.key) {
1432 if (key.type === TID_KEY)
1433 key.subdir_open = false;
1434 else
1435 key.expanded = false;
1436 }
1437}
1438
1439function toggle_handles(e) {
1440 let tb = getOdbTb(e);
1441 let odb = tb.odb;
1442
1443 // clear all expanded flags
1444 clear_expanded(odb);
1445 odb_print_all(tb);
1446
1447 let n = document.getElementsByName('odbHandle');
1448 odb.handleColumn = !odb.handleColumn;
1449
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);
1459 }
1460
1461 d.style.display = 'table-cell';
1462 }
1463 } else {
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);
1472 }
1473
1474 d.style.display = 'none';
1475 }
1476 }
1477
1478 odb_update(tb);
1479}
1480
1481function toggle_expanded(e) {
1482 let tr = e;
1483 while (tr.tagName !== 'TR')
1484 tr = tr.parentNode;
1485
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);
1489 } else
1490 do_toggle_expanded(true, tr);
1491}
1492
1493function do_toggle_expanded(flag, tr) {
1494 if (flag) {
1495 let tb = getOdbTb(tr);
1496 tr.key.expanded = !tr.key.expanded;
1497 unselect_all_keys(tb);
1498 unselect_all_array_elements(tb);
1499 odb_print_all(tb);
1500 }
1501}
1502
1503function odb_delete(e) {
1504
1505 let tb = getOdbTb(e);
1506 let currentPath = tb.odb.path;
1507 let paths = [];
1508 for (const tr of tb.childNodes) {
1509 if (tr.odbSelected)
1510 paths.push(tr.odbPath);
1511 }
1512
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('/'));
1518 if (newPath === "")
1519 newPath = '/';
1520
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});
1526 else
1527 dlgConfirm('Are you sure to delete ' + paths.length + ' keys ?',
1528 do_odb_delete, {"e": e, "paths": paths});
1529}
1530
1531function do_odb_delete(flag, param) {
1532 if (flag) {
1533 mjsonrpc_db_delete(param.paths).then((rpc) => {
1534 // check status
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]);
1541 } else {
1542 console.log(rpc);
1543 if (param.newPath)
1544 subdir_goto(param.e, param.newPath);
1545 else {
1546 let tb = getOdbTb(param.e);
1547 odb_update(tb);
1548 }
1549 }
1550 }).catch(error => mjsonrpc_error_alert(error));
1551 }
1552}
1553
1554function rename_key(element) {
1555 let tb = getOdbTb(element);
1556 let n = 0;
1557 let tr;
1558 for (const t of tb.childNodes) {
1559 if (t.odbSelected) {
1560 n++;
1561 tr = t;
1562 }
1563 }
1564
1565 if (n === 0)
1566 dlgAlert("Please select key to be renamed");
1567 else if (n > 1)
1568 dlgAlert("Please select only single key to be renamed");
1569 else {
1570 tr.odbSelected = false;
1571 tr.style.backgroundColor = '';
1572 change_color(tr, '');
1573
1574 let elem;
1575
1576 if (tr.key.type === TID_KEY)
1577 elem = tr.childNodes[1].childNodes[2];
1578 else
1579 elem = tr.childNodes[1].childNodes[1];
1580
1581 let key = elem.innerHTML;
1582 tr.oldKey = key;
1583
1584 inline_edit(undefined,
1585 elem,
1586 key,
1587 do_rename_key,
1588 key.length,
1589 tr.odbPath);
1590 }
1591}
1592
1593function do_rename_key(p, str, path) {
1594 if (str === '') {
1595 dlgAlert("Empty name not allowed");
1596 return;
1597 }
1598 str = str.trim();
1599 mjsonrpc_call("db_rename", { "paths": [path], "new_names": [str]})
1600 .then()
1601 .catch(error => mjsonrpc_error_alert(error));
1602
1603 let old = p.parentElement.parentElement.oldKey;
1604
1605 p.innerHTML = p.innerHTML.replace(old, str);
1606}
1607
1608function search_key() {
1609 dlgQuery("Enter key name (substring case insensitive):", "", do_search_key);
1610}
1611
1612function do_search_key(str) {
1613 if (str !== false)
1614 window.location.href = "?cmd=Find&value=" + str;
1615}
1616
1617function odb_copy(e) {
1618 let tb = getOdbTb(e);
1619 let selKeys = get_selected_keys(tb);
1620 let paths = [];
1621 let dirFlag = false;
1622 if (selKeys.length === 0) {
1623 paths.push(tb.odb.path);
1624 dirFlag = true;
1625 } else {
1626 for (const k of selKeys)
1627 paths.push(k.path);
1628 }
1629 mjsonrpc_db_copy(paths).then(rpc => {
1630 let text = '';
1631
1632 // generate text from objects, add top directoy which is missing from db_copy
1633 for (const [i,d] of rpc.result.data.entries()) {
1634 if (i === 0) {
1635 if (dirFlag) {
1636 let dir = tb.odb.path.substring(tb.odb.path.lastIndexOf('/')+1);
1637 if (dir === '')
1638 dir = 'root';
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, ' ')+'}';
1643 } else
1644 text += JSON.stringify(d, null, ' ');
1645 } else
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 ','
1649 }
1650
1651 // put text to clipboard if possible
1652 if (navigator.clipboard && navigator.clipboard.writeText) {
1653 try {
1654 navigator.clipboard.writeText(text).then( () => {
1655 if (dirFlag)
1656 dlgAlert("ODB directory \"" + paths + "\" copied to clipboard");
1657 else {
1658 if (paths.length === 1)
1659 dlgAlert("ODB key \"" + paths + "\" copied to clipboard");
1660 else
1661 dlgAlert("ODB keys \"" + paths + "\" copied to clipboard");
1662 }
1663 }).catch(e => dlgAlert(e));
1664 } catch(error) {
1665 dlgAlert(error);
1666 }
1667 }
1668
1669 // store text locally as a backup
1670 try {
1671 sessionStorage.setItem('odbclip', text);
1672 } catch (error) {
1673 dlgAlert(error);
1674 }
1675
1676 }).catch(error => mjsonrpc_error_alert(error));
1677}
1678
1679function odb_paste(e) {
1680 let tb = getOdbTb(e);
1681 let odb = tb.odb;
1682 try {
1683 let text = sessionStorage.getItem('odbclip');
1684 if (text) {
1685 let keys = JSON.parse(text);
1686 odb_paste_keys(tb, keys);
1687 return;
1688 }
1689 } catch (error) {
1690 dlgAlert("Invalid JSON format in clipboard:<br /><br />" + error);
1691 return;
1692 }
1693
1694 if (navigator.clipboard && navigator.clipboard.readText) {
1695 navigator.clipboard.readText().then(text => {
1696 try {
1697 if (text) {
1698 let keys = JSON.parse(text);
1699 odb_paste_keys(tb, keys);
1700 return;
1701 } else {
1702 dlgAlert("Nothing stored in clipboard");
1703 }
1704 } catch (error) {
1705 dlgAlert("Invalid JSON format in clipboard:<br /><br />" + error);
1706 }
1707 });
1708 } else
1709 dlgAlert("Paste not possible in this browser");
1710}
1711
1712function odb_paste_keys(tb, newKeys) {
1713 let odb = tb.odb;
1714
1715 for (const key in newKeys) {
1716 if (key.indexOf('/') !== -1)
1717 continue;
1718
1719 // check for existing key in current keys
1720 let cKeyMaxIndex = 0;
1721 let cKeyIndex = 0;
1722 let cKeyBare = "";
1723 let ckb = "";
1724 let found = false;
1725 for (const cKey of odb.key) {
1726 if (cKey.name.indexOf(' copy') === -1)
1727 ckb = cKey.name;
1728 else
1729 ckb = cKey.name.substring(0, cKey.name.indexOf(' copy'));
1730
1731 if (ckb === key) {
1732 found = true;
1733 cKeyBare = ckb;
1734 if (cKey.name.indexOf(' copy') !== -1) {
1735 cKeyIndex = parseInt(cKey.name.substring(cKey.name.indexOf(' copy')+5));
1736 if (isNaN(cKeyIndex))
1737 cKeyIndex = 1;
1738 if (cKeyIndex > cKeyMaxIndex)
1739 cKeyMaxIndex = cKeyIndex;
1740 }
1741 }
1742 }
1743
1744 // rename new key if name exists
1745 if (found) {
1746 let newName = cKeyBare + ' copy';
1747 if (cKeyIndex > 0)
1748 newName += ' ' + (cKeyIndex+1).toString();
1749
1750 newKeys[newName] = newKeys[key];
1751 delete newKeys[key];
1752
1753 newKeys[newName + '/key'] = newKeys[key + '/key'];
1754 delete newKeys[key + '/key'];
1755 }
1756 }
1757
1758 mjsonrpc_db_paste([odb.path], [newKeys]).then(rpc =>
1759 odb_update(tb)).catch(error =>
1760 dlgAlert(error));
1761
1762}
1763
1764function odbASCII(o, path) {
1765 // convert ODB keys to plain ASCII representation
1766 let t = "";
1767 let need_path = true;
1768 if (path === undefined)
1769 need_path = false;
1770 for (const key in o) {
1771 if (key.indexOf('/') !== -1)
1772 continue;
1773
1774 if (o[key+'/key'] === undefined && path) {
1775 t += odbASCII(o[key], path + '/' + key);
1776 need_path = true;
1777 continue;
1778 }
1779
1780 if (need_path) {
1781 need_path = false;
1782 t += '\n[' + path + ']\n';
1783 }
1784
1785 let tid = o[key+'/key'].type;
1786 let num_values = o[key+'/key'].num_values;
1787 t += key;
1788
1789 if (num_values > 1) {
1790 t += ":\n";
1791 for (let i=0 ; i<num_values ; i++) {
1792 t += '[' + i + ']\t';
1793 t += mie_to_string(tid, o[key][i]);
1794 t += '\n';
1795 }
1796 } else {
1797 t += ":\t";
1798 t += mie_to_string(tid, o[key]);
1799 t += '\n';
1800 }
1801 }
1802 return t;
1803}
1804
1805function odb_export(e) {
1806 let tb = getOdbTb(e);
1807 mjsonrpc_db_copy([tb.odb.path]).then(rpc => {
1808
1809 let dirs = tb.odb.path.split('/');
1810 let filename = dirs[dirs.length - 1];
1811 if (filename === '')
1812 filename = 'root';
1813 filename += ".json";
1814
1815 let header = {
1816 "/MIDAS version": "2.1",
1817 "/filename": filename,
1818 "/ODB path": tb.odb.path
1819 }
1820 header = JSON.stringify(header, null, ' ');
1821 header = header.substring(0, header.length-2) + ','; // strip trailing '}'
1822
1823 let odbJson = JSON.stringify(rpc.result.data[0], null, ' ');
1824 if (odbJson.indexOf('{') === 0)
1825 odbJson = odbJson.substring(1); // strip leading '{'
1826
1827 odbJson = header + odbJson;
1828
1829 // use trick from FileSaver.js
1830 let a = document.getElementById('downloadHook');
1831 if (a === null) {
1832 a = document.createElement("a");
1833 a.style.display = "none";
1834 a.id = "downloadHook";
1835 document.body.appendChild(a);
1836 }
1837
1838 let blob = new Blob([odbJson], {type: "text/json"});
1839 let url = window.URL.createObjectURL(blob);
1840
1841 a.href = url;
1842 a.download = filename;
1843 a.click();
1844 window.URL.revokeObjectURL(url);
1845 dlgAlert("ODB subtree \"" + tb.odb.path +
1846 "\" downloaded to file \"" + filename + "\"");
1847
1848 }).catch(error => mjsonrpc_error_alert(error));
1849}
1850
1851let _odb_path;
1852function odb_save_picker(e) {
1853 let tb = getOdbTb(e);
1854 _odb_path = tb.odb.path;
1855 file_picker("", "*.json", odb_save, true);
1856}
1857function odb_save(filename) {
1858 mjsonrpc_db_copy([_odb_path]).then(rpc => {
1859
1860 if (filename.indexOf('.json') === -1)
1861 filename += '.json';
1862
1863 let header = {
1864 "/MIDAS version": "2.1",
1865 "/filename": filename,
1866 "/ODB path": _odb_path
1867 }
1868 header = JSON.stringify(header, null, ' ');
1869 header = header.substring(0, header.length-2) + ','; // strip trailing '}'
1870
1871 let odbJson = JSON.stringify(rpc.result.data[0], null, ' ');
1872 if (odbJson.indexOf('{') === 0)
1873 odbJson = odbJson.substring(1); // strip leading '{'
1874
1875 odbJson = header + odbJson;
1876
1877 file_save_ascii(filename, odbJson, "ODB subtree \"" + _odb_path +
1878 "\" saved to file \"" + filename + "\"");
1879
1880 }).catch(error => mjsonrpc_error_alert(error));
1881}
1882
1883async function odb_import(e) {
1884 try {
1885 // Check if showOpenFilePicker is available in the browser
1886 // The "else" is more of a JS standard
1887 if (window.showOpenFilePicker) {
1888 let fileHandle;
1889 [fileHandle] = await window.showOpenFilePicker();
1890 const file = await fileHandle.getFile();
1891 const text = await file.text();
1892 pasteBuffer(e, text, file.name);
1893 } else {
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];
1900 if (file) {
1901 const reader = new FileReader();
1902 reader.onload = async (evObj) => {
1903 const text = evObj.target.result;
1904 pasteBuffer(e, text, file.name);
1905 };
1906 reader.readAsText(file);
1907 }
1908 });
1909 // Trigger the file input element
1910 fileInput.click();
1911 }
1912 } catch (error) {
1913 if (error.name !== 'AbortError') {
1914 // Handle errors as needed
1915 dlgAlert(error);
1916 }
1917 }
1918}
1919
1920function loadFileFromSelector(e) {
1921
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);
1927
1928 reader.onerror = function () {
1929 dlgAlert('File read error: ' + reader.error);
1930 };
1931
1932 reader.onload = function () {
1933 pasteBuffer(e.param, reader.result, file.name);
1934 };
1935 }
1936}
1937
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);
1943}
1944
1945function loadFile(filename) {
1946 file_load_ascii(filename, (text) => pasteBuffer(undefined, text, filename));
1947}
1948
1949function pasteBuffer(e, text, filename) {
1950 let tb;
1951 let path;
1952
1953 if (e === undefined)
1954 path = _odb_path;
1955 else {
1956 tb = getOdbTb(e);
1957 path = tb.odb.path;
1958 }
1959
1960 let odbJson;
1961 try {
1962 odbJson = JSON.parse(text);
1963 } catch (error) {
1964 dlgAlert(error);
1965 return;
1966 }
1967 // delete /MIDAS version, /filename etc.
1968 for (let [name, value] of Object.entries(odbJson)) {
1969 if (name[0] === '/')
1970 delete odbJson[name];
1971 }
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));
1975}
1976
1977function make_dialog_scroll(tb) {
1978 let d = tb.parentElement.parentElement.parentElement.parentElement;
1979
1980 if (tb.odb.picker)
1981 d.style.position = "absolute"; // remove "fixed"
1982}
1983
1984function subdir_open_click(event, e) {
1985 event.stopPropagation();
1986 let tr = e.parentNode;
1987 while (tr.tagName !== 'TR')
1988 tr = tr.parentNode;
1989 let path = tr.odbPath;
1990
1991 subdir_open(event.target, path);
1992}
1993
1994// user clicks on subdirectory, so open it
1995function subdir_open(tr, path) {
1996 while (tr.tagName !== 'TR')
1997 tr = tr.parentNode;
1998 let tb = getOdbTb(tr);
1999 let odb = tb.odb;
2000
2001 // don't open subdirs if in reorder mode
2002 if (odb.handleColumn)
2003 return;
2004
2005 unselect_all_keys(tb);
2006 unselect_all_array_elements(tb);
2007 make_dialog_scroll(tb);
2008
2009 // find key belonging to 'path'
2010 tr.key.subdir_open = !tr.key.subdir_open;
2011 tb.odb.skip_yellow = true;
2012 odb_update(tb);
2013}
2014
2015function subdir_goto_click(event, e) {
2016 event.stopPropagation();
2017 let tr = e.parentNode;
2018 while (tr.tagName !== 'TR')
2019 tr = tr.parentNode;
2020 let path = tr.odbPath;
2021
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)
2025 return;
2026
2027 subdir_goto(event.target, path);
2028}
2029
2030function subdir_goto(e, path) {
2031 let tb = getOdbTb(e);
2032 let odb = tb.odb;
2033
2034 unselect_all_keys(tb);
2035 unselect_all_array_elements(tb);
2036
2037 // kill old timer
2038 if (odb.updateTimer !== undefined)
2039 window.clearTimeout(odb.updateTimer);
2040
2041 // update URL
2042 if (!odb.picker) {
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
2047 let skip = false;
2048
2049 // "cmd=ODB" vs. "cmd=ODB&odb_path=/"
2050 if (window.location.href.indexOf('&') === -1 &&
2051 path === '/')
2052 skip = true;
2053
2054 if (url !== window.location.href && !skip)
2055 window.history.pushState({'path': path}, '', url);
2056 }
2057
2058 // modify title
2059 if (document.title.indexOf("ODB") !== -1) {
2060 if (path === '/')
2061 document.title = "ODB";
2062 else
2063 document.title = "ODB " + path;
2064 }
2065
2066 // update ODB object
2067 odb.path = path;
2068 odb.level = 0;
2069 odb.dataIndex = 0;
2070 odb.key = [];
2071 odb.skip_yellow = true;
2072
2073 odb_update(tb);
2074}
2075
2076// push all paths of open subdirectories recursively for a following db_ls
2077function push_paths(paths, odb, path) {
2078 odb.dataIndex = paths.length;
2079 paths.push(path);
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 = {
2084 id: odb.id,
2085 path: odb.path === '/' ? '/' + odb.key[i].name : odb.path + '/' + odb.key[i].name,
2086 level: odb.level + 1,
2087 dataIndex: paths.length,
2088 key: []
2089 };
2090 }
2091 push_paths(paths, odb.key[i].value, odb.key[i].value.path);
2092 }
2093 }
2094}
2095
2096// update overall ODB display
2097function odb_update(tb) {
2098
2099 // console.log("odb_update" + new Date());
2100 let odb;
2101 if (tb === undefined)
2102 tb = document.getElementById("odbTable").firstChild;
2103 odb = tb.odb;
2104
2105 if (odb.updateTimer !== undefined)
2106 window.clearTimeout(odb.updateTimer);
2107 let row = { i:4, nvk:0 };
2108
2109 // show clickable ODB path in top row
2110 let dirs;
2111 if (odb.path === '/')
2112 dirs = ['/'];
2113 else
2114 dirs = odb.path.split('/');
2115 let path = '';
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;\">" +
2118 "</a>&nbsp;";
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 += "&nbsp;/&nbsp;";
2124 }
2125 if (s !== tb.childNodes[1].firstChild.innerHTML)
2126 tb.childNodes[1].firstChild.innerHTML = s;
2127
2128 // request ODB data
2129 let paths = [];
2130 push_paths(paths, odb, odb.path);
2131
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;
2136
2137 // call timer in one second to update again
2138 odb.updateTimer = window.setTimeout(odb_update, 1000, tb);
2139 }).catch();
2140}
2141
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];
2145 let n = 0;
2146 for (const item in data) {
2147 if (item.indexOf('/') !== -1)
2148 continue;
2149
2150 let key = {};
2151 key.name = item;
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) {
2155 key.type = TID_KEY;
2156 key.subdir_open = false;
2157 key.num_values = 1;
2158 key.item_size = 0;
2159 key.last_written = 0;
2160 key.access_mode = 0;
2161 } else {
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)
2166 key.num_values = 1;
2167 if (data[item + '/key'].item_size !== undefined)
2168 key.item_size = data[item + '/key'].item_size;
2169 else
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;
2173 }
2174 key.options = (key.name.substring(0, 7) === "Options");
2175
2176 if (odb.key.length <= n) {
2177 key.expanded = (key.num_values > 1 && key.num_values <= 10);
2178 if (odb.picker)
2179 key.expanded = false;
2180 odb.key.push(key);
2181 } else {
2182 if (odb.key[n].subdir_open) {
2183 key.subdir_open = true;
2184 key.value = odb.key[n].value;
2185 }
2186 if (odb.key[n].expanded !== undefined) {
2187 key.expanded = odb.key[n].expanded;
2188 }
2189 odb.key[n] = key;
2190 }
2191 n++;
2192
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);
2197 }
2198 }
2199
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;
2204 }
2205}
2206
2207function odb_print_all(tb) {
2208 let odb = tb.odb;
2209 let row = { i:4, nvk:0 };
2210 let nValueKeys = 0;
2211 odb.skip_yellow = true;
2212 odb_print(tb, row, tb.odb);
2213}
2214
2215function odb_print(tb, row, odb) {
2216
2217 // first print all directories
2218 if (odb.subdirFirst) {
2219 for (const key of odb.key) {
2220 if (key.type !== TID_KEY)
2221 continue;
2222
2223 // Print current key
2224 odb_print_key(tb, row, odb.path, key, odb.level);
2225 row.i++;
2226
2227 // Print whole subdirectory if open
2228 if (key.subdir_open) {
2229
2230 // Propagate flags to all subkeys
2231 key.value.skip_yellow = odb.skip_yellow;
2232 key.value.subdirFirst = odb.subdirFirst;
2233
2234 // Print whole subdirectory
2235 odb_print(tb, row, key.value);
2236 }
2237 }
2238 }
2239
2240 // now print all keys
2241 for (const key of odb.key) {
2242
2243 if (odb.subdirFirst)
2244 if (key.type === TID_KEY)
2245 continue;
2246
2247 // search for "Options <key>"
2248 let options = odb.key.find(o => o.name === 'Options ' + key.name);
2249
2250 // Print current key
2251 odb_print_key(tb, row, odb.path, key, odb.level, options ? options.value : undefined);
2252 row.i++;
2253 row.nvk++;
2254 }
2255
2256 // Hide 'value' if only directories are listed
2257 let ds = document.getElementsByName('valueHeader');
2258 for (d of ds)
2259 if (tb.contains(d))
2260 d.style.display = row.nvk > 0 ? 'table-cell' : 'none';
2261
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]);
2266}
2267
2268function find_input_element(e) {
2269 if (e === undefined)
2270 return false;
2271 for (const c of e.childNodes) {
2272 if (c.tagName === 'INPUT')
2273 return true;
2274 if (c.tagName === 'SELECT' && e.inEdit)
2275 return true;
2276 if (c.childNodes.length > 0)
2277 if (find_input_element(c))
2278 return true;
2279 }
2280 return false;
2281}
2282
2283function bnToHex(val) {
2284 let hex;
2285 if (val < 0) {
2286 hex = ((1n << 64n) + BigInt(val)).toString(16).toUpperCase();
2287 hex = hex.padStart(16, '0');
2288 } else {
2289 hex = val.toString(16).toUpperCase();
2290 if (hex.length % 2) {
2291 hex = '0' + hex;
2292 }
2293 }
2294
2295 return hex;
2296}
2297
2298function odb_print_key(tb, row, path, key, level, options) {
2299 let odb = tb.odb;
2300
2301 // ful path to key
2302 let keyPath = (path === '/' ? '/' + key.name : path + '/' + key.name);
2303
2304 // create empty row
2305 let tr = document.createElement('TR');
2306
2307 // Handle column
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";
2312 tr.appendChild(td);
2313 td.innerHTML = '<img style="cursor: all-scroll; height: 13px; padding: 0; border: 0" src="icons/menu.svg">';
2314
2315 td.childNodes[0].setAttribute('draggable', false);
2316
2317 // Key name
2318 td = document.createElement('TD');
2319 tr.appendChild(td);
2320
2321 // Add three spaces of indent for each level
2322 let indent = "<span>";
2323 for (let i = 0; i < level; i++)
2324 indent += "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;";
2325 indent += "</span>";
2326
2327 // Remember indent level for key rename
2328 tr.level = odb.level;
2329
2330 // Remember key (need to toggle expanded flag)
2331 tr.key = key;
2332
2333 // Remember key path
2334 tr.odbPath = keyPath;
2335
2336 // Set special class for "Options ..." keys
2337 if (key.options && !odb.detailsColumn)
2338 tr.style.display = 'none';
2339
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 + "&nbsp;\u25B8&nbsp;" + escapeHTML(key.name);
2345 } else {
2346 let handler = "onclick=\"subdir_open_click(event, this);return false;\" ";
2347
2348 if (key.subdir_open)
2349 td.innerHTML = indent + "<a href='#' " +
2350 handler +
2351 ">&nbsp;\u25BE&nbsp;</a>";
2352 else
2353 td.innerHTML = indent + "<a href='#' " +
2354 handler +
2355 ">&nbsp;\u25B8&nbsp;</a>";
2356
2357 handler = "onclick=\"subdir_goto_click(event, this);return false;\" ";
2358 td.innerHTML += "<a href='#' " + handler + ">" + escapeHTML(key.name) + "</a>";
2359 }
2360
2361 if (key.link) {
2362 if (odb.picker)
2363 td.innerHTML += ' &rarr; <span>' + key.link + '</span>';
2364 else
2365 td.innerHTML += ' &rarr; <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 +
2370 '</a></span>';
2371 }
2372
2373 } else if (key.link === undefined) {
2374 td.innerHTML = indent + '<span>' + escapeHTML(key.name) + '</span>';
2375 } else {
2376 td.innerHTML = indent + '<span>' + escapeHTML(key.name) + '</span>';
2377 if (odb.picker)
2378 td.innerHTML += ' &rarr; <span>' + escapeHTML(key.link) + '</span>';
2379 else
2380 td.innerHTML += ' &rarr; <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) +
2385 '</a></span>';
2386 }
2387
2388 // Subdirs occupy all 8 columns
2389 if (key.type === TID_KEY) {
2390 td.colSpan = "8";
2391
2392 if (tb.childNodes.length > row.i) {
2393 let selected = tb.childNodes[row.i].odbSelected;
2394 if (selected) {
2395 tr.style.backgroundColor = '#004CBD';
2396 change_color(tr, '#FFFFFF');
2397 }
2398
2399 if (!tb.childNodes[row.i].isEqualNode(tr)) {
2400 // check for edit mode
2401 let inlineEdit = find_input_element(tb.childNodes[row.i]);
2402 if (!inlineEdit) {
2403 if (tb.childNodes[row.i].innerHTML != tr.innerHTML)
2404 tb.childNodes[row.i].innerHTML = tr.innerHTML;
2405 }
2406 }
2407 } else
2408 tb.appendChild(tr);
2409
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);
2414 }
2415 tb.childNodes[row.i].addEventListener('mousedown', select_key);
2416 if (!odb.picker) {
2417 tb.childNodes[row.i].addEventListener('mousemove', select_key);
2418 tb.childNodes[row.i].addEventListener('contextmenu', context_menu);
2419 }
2420
2421 // Remember ODB path in row
2422 tb.childNodes[row.i].odbPath = keyPath;
2423 tb.childNodes[row.i].key = key;
2424
2425 // Remove and odbOptions class or invisibility
2426 tb.childNodes[row.i].className = "";
2427 tb.childNodes[row.i].style.display = 'table-row';
2428
2429 return;
2430 }
2431
2432 // Value
2433 td = document.createElement('TD');
2434 tr.appendChild(td);
2435
2436 if (Array.isArray(key.value)) {
2437 if (odb.picker)
2438 td.innerHTML =
2439 '<div style="display: inline;"></div>';
2440 else {
2441 if (key.type === TID_BOOL) {
2442 td.innerHTML =
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>' +
2449 '</select>' +
2450 '</div>';
2451 } else
2452 td.innerHTML =
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>';
2458 }
2459
2460 if (key.expanded === false)
2461 td.innerHTML +=
2462 '<div style="display: inline;float: right">' +
2463 '<a href="#"' +
2464 'onclick="toggle_expanded(this.parentNode);return false;" ' +
2465 'title="Show array elements">\u25B8</a>' +
2466 '</div>';
2467 else
2468 td.innerHTML +=
2469 '<div style="display: inline;float: right">' +
2470 '<a href="#"' +
2471 'onclick="toggle_expanded(this.parentNode);return false;" ' +
2472 'title="Hide array elements">\u25BE</a>' +
2473 '</div>';
2474 } else {
2475
2476 if (key.type === TID_BOOL) {
2477
2478 let h = "<select onchange='odb_setoption(this)' " +
2479 " onfocus='option_edit(event, this, true)'" +
2480 " onblur='option_edit(event, this, false)'" +
2481 "'>\n";
2482
2483 if (key.value === false) {
2484 h += "<option selected value='" + 0 + "'>No</option>";
2485 h += "<option value='" + 1 + "'>Yes</option>";
2486 } else {
2487 h += "<option value='" + 0 + "'>No</option>";
2488 h += "<option selected value='" + 1 + "'>Yes</option>";
2489 }
2490 h += "</select>\n";
2491 td.innerHTML = h;
2492
2493 } else if (options) {
2494
2495 let oc = options.slice(); // make copy of all elements
2496
2497 let h = "<select onchange='odb_setoption(this)' " +
2498 " onfocus='option_edit(event, this, true)'" +
2499 " onblur='option_edit(event, this, false)'" +
2500 "'>\n";
2501
2502 // check if current value is in options, add it if not
2503 if (!oc.includes(key.value))
2504 oc.unshift(key.value);
2505
2506 for (const o of oc) {
2507 if (key.value === o)
2508 h += "<option selected value='" + o + "'>" + o + "</option>";
2509 else
2510 h += "<option value='" + o + "'>" + o + "</option>";
2511 }
2512 h += "</select>\n";
2513 td.innerHTML = h;
2514
2515 } else {
2516
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">';
2525
2526 let v = escapeHTML(key.value.toString());
2527 let vHex = "";
2528 let hexValue = false;
2529 if (key.type === TID_STRING && v === "")
2530 v = "(empty)";
2531 else if (key.type === TID_STRING && v.trim() === "")
2532 v = "(spaces)";
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") {
2536 hexValue = true;
2537 vHex = "0x" + v.substring(2).toUpperCase();
2538 if (key.type === TID_UINT64)
2539 v = BigInt(key.value).toString();
2540 else
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));
2551 else
2552 vHex = "0x" + key.value.toString(16).toUpperCase();
2553 }
2554 if (odb.picker || odb.handleColumn)
2555 td.innerHTML = v + vHex;
2556 else {
2557 if (vHex === "")
2558 td.innerHTML = edit + v + '</a>';
2559 else {
2560 if (hexValue)
2561 td.innerHTML = editHex + vHex + '</a> ' + ' (' + edit + v + '</a>)';
2562 else
2563 td.innerHTML = edit + v + '</a> ' + ' (' + editHex + vHex + '</a>)';
2564 }
2565 }
2566 }
2567 }
2568
2569 // Type
2570 td = document.createElement('TD');
2571 td.setAttribute('name', 'odbExt');
2572 td.style.display = odb.detailsColumn ? 'table-cell' : 'none';
2573 tr.appendChild(td);
2574 td.appendChild(document.createTextNode(tid_name[key.type]));
2575
2576 // #Val
2577 td = document.createElement('TD');
2578 td.setAttribute('name', 'odbExt');
2579 td.style.display = odb.detailsColumn ? 'table-cell' : 'none';
2580 tr.appendChild(td);
2581 td.innerHTML = '<a href="#" onclick="resize_array(event, \''+keyPath+'\','+key.num_values+');return false;">'
2582 + key.num_values + '</a>';
2583
2584 // Size
2585 td = document.createElement('TD');
2586 td.setAttribute('name', 'odbExt');
2587 td.style.display = odb.detailsColumn ? 'table-cell' : 'none';
2588 tr.appendChild(td);
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>';
2592 else
2593 td.innerHTML = key.item_size;
2594
2595 // Written
2596 td = document.createElement('TD');
2597 td.setAttribute('name', 'odbExt');
2598 td.style.display = odb.detailsColumn ? 'table-cell' : 'none';
2599 tr.appendChild(td);
2600 let s = Math.floor(0.5 + new Date().getTime() / 1000 - key.last_written);
2601 let t;
2602 if (s < 60)
2603 t = s + 's';
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';
2610 else
2611 t = ">99d";
2612 td.appendChild(document.createTextNode(t));
2613
2614 // Mode
2615 td = document.createElement('TD');
2616 td.setAttribute('name', 'odbExt');
2617 td.style.display = odb.detailsColumn ? 'table-cell' : 'none';
2618 tr.appendChild(td);
2619 let mode = key.access_mode;
2620 let m = "";
2621 if (mode & MODE_READ)
2622 m += "R";
2623 if (mode & MODE_WRITE)
2624 m += "W";
2625 if (mode & MODE_DELETE)
2626 m += "D";
2627 if (mode & MODE_EXCLUSIVE)
2628 m += "X";
2629 if (mode & MODE_WATCH)
2630 m += "W";
2631 td.appendChild(document.createTextNode(m));
2632
2633 // invert color if selected
2634 if (tb.childNodes.length > row.i) {
2635 let selected = tb.childNodes[row.i].odbSelected;
2636 if (selected) {
2637 tr.style.backgroundColor = '#004CBD';
2638 change_color(tr, '#FFFFFF');
2639 }
2640 }
2641
2642 if (row.i >= tb.childNodes.length) {
2643 // append new row if nothing exists
2644 tb.appendChild(tr);
2645 } else {
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;
2652
2653 // check for data change in value column
2654 if (i === 2) {
2655 let oldElem = tb.childNodes[row.i].childNodes[i];
2656 if (oldElem === undefined)
2657 oldElem = "";
2658 else {
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;
2663 }
2664 let newElem = tr.childNodes[i];
2665 if (newElem === undefined)
2666 newElem = "";
2667 else {
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;
2672 }
2673
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)
2680 yellowBg = true;
2681 else
2682 yellowBg = false;
2683 }
2684
2685 // check for edit mode
2686 let inlineEdit = find_input_element(tb.childNodes[row.i].childNodes[i]);
2687
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];
2692
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;
2695
2696 if (yellowBg) {
2697 e.style.backgroundColor = 'var(--myellow)';
2698 e.style.setProperty("-webkit-transition", "", "");
2699 e.style.setProperty("transition", "", "");
2700 e.age = new Date() / 1000;
2701 }
2702
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 = "";
2707 }
2708 }
2709 }
2710 }
2711 }
2712
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);
2717 }
2718 tb.childNodes[row.i].addEventListener('mousedown', select_key);
2719 if (!odb.picker) {
2720 tb.childNodes[row.i].addEventListener('mousemove', select_key);
2721 tb.childNodes[row.i].addEventListener('contextmenu', context_menu);
2722 }
2723
2724 // Remember ODB path and key in row
2725 tb.childNodes[row.i].odbPath = keyPath;
2726 tb.childNodes[row.i].key = key;
2727
2728 // Copy visibility for odbOptions
2729 tb.childNodes[row.i].style.display = tr.style.display;
2730
2731 // Print array values
2732 if (Array.isArray(key.value)) {
2733 if (key.expanded === false) {
2734 // do nothing
2735 } else for (let i=0 ; i<key.value.length ; i++) {
2736 row.i++;
2737
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)
2742 continue;
2743
2744 // create empty row
2745 let tr = document.createElement('TR');
2746
2747 // store key path
2748 tr.odbPath = keyPath + '[' + i + ']';
2749
2750 // hide option keys if not in detailed column mode
2751 if (key.options && !odb.detailsColumn)
2752 tr.style.display = 'none';
2753
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";
2759 tr.appendChild(td);
2760
2761 // Key name
2762 td = document.createElement('TD');
2763 tr.appendChild(td);
2764
2765 // Key value
2766 td = document.createElement('TD');
2767 tr.appendChild(td);
2768 let p = (path === '/' ? '/' + key.name : path + '/' + key.name);
2769 p += '['+i+']';
2770
2771 if (key.type === TID_BOOL) {
2772
2773 let h = "<select onchange='odb_setoption(this," + i + ")' " +
2774 " onfocus='option_edit(event, this, true)'" +
2775 " onblur='option_edit(event, this, false)'" +
2776 "'>\n";
2777
2778 if (key.value[i] === false) {
2779 h += "<option selected value='" + 0 + "'>No</option>";
2780 h += "<option value='" + 1 + "'>Yes</option>";
2781 } else {
2782 h += "<option value='" + 0 + "'>No</option>";
2783 h += "<option selected value='" + 1 + "'>Yes</option>";
2784 }
2785 h += "</select>\n";
2786 td.innerHTML = "[" + i + "]" + h;
2787
2788 } else if (options) {
2789
2790 let oc = options.slice(); // make copy of all elements
2791
2792 let h = "<select onchange='odb_setoption(this," + i + ")' " +
2793 " onfocus='option_edit(event, this, true)'" +
2794 " onblur='option_edit(event, this, false)'" +
2795 "'>\n";
2796
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]);
2800
2801 for (const o of oc) {
2802 if (key.value[i] === o)
2803 h += "<option selected value='" + o + "'>" + o + "</option>";
2804 else
2805 h += "<option value='" + o + "'>" + o + "</option>";
2806 }
2807 h += "</select>\n";
2808 td.innerHTML = h;
2809
2810 } else {
2811 let edit = '<a href="#" onclick="ODBInlineEdit(this.parentNode, \'' + p + '\');return false;" ' +
2812 'onfocus="ODBInlineEdit(this.parentNode, \'' + p + '\')" title="Change array element">';
2813
2814 let v = escapeHTML(key.value[i].toString());
2815 if (key.type === TID_STRING && v === "")
2816 v = "(empty)";
2817 else if (key.type === TID_STRING && v.trim() === "")
2818 v = "(spaces)";
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() + ')';
2825 else
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])) + ')';
2836 else
2837 v += " (0x" + key.value[i].toString(16).toUpperCase() + ')';
2838 }
2839
2840 if (odb.picker)
2841 td.innerHTML = '[' + i + '] ' + v;
2842 else if (odb.handlecolumn)
2843 td.innerHTML = v;
2844 else
2845 td.innerHTML = '[' + i + '] ' + edit + v + '</a>';
2846 }
2847
2848 // Empty fill cells
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';
2853 tr.appendChild(td);
2854 }
2855
2856 if (row.i >= tb.childNodes.length) {
2857 tb.appendChild(tr);
2858 } else {
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;
2869 let oldValue;
2870 let newValue;
2871
2872 if (i === 2) {
2873 let select = (tr.childNodes[2].childNodes[0] &&
2874 tr.childNodes[2].childNodes[0].tagName == 'SELECT');
2875
2876 if (tb.childNodes[row.i].childNodes[2] !== undefined) {
2877 if (select)
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
2883 else
2884 oldValue = tb.childNodes[row.i].childNodes[2].innerHTML;
2885 }
2886 if (select)
2887 newValue = tr.childNodes[2].childNodes[0].value;
2888 else {
2889 if (tr.childNodes[2].childNodes[1] === undefined)
2890 newValue = tr.childNodes[2].childNodes[0].data;
2891 else
2892 newValue = tr.childNodes[2].childNodes[1].innerHTML;
2893 }
2894 }
2895
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)
2905 changed = true;
2906
2907 if (tb.childNodes[row.i].childNodes[i] === undefined)
2908 tb.childNodes[row.i].appendChild(tr.childNodes[i].cloneNode(true));
2909 else {
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);
2914 }
2915
2916 let e = tb.childNodes[row.i].childNodes[i];
2917 if (changed) {
2918 e.style.backgroundColor = 'var(--myellow)';
2919 e.style.setProperty("-webkit-transition", "", "");
2920 e.style.setProperty("transition", "", "");
2921 e.age = new Date() / 1000;
2922 }
2923
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 = "";
2928 }
2929 if (e.age !== undefined && new Date() / 1000 > e.age + 2) {
2930 e.style.cssText = "";
2931 e.removeAttribute("style");
2932 e.age = undefined;
2933 }
2934 }
2935
2936 tb.childNodes[row.i].odbSelected = odbSelected;
2937 tb.childNodes[row.i].odbLastSelected = odbLastSelected;
2938 }
2939 }
2940 }
2941
2942 // remove/install mouse click handlers
2943 tb.childNodes[row.i].removeEventListener('mousedown', select_key);
2944 if (!odb.picker) {
2945 tb.childNodes[row.i].removeEventListener('mousemove', select_key);
2946 tb.childNodes[row.i].removeEventListener('contextmenu', context_menu);
2947 }
2948
2949 tb.childNodes[row.i].childNodes[2].addEventListener('mousedown', select_array_element);
2950 tb.childNodes[row.i].childNodes[2].addEventListener('mousemove', select_array_element);
2951
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;
2956
2957 tb.childNodes[row.i].style.display = tr.style.display;
2958
2959 }
2960 }
2961}
2962
2963function resize_array(event, path, n) {
2964 event.stopPropagation(); // do not select row
2965 dlgQuery("Enter size of array:", n, do_resize_array, path);
2966}
2967
2968function do_resize_array(n, path) {
2969 mjsonrpc_db_resize([path],[parseInt(n)]).then(rpc => {
2970 }).catch(error => mjsonrpc_error_alert(error));
2971}
2972
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 });
2976}
2977
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));
2984}
2985
2986function dlgCreateKeyDown(event, inp) {
2987 let keyCode = ('which' in event) ? event.which : event.keyCode;
2988
2989 if (keyCode === 27) {
2990 // cancel editing
2991 dlgHide('dlgCreate');
2992 return false;
2993 }
2994
2995 if (keyCode === 13) {
2996 // finish editing
2997 if (do_new_key(event.target))
2998 dlgHide('dlgCreate');
2999 return false;
3000 }
3001
3002 return true;
3003}
3004
3005function dlgCreateLinkKeyDown(event, inp) {
3006 let keyCode = ('which' in event) ? event.which : event.keyCode;
3007
3008 if (keyCode === 27) {
3009 // cancel editing
3010 dlgHide('dlgCreateLink');
3011 return false;
3012 }
3013
3014 if (keyCode === 13) {
3015 // finish editing
3016 if (do_new_link(this))
3017 dlgHide('dlgCreateLink');
3018 return false;
3019 }
3020
3021 return true;
3022}
3023
3024function drag_start(event) {
3025 let tr = event.target;
3026 while (tr.tagName !== 'TR')
3027 tr = tr.parentNode;
3028 let tb = getOdbTb(tr);
3029
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);
3034
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";
3042 }, 10);
3043
3044}
3045
3046function drag_move(event, td) {
3047 event.preventDefault();
3048
3049 let tr = event.target;
3050 while (tr.tagName !== 'TR')
3051 tr = tr.parentNode;
3052 let tb = getOdbTb(tr);
3053 let children = Array.from(tb.children);
3054
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]);
3059
3060 tb.dragTargetRow = children.indexOf(tr);
3061}
3062
3063function drag_end(event, td) {
3064 let tr = event.target;
3065 while (tr.tagName !== 'TR')
3066 tr = tr.parentNode;
3067 let tb = getOdbTb(tr);
3068
3069 let ttr = tb.childNodes[tb.dragTargetRow];
3070 ttr.innerHTML = tb.dragRowContent.innerHTML;
3071 ttr.style.height = "";
3072 ttr.style.width = "";
3073
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));
3078 } else
3079 tb.odb.updateTimer = window.setTimeout(odb_update, 10, tb);
3080}
3081
3082function show_open_records(odb) {
3083 let title = "ODB open records under \"" + odb.path + "\"";
3084
3085 let d = document.createElement("div");
3086 d.className = "dlgFrame";
3087 d.style.zIndex = "30";
3088 d.style.minWidth = "400px";
3089 d.shouldDestroy = true;
3090
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>" +
3097 "</div>";
3098
3099 document.body.appendChild(d);
3100
3101 update_open_records(odb);
3102 dlgShow(d, false);
3103}
3104
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;
3109 let paths = {};
3110 for (const s of sor) {
3111 if (paths[s.path])
3112 paths[s.path] += ', ' + s.name;
3113 else
3114 paths[s.path] = s.name;
3115 }
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>';
3123
3124 let d = document.getElementById('dlgSOR');
3125 if (d === null)
3126 return; // dialog has been closed
3127 if (d.innerHTML !== html) {
3128 d.innerHTML = html;
3129 dlgCenter(d.parentElement.parentElement);
3130 }
3131 window.setTimeout(update_open_records, 1000, odb);
3132
3133 }).catch( error => mjsonrpc_error_alert(error));
3134}
3135
3136function show_open_clients(e) {
3137 let odb = getOdbTb(e).odb;
3138 let title = "ODB clients";
3139
3140 let d = document.createElement("div");
3141 d.className = "dlgFrame";
3142 d.style.zIndex = "30";
3143 d.style.minWidth = "400px";
3144 d.shouldDestroy = true;
3145
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>" +
3152 "</div>";
3153
3154 document.body.appendChild(d);
3155
3156 update_open_clients(odb);
3157 dlgShow(d, false);
3158}
3159
3160function update_open_clients(odb) {
3161 let path = odb.path;
3162 mjsonrpc_call("db_scl").then (rpc => {
3163 let scl = rpc.result.scl.clients;
3164
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>';
3175 html += '</tr>';
3176 }
3177 html += '</tbody></table>';
3178
3179 let d = document.getElementById('dlgSCL');
3180 if (d === null)
3181 return; // dialog has been closed
3182 if (d.innerHTML !== html) {
3183 d.innerHTML = html;
3184 dlgCenter(d.parentElement.parentElement);
3185 }
3186 window.setTimeout(update_open_clients, 1000, odb);
3187
3188 }).catch( error => mjsonrpc_error_alert(error));
3189}