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 if (event.type !== "mousemove") {
941 let m = document.getElementById('moreMenu');
942 if (m !== null && m.style.display === 'block')
943 m.style.display = 'none';
944
945 // hide context menu if visible
946 m = document.getElementById('contextMenu');
947 if (m !== null && m.style.display === 'block')
948 m.style.display = 'none';
949 }
950 // un-select all array elements
951 unselect_all_array_elements(tb);
952
953 // don't select key when we are in edit mode
954 while (tr.tagName !== 'TR') {
955 tr = tr.parentNode;
956 if (tr === null)
957 return;
958 }
959 if (find_input_element(tr))
960 return;
961
962 if (odb.picker) {
963 // unselect all keys
964 for (let i = 4; i < tb.childNodes.length; i++)
965 tb.childNodes[i].odbSelected = false;
966
967 // don't select array values
968 if (tr.childNodes[1] && tr.childNodes[1].innerHTML !== "")
969 tr.odbSelected = true;
970
971 odb.selectedKey = tr.odbPath;
972 } else {
973 // check if click is on header row, if so remove selection further down
974 let headerRow = false;
975 for (let i = 0; i < 4; i++)
976 if (tb.childNodes[i] === tr) {
977 headerRow = true;
978 break;
979 }
980
981 if (event.type === "mousemove") {
982
983 if (event.buttons === 1 && !headerRow) {
984 // search last selected row
985 let i1;
986 for (i1 = 4; i1 < tb.childNodes.length; i1++)
987 if (tb.childNodes[i1].odbLastSelected)
988 break;
989 if (i1 === tb.childNodes.length)
990 i1 = 4; // none selected, so use first one
991
992 let i2;
993 for (i2 = 4; i2 < tb.childNodes.length; i2++)
994 if (tb.childNodes[i2] === tr)
995 break;
996
997 if (i2 < tb.childNodes.length) {
998 if (i1 > i2)
999 [i1, i2] = [i2, i1];
1000
1001 for (let i = i1; i <= i2; i++) {
1002 // don't select arrays
1003 if (tb.childNodes[i].childNodes[1].innerHTML !== "")
1004 tb.childNodes[i].odbSelected = true;
1005 }
1006 }
1007 }
1008
1009 } else { // mousedown
1010
1011 if (event.shiftKey && !headerRow) {
1012 // search last selected row
1013 let i1;
1014 for (i1 = 4; i1 < tb.childNodes.length; i1++)
1015 if (tb.childNodes[i1].odbLastSelected)
1016 break;
1017 if (i1 === tb.childNodes.length)
1018 i1 = 4; // none selected, so use first one
1019
1020 let i2;
1021 for (i2 = 4; i2 < tb.childNodes.length; i2++)
1022 if (tb.childNodes[i2] === tr)
1023 break;
1024
1025 if (i2 < tb.childNodes.length) {
1026 if (i1 > i2)
1027 [i1, i2] = [i2, i1];
1028
1029 for (let i = i1; i <= i2; i++) {
1030 // don't select arrays
1031 if (tb.childNodes[i].childNodes[1].innerHTML !== "")
1032 tb.childNodes[i].odbSelected = true;
1033 }
1034 }
1035
1036 } else if ((event.metaKey || event.ctrlKey) && !headerRow) {
1037
1038 // command key just toggles current selection
1039 tr.odbSelected = !tr.odbSelected;
1040
1041 for (let i = 4; i < tb.childNodes.length; i++)
1042 tb.childNodes[i].odbLastSelected = false;
1043 if (tr.odbSelected)
1044 tr.odbLastSelected = true;
1045
1046 } else {
1047
1048 if (event.button !== 2) {
1049 // no key pressed -> un-select all but current
1050 for (let i = 4; i < tb.childNodes.length; i++) {
1051 tb.childNodes[i].odbSelected = false;
1052 tb.childNodes[i].odbLastSelected = false;
1053 }
1054
1055 // don't select header row and array values
1056 if (!headerRow && tr.childNodes[1].innerHTML !== "") {
1057 tr.odbSelected = true;
1058 tr.odbLastSelected = true;
1059 tr.odbSelectTime = new Date().getTime();
1060 }
1061 }
1062 }
1063 }
1064 }
1065
1066 // change color of all rows according to selection
1067 for (let i=4 ; i<tb.childNodes.length ; i++) {
1068 let tr = tb.childNodes[i];
1069
1070 tr.style.backgroundColor = tr.odbSelected ? '#004CBD' : '';
1071 change_color(tr, tr.odbSelected ? '#FFFFFF' : '');
1072 }
1073}
1074
1075function getArrayTr(tr) {
1076 // collect all rows belonging to "tr" in an array
1077 let tb = getOdbTb(tr);
1078 let atr = [];
1079 let i;
1080 for (i = 0; i < tb.childNodes.length; i++)
1081 if (tb.childNodes[i] === tr)
1082 break;
1083 while (tb.childNodes[i].odbPath.indexOf('[') !== -1)
1084 i--;
1085 i++; // first element
1086 do {
1087 atr.push(tb.childNodes[i++]);
1088 } while (i < tb.childNodes.length && tb.childNodes[i].odbPath.indexOf('[') !== undefined);
1089 return atr;
1090}
1091
1092let mouseDragged = false;
1093
1094function mouseStopDragging() {
1095 mouseDragged = false;
1096}
1097
1098function select_array_element(event) {
1099
1100 if (event.type === 'mousemove' && !mouseDragged)
1101 return;
1102
1103 // keep off secondary mouse buttons
1104 if (event.button !== 0)
1105 return;
1106
1107 // keep off drop-down boxes
1108 if (event.target.tagName === "SELECT")
1109 return;
1110
1111 event.preventDefault();
1112 event.stopPropagation();
1113
1114 let tr = event.target;
1115 let tb = getOdbTb(tr);
1116 if (tb === undefined)
1117 return;
1118
1119 // don't select key when we are in edit mode
1120 while (tr.tagName !== 'TR') {
1121 tr = tr.parentNode;
1122 if (tr === null)
1123 return;
1124 }
1125 if (find_input_element(tr))
1126 return;
1127
1128 let odb = tb.odb;
1129
1130 // hide submenu if visible
1131 let m = document.getElementById('moreMenu');
1132 if (m !== null && m.style.display === 'block')
1133 m.style.display = 'none';
1134
1135 // hide context menu if visible
1136 m = document.getElementById('contextMenu');
1137 if (m !== null && m.style.display === 'block')
1138 m.style.display = 'none';
1139
1140 // remove selection from all non-array keys
1141 for (let i=0 ; i<tb.childNodes.length ; i++) {
1142 if (tb.childNodes[i].childNodes.length > 1 &&
1143 tb.childNodes[i].childNodes[1].innerHTML !== "" &&
1144 tb.childNodes[i].odbSelected) {
1145 tb.childNodes[i].odbSelected = false;
1146 tb.childNodes[i].style.backgroundColor = '';
1147 change_color(tb.childNodes[i], '');
1148 }
1149 }
1150
1151 // create array of all array elements
1152 let atr = getArrayTr(tr);
1153
1154 if (event.type === 'mousedown') {
1155 mouseDragged = true;
1156
1157 if (odb.picker) {
1158 // un-select all but current
1159 for (const tr of atr) {
1160 tr.odbSelected = false;
1161 tr.odbLastSelected = false;
1162 }
1163 tr.odbSelected = true;
1164 tr.odbLastSelected = true;
1165 odb.selectedKey = tr.odbPath;
1166
1167 } else {
1168
1169 if (event.shiftKey) {
1170 // search last selected row
1171 let i1;
1172 for (i1 = 0; i1 < atr.length; i1++)
1173 if (atr[i1].odbLastSelected)
1174 break;
1175 if (i1 === atr.length)
1176 i1 = 0; // non selected, so use first one
1177
1178 let i2;
1179 for (i2 = 0; i2 < atr.length; i2++)
1180 if (atr[i2] === tr)
1181 break;
1182
1183 if (i2 < atr.length) {
1184 if (i1 > i2)
1185 [i1, i2] = [i2, i1];
1186
1187 for (let i = i1; i <= i2; i++)
1188 atr[i].odbSelected = true;
1189 }
1190
1191 } else if (event.metaKey || event.ctrlKey) {
1192
1193 // command key just toggles current selection
1194 tr.odbSelected = !tr.odbSelected;
1195
1196 for (const tr of atr)
1197 tr.odbLastSelected = false;
1198 if (tr.odbSelected)
1199 tr.odbLastSelected = true;
1200
1201 } else {
1202
1203 // no key pressed -> un-select all but current
1204 for (const tr of atr) {
1205 tr.odbSelected = false;
1206 tr.odbLastSelected = false;
1207 }
1208 tr.odbSelected = true;
1209 tr.odbLastSelected = true;
1210 }
1211 }
1212 }
1213
1214 if (event.type === 'mousemove' && mouseDragged) {
1215
1216 if (!event.shiftKey && !event.metaKey && !event.ctrlKey) {
1217 // unselect all
1218 for (const tr of atr)
1219 tr.odbSelected = false;
1220
1221 // search last selected row
1222 for (i1 = 0; i1 < atr.length; i1++)
1223 if (atr[i1].odbLastSelected)
1224 break;
1225 if (i1 === atr.length)
1226 i1 = 0; // non selected, so use first one
1227
1228 let i2;
1229 for (i2 = 0; i2 < atr.length; i2++)
1230 if (atr[i2] === tr)
1231 break;
1232
1233 if (i2 < atr.length) {
1234 if (i1 > i2)
1235 [i1, i2] = [i2, i1];
1236
1237 for (let i = i1; i <= i2; i++)
1238 atr[i].odbSelected = true;
1239 }
1240 }
1241 }
1242
1243 // change color of all rows according to selection
1244 for (const e of atr) {
1245 if (e.childNodes.length >= 3) {
1246 e.childNodes[2].style.backgroundColor = e.odbSelected ? '#004CBD' : '';
1247 change_color(e.childNodes[2], e.odbSelected ? '#FFFFFF' : '');
1248 }
1249 }
1250}
1251
1252function context_menu(event) {
1253 // don't show context menu if we are in input box (user wants traditional copy/paste)
1254 if (event.target.tagName === 'INPUT')
1255 return;
1256
1257 event.preventDefault();
1258 event.stopPropagation();
1259
1260 let tr = event.target;
1261 while (tr.tagName !== 'TR')
1262 tr = tr.parentNode;
1263 let tb = getOdbTb(tr);
1264 let odb = tb.odb;
1265 let path = tr.odbPath;
1266
1267 if (!tr.odbSelected)
1268 select_key(event);
1269
1270 let d = document.getElementById('contextMenu');
1271 if (d === null) {
1272 // create menu on the first call
1273
1274 d = document.createElement("div");
1275 d.id = "contextMenu";
1276 d.style.display = "none";
1277 d.style.position = "absolute";
1278 d.className = "msidenav";
1279 d.style.borderRadius = "0";
1280 d.style.border = "2px solid #808080";
1281 d.style.margin = "0";
1282 d.style.backgroundColor = "#F0F0F0";
1283
1284 let cm = document.createElement("div");
1285
1286 let menu = ["Copy key", "Copy plain text", "Delete key", "Rename key...",
1287 "Open in new tab...", "Open in new window..."];
1288 let mDiv;
1289
1290 for (const m of menu) {
1291 mDiv = document.createElement("div");
1292 mDiv.className = 'mmenuitem mmenulink';
1293 mDiv.innerHTML = '<nobr>' + m + '</nobr>';
1294 mDiv.title = m;
1295 cm.appendChild(mDiv);
1296 }
1297
1298 d.appendChild(cm);
1299 document.body.appendChild(d);
1300 }
1301
1302 // set event handler for copy menu
1303 d.childNodes[0].childNodes[0].onmousedown = function () {
1304 d.style.display = 'none';
1305 odb_copy(tb);
1306 }
1307
1308 // set event handler for copy plain text menu
1309 d.childNodes[0].childNodes[1].onmousedown = function () {
1310 d.style.display = 'none';
1311 let selKeys = get_selected_keys(tb);
1312 let paths = [];
1313 let dirFlag = false;
1314 if (selKeys.length === 0) {
1315 paths.push(tb.odb.path);
1316 dirFlag = true;
1317 } else {
1318 for (const k of selKeys)
1319 paths.push(k.path);
1320 }
1321
1322 mjsonrpc_db_copy(paths).then(rpc => {
1323 let text = '';
1324 let dir = '';
1325 let needPath = false;
1326
1327 if (dirFlag) {
1328 dir = tb.odb.path.substring(tb.odb.path.lastIndexOf('/') + 1);
1329 if (dir === '')
1330 dir = 'root';
1331 text += odbASCII(rpc.result.data[0], dir);
1332 } else {
1333
1334 for (let i = 0; i < rpc.result.data.length; i++) {
1335 if (selKeys[i].key.type === TID_KEY) {
1336 text += odbASCII(rpc.result.data[i], paths[i]);
1337 needPath = true;
1338 } else {
1339 if (needPath)
1340 text += odbASCII(rpc.result.data[i], tb.odb.path);
1341 else
1342 text += odbASCII(rpc.result.data[i]);
1343 needPath = false;
1344 }
1345 }
1346 }
1347
1348 let query = "";
1349 if (dirFlag)
1350 query = "Copy ODB directory \"" + dir + "\" to clipboard?";
1351 else if (paths.length === 1)
1352 query = "Copy ODB key \"" + selKeys[0].key.name + "\" to clipboard?";
1353 else
1354 query = "Copy " + paths.length + " ODB keys to clipboard?";
1355
1356 dlgConfirm(query, function (flag, t) {
1357 if (flag) {
1358 if (navigator.clipboard && navigator.clipboard.writeText) {
1359 try {
1360 navigator.clipboard.writeText(text).then( () => {
1361 }).catch(e => dlgAlert(e));
1362 } catch(error) {
1363 dlgAlert(error);
1364 }
1365 }
1366 }
1367 }, text);
1368
1369 }).catch(error => mjsonrpc_error_alert(error));
1370 }
1371
1372 // set event handler for delete key menu
1373 d.childNodes[0].childNodes[2].onmousedown = function () {
1374 d.style.display = 'none';
1375 odb_delete(tr);
1376 }
1377
1378 // set event handler for rename menu
1379 d.childNodes[0].childNodes[3].onmousedown = function () {
1380 d.style.display = 'none';
1381
1382 let elem;
1383 if (tr.key.type === TID_KEY)
1384 elem = tr.childNodes[1].childNodes[2];
1385 else
1386 elem = tr.childNodes[1].childNodes[1];
1387
1388 let key = elem.innerHTML.trim();
1389 tr.oldKey = key;
1390
1391 inline_edit(event,
1392 elem,
1393 key,
1394 do_rename_key,
1395 key.length,
1396 tr.odbPath);
1397 }
1398
1399 // set event handler for open in new tab
1400 d.childNodes[0].childNodes[4].onmousedown = function () {
1401 d.style.display = 'none';
1402
1403 let url = window.location.href;
1404 if (url.search("&odb_path") !== -1)
1405 url = url.slice(0, url.search("&odb_path"));
1406 url += "&odb_path=" + encodeURIComponent(tr.odbPath); // convert spaces to %20 etc
1407
1408 window.open(url, '_blank').focus();
1409 }
1410
1411 // set event handler for open in new window
1412 d.childNodes[0].childNodes[5].onmousedown = function () {
1413 d.style.display = 'none';
1414
1415 let url = window.location.href;
1416 if (url.search("&odb_path") !== -1)
1417 url = url.slice(0, url.search("&odb_path"));
1418 url += "&odb_path=" + encodeURIComponent(tr.odbPath); // convert spaces to %20 etc
1419
1420 window.open(url, "", "menubar=yes,location=yes,status=yes");
1421 }
1422
1423 let rect = event.target.getBoundingClientRect();
1424
1425 d.style.display = 'block';
1426 d.style.left = (event.offsetX + rect.left + window.scrollX) + 'px';
1427 d.style.top = (event.offsetY+rect.top + window.scrollY) + 'px';
1428
1429 if (parseInt(d.style.left) + d.offsetWidth > document.body.clientWidth)
1430 d.style.left = (document.body.clientWidth - d.offsetWidth) + 'px';
1431
1432 return false;
1433}
1434
1435function expand(e) {
1436 let tb = getOdbTb(e);
1437 let odb = tb.odb;
1438
1439 let rarr = document.getElementById('expRight');
1440 let cells = document.getElementsByName('odbExt');
1441 let table = document.getElementById('odbTable');
1442 odb.detailsColumn = !odb.detailsColumn;
1443
1444 if (odb.detailsColumn) {
1445 for (const d of cells)
1446 d.style.display = 'table-cell';
1447 rarr.style.display = 'none';
1448 table.style.minWidth = '800px';
1449 } else {
1450 for (const d of cells)
1451 d.style.display = 'none';
1452 rarr.style.display = 'inline';
1453 table.style.minWidth = '600px';
1454 }
1455 odb.skip_yellow = true;
1456 odb_update(tb);
1457}
1458
1459function clear_expanded(odb) {
1460 for (const key of odb.key) {
1461 if (key.type === TID_KEY)
1462 key.subdir_open = false;
1463 else
1464 key.expanded = false;
1465 }
1466}
1467
1468function toggle_handles(e) {
1469 let tb = getOdbTb(e);
1470 let odb = tb.odb;
1471
1472 // clear all expanded flags
1473 clear_expanded(odb);
1474 odb_print_all(tb);
1475
1476 let n = document.getElementsByName('odbHandle');
1477 odb.handleColumn = !odb.handleColumn;
1478
1479 if (odb.handleColumn) {
1480 odb.subdirFirst = false;
1481 for (const d of n) {
1482 let tr = d.parentNode;
1483 if (tr.firstChild.className !== 'colHeader') {
1484 tr.setAttribute('draggable', true);
1485 tr.addEventListener('dragstart', drag_start);
1486 tr.addEventListener('dragover', drag_move);
1487 tr.addEventListener('dragend', drag_end);
1488 }
1489
1490 d.style.display = 'table-cell';
1491 }
1492 } else {
1493 odb.subdirFirst = true;
1494 for (const d of n) {
1495 let tr = d.parentNode;
1496 if (tr.firstChild.className !== 'colHeader') {
1497 tr.removeAttribute('draggable');
1498 tr.removeEventListener('dragstart', drag_start);
1499 tr.removeEventListener('dragover', drag_move);
1500 tr.removeEventListener('dragend', drag_end);
1501 }
1502
1503 d.style.display = 'none';
1504 }
1505 }
1506
1507 odb_update(tb);
1508}
1509
1510function toggle_expanded(e) {
1511 let tr = e;
1512 while (tr.tagName !== 'TR')
1513 tr = tr.parentNode;
1514
1515 if (tr.key.num_values >= 5000 && !tr.key.expanded) {
1516 dlgConfirm("ODB key \""+ tr.key.name + "\" has " + tr.key.num_values +
1517 " array elements. This can take very long. Do you want to continue?", do_toggle_expanded, tr);
1518 } else
1519 do_toggle_expanded(true, tr);
1520}
1521
1522function do_toggle_expanded(flag, tr) {
1523 if (flag) {
1524 let tb = getOdbTb(tr);
1525 tr.key.expanded = !tr.key.expanded;
1526 unselect_all_keys(tb);
1527 unselect_all_array_elements(tb);
1528 odb_print_all(tb);
1529 }
1530}
1531
1532function odb_delete(e) {
1533
1534 let tb = getOdbTb(e);
1535 let currentPath = tb.odb.path;
1536 let paths = [];
1537 for (const tr of tb.childNodes) {
1538 if (tr.odbSelected)
1539 paths.push(tr.odbPath);
1540 }
1541
1542 if (paths.length == 0) {
1543 // strip last directory from current path
1544 let newPath = currentPath;
1545 if (newPath.lastIndexOf('/') !== -1)
1546 newPath = newPath.substring(0, newPath.lastIndexOf('/'));
1547 if (newPath === "")
1548 newPath = '/';
1549
1550 dlgConfirm('Are you sure to delete odb directory \"' + currentPath + '\" ?',
1551 do_odb_delete, {"e": e, "paths": [currentPath], "newPath": newPath} );
1552 } else if (paths.length == 1)
1553 dlgConfirm('Are you sure to delete odb key \"' + paths[0] + '\" ?',
1554 do_odb_delete, {"e": e, "paths": paths});
1555 else
1556 dlgConfirm('Are you sure to delete ' + paths.length + ' keys ?',
1557 do_odb_delete, {"e": e, "paths": paths});
1558}
1559
1560function do_odb_delete(flag, param) {
1561 if (flag) {
1562 mjsonrpc_db_delete(param.paths).then((rpc) => {
1563 // check status
1564 if (rpc.result.status[0] === 318) {
1565 dlgAlert("ODB key is write protected and cannot be deleted");
1566 } else if (rpc.result.status[0] === 320) {
1567 dlgAlert("ODB key is locked by other program and cannot be deleted");
1568 } else if (rpc.result.status[0] !== 1) {
1569 dlgAlert("ODB error: " + rpc.result.status[0]);
1570 } else {
1571 if (param.newPath)
1572 subdir_goto(param.e, param.newPath);
1573 else {
1574 let tb = getOdbTb(param.e);
1575 odb_update(tb);
1576 }
1577 }
1578 }).catch(error => mjsonrpc_error_alert(error));
1579 }
1580}
1581
1582function rename_key(element) {
1583 let tb = getOdbTb(element);
1584 let n = 0;
1585 let tr;
1586 for (const t of tb.childNodes) {
1587 if (t.odbSelected) {
1588 n++;
1589 tr = t;
1590 }
1591 }
1592
1593 if (n === 0)
1594 dlgAlert("Please select key to be renamed");
1595 else if (n > 1)
1596 dlgAlert("Please select only single key to be renamed");
1597 else {
1598 tr.odbSelected = false;
1599 tr.style.backgroundColor = '';
1600 change_color(tr, '');
1601
1602 let elem;
1603
1604 if (tr.key.type === TID_KEY)
1605 elem = tr.childNodes[1].childNodes[2];
1606 else
1607 elem = tr.childNodes[1].childNodes[1];
1608
1609 let key = elem.innerHTML;
1610 tr.oldKey = key;
1611
1612 inline_edit(undefined,
1613 elem,
1614 key,
1615 do_rename_key,
1616 key.length,
1617 tr.odbPath);
1618 }
1619}
1620
1621function do_rename_key(p, str, path) {
1622 if (str === '') {
1623 dlgAlert("Empty name not allowed");
1624 return;
1625 }
1626 str = str.trim();
1627 mjsonrpc_call("db_rename", { "paths": [path], "new_names": [str]})
1628 .then()
1629 .catch(error => mjsonrpc_error_alert(error));
1630
1631 let old = p.parentElement.parentElement.oldKey;
1632
1633 p.innerHTML = p.innerHTML.replace(old, str);
1634}
1635
1636function search_key() {
1637 dlgQuery("Enter key name (substring case insensitive):", "", do_search_key);
1638}
1639
1640function do_search_key(str) {
1641 if (str !== false)
1642 window.location.href = "?cmd=Find&value=" + str;
1643}
1644
1645function odb_copy(e) {
1646 let tb = getOdbTb(e);
1647 let selKeys = get_selected_keys(tb);
1648 let paths = [];
1649 let dirFlag = false;
1650 if (selKeys.length === 0) {
1651 paths.push(tb.odb.path);
1652 dirFlag = true;
1653 } else {
1654 for (const k of selKeys)
1655 paths.push(k.path);
1656 }
1657 mjsonrpc_db_copy(paths).then(rpc => {
1658 let text = '';
1659
1660 // generate text from objects, add top directoy which is missing from db_copy
1661 for (const [i,d] of rpc.result.data.entries()) {
1662 if (i === 0) {
1663 if (dirFlag) {
1664 let dir = tb.odb.path.substring(tb.odb.path.lastIndexOf('/')+1);
1665 if (dir === '')
1666 dir = 'root';
1667 text += '{ \"'+ dir +
1668 '\": '+JSON.stringify(d, null, ' ')+'}';
1669 } else if (selKeys[i].key.type === TID_KEY) {
1670 text += '{ \"'+ selKeys[i].key.name +'\": '+JSON.stringify(d, null, ' ')+'}';
1671 } else
1672 text += JSON.stringify(d, null, ' ');
1673 } else
1674 text += JSON.stringify(d, null, ' ').substring(1); // strip first '{'
1675 if (i < rpc.result.data.length - 1)
1676 text = text.substring(0, text.lastIndexOf('}')) + ','; // replace last '}' by ','
1677 }
1678
1679 let query = "";
1680 if (dirFlag)
1681 query = "Copy ODB directory \"" + paths + "\" to clipboard?";
1682 else {
1683 if (paths.length === 1)
1684 query = "Copy ODB key \"" + paths + "\" to clipboard?";
1685 else
1686 query = "Copy " + paths.length + " ODB keys to clipboard?";
1687 }
1688
1689 dlgConfirm(query, function (flag, t) {
1690 if (flag && navigator.clipboard && navigator.clipboard.writeText) {
1691 console.log(t);
1692 try {
1693 navigator.clipboard.writeText(t).then( () => {
1694 }).catch(e => dlgAlert(e));
1695 } catch(error) {
1696 dlgAlert(error);
1697 }
1698 }
1699 }, text);
1700
1701 // store text locally as a backup
1702 try {
1703 sessionStorage.setItem('odbclip', text);
1704 } catch (error) {
1705 dlgAlert(error);
1706 }
1707
1708 }).catch(error => mjsonrpc_error_alert(error));
1709}
1710
1711function odb_paste(e) {
1712 let tb = getOdbTb(e);
1713 let odb = tb.odb;
1714 try {
1715 let text = sessionStorage.getItem('odbclip');
1716 if (text) {
1717 let keys = JSON.parse(text);
1718 odb_paste_keys(tb, keys);
1719 return;
1720 }
1721 } catch (error) {
1722 dlgAlert("Invalid JSON format in clipboard:<br /><br />" + error);
1723 return;
1724 }
1725
1726 if (navigator.clipboard && navigator.clipboard.readText) {
1727 navigator.clipboard.readText().then(text => {
1728 try {
1729 if (text) {
1730 let keys = JSON.parse(text);
1731 odb_paste_keys(tb, keys);
1732 return;
1733 } else {
1734 dlgAlert("Nothing stored in clipboard");
1735 }
1736 } catch (error) {
1737 dlgAlert("Invalid JSON format in clipboard:<br /><br />" + error);
1738 }
1739 });
1740 } else
1741 dlgAlert("Paste not possible in this browser");
1742}
1743
1744function odb_paste_keys(tb, newKeys) {
1745 let odb = tb.odb;
1746
1747 for (const key in newKeys) {
1748 if (key.indexOf('/') !== -1)
1749 continue;
1750
1751 // check for existing key in current keys
1752 let cKeyMaxIndex = 0;
1753 let cKeyIndex = 0;
1754 let cKeyBare = "";
1755 let ckb = "";
1756 let found = false;
1757 for (const cKey of odb.key) {
1758 if (cKey.name.indexOf(' copy') === -1)
1759 ckb = cKey.name;
1760 else
1761 ckb = cKey.name.substring(0, cKey.name.indexOf(' copy'));
1762
1763 if (ckb === key) {
1764 found = true;
1765 cKeyBare = ckb;
1766 if (cKey.name.indexOf(' copy') !== -1) {
1767 cKeyIndex = parseInt(cKey.name.substring(cKey.name.indexOf(' copy')+5));
1768 if (isNaN(cKeyIndex))
1769 cKeyIndex = 1;
1770 if (cKeyIndex > cKeyMaxIndex)
1771 cKeyMaxIndex = cKeyIndex;
1772 }
1773 }
1774 }
1775
1776 // rename new key if name exists
1777 if (found) {
1778 let newName = cKeyBare + ' copy';
1779 if (cKeyIndex > 0)
1780 newName += ' ' + (cKeyIndex+1).toString();
1781
1782 newKeys[newName] = newKeys[key];
1783 delete newKeys[key];
1784
1785 newKeys[newName + '/key'] = newKeys[key + '/key'];
1786 delete newKeys[key + '/key'];
1787 }
1788 }
1789
1790 mjsonrpc_db_paste([odb.path], [newKeys]).then(rpc =>
1791 odb_update(tb)).catch(error =>
1792 dlgAlert(error));
1793
1794}
1795
1796function odbASCII(o, path) {
1797 // convert ODB keys to plain ASCII representation
1798 let t = "";
1799 let need_path = true;
1800 if (path === undefined)
1801 need_path = false;
1802 for (const key in o) {
1803 if (key.indexOf('/') !== -1)
1804 continue;
1805
1806 if (o[key+'/key'] === undefined && path) {
1807 t += odbASCII(o[key], path + '/' + key);
1808 need_path = true;
1809 continue;
1810 }
1811
1812 if (need_path) {
1813 need_path = false;
1814 t += '\n[' + path + ']\n';
1815 }
1816
1817 let tid = o[key+'/key'].type;
1818 let num_values = o[key+'/key'].num_values;
1819 t += key;
1820
1821 if (num_values > 1) {
1822 t += ":\n";
1823 for (let i=0 ; i<num_values ; i++) {
1824 t += '[' + i + ']\t';
1825 t += mie_to_string(tid, o[key][i]);
1826 t += '\n';
1827 }
1828 } else {
1829 t += ":\t";
1830 t += mie_to_string(tid, o[key]);
1831 t += '\n';
1832 }
1833 }
1834 return t;
1835}
1836
1837function odb_export(e) {
1838 let tb = getOdbTb(e);
1839 mjsonrpc_db_copy([tb.odb.path]).then(rpc => {
1840
1841 let dirs = tb.odb.path.split('/');
1842 let filename = dirs[dirs.length - 1];
1843 if (filename === '')
1844 filename = 'root';
1845 filename += ".json";
1846
1847 let header = {
1848 "/MIDAS version": "2.1",
1849 "/filename": filename,
1850 "/ODB path": tb.odb.path
1851 }
1852 header = JSON.stringify(header, null, ' ');
1853 header = header.substring(0, header.length-2) + ','; // strip trailing '}'
1854
1855 let odbJson = JSON.stringify(rpc.result.data[0], null, ' ');
1856 if (odbJson.indexOf('{') === 0)
1857 odbJson = odbJson.substring(1); // strip leading '{'
1858
1859 odbJson = header + odbJson;
1860
1861 // use trick from FileSaver.js
1862 let a = document.getElementById('downloadHook');
1863 if (a === null) {
1864 a = document.createElement("a");
1865 a.style.display = "none";
1866 a.id = "downloadHook";
1867 document.body.appendChild(a);
1868 }
1869
1870 let blob = new Blob([odbJson], {type: "text/json"});
1871 let url = window.URL.createObjectURL(blob);
1872
1873 a.href = url;
1874 a.download = filename;
1875 a.click();
1876 window.URL.revokeObjectURL(url);
1877 dlgAlert("ODB subtree \"" + tb.odb.path +
1878 "\" downloaded to file \"" + filename + "\"");
1879
1880 }).catch(error => mjsonrpc_error_alert(error));
1881}
1882
1883let _odb_path;
1884function odb_save_picker(e) {
1885 let tb = getOdbTb(e);
1886 _odb_path = tb.odb.path;
1887 file_picker("", "*.json", odb_save, true);
1888}
1889function odb_save(filename) {
1890 mjsonrpc_db_copy([_odb_path]).then(rpc => {
1891
1892 if (filename.indexOf('.json') === -1)
1893 filename += '.json';
1894
1895 let header = {
1896 "/MIDAS version": "2.1",
1897 "/filename": filename,
1898 "/ODB path": _odb_path
1899 }
1900 header = JSON.stringify(header, null, ' ');
1901 header = header.substring(0, header.length-2) + ','; // strip trailing '}'
1902
1903 let odbJson = JSON.stringify(rpc.result.data[0], null, ' ');
1904 if (odbJson.indexOf('{') === 0)
1905 odbJson = odbJson.substring(1); // strip leading '{'
1906
1907 odbJson = header + odbJson;
1908
1909 file_save_ascii(filename, odbJson, "ODB subtree \"" + _odb_path +
1910 "\" saved to file \"" + filename + "\"");
1911
1912 }).catch(error => mjsonrpc_error_alert(error));
1913}
1914
1915async function odb_import(e) {
1916 try {
1917 // Check if showOpenFilePicker is available in the browser
1918 // The "else" is more of a JS standard
1919 if (window.showOpenFilePicker) {
1920 let fileHandle;
1921 [fileHandle] = await window.showOpenFilePicker();
1922 const file = await fileHandle.getFile();
1923 const text = await file.text();
1924 pasteBuffer(e, text, file.name);
1925 } else {
1926 // Show a traditional file input element
1927 const fileInput = document.createElement('input');
1928 fileInput.type = 'file';
1929 fileInput.accept = 'application/JSON';
1930 fileInput.addEventListener('change', (event) => {
1931 const file = event.target.files[0];
1932 if (file) {
1933 const reader = new FileReader();
1934 reader.onload = async (evObj) => {
1935 const text = evObj.target.result;
1936 pasteBuffer(e, text, file.name);
1937 };
1938 reader.readAsText(file);
1939 }
1940 });
1941 // Trigger the file input element
1942 fileInput.click();
1943 }
1944 } catch (error) {
1945 if (error.name !== 'AbortError') {
1946 // Handle errors as needed
1947 dlgAlert(error);
1948 }
1949 }
1950}
1951
1952function loadFileFromSelector(e) {
1953
1954 let input = document.getElementById('fileSelector');
1955 let file = input.files[0];
1956 if (file !== undefined) {
1957 let reader = new FileReader();
1958 reader.readAsText(file);
1959
1960 reader.onerror = function () {
1961 dlgAlert('File read error: ' + reader.error);
1962 };
1963
1964 reader.onload = function () {
1965 pasteBuffer(e.param, reader.result, file.name);
1966 };
1967 }
1968}
1969
1970function odb_load(e) {
1971 let tb = getOdbTb(e);
1972 let path = tb.odb.path;
1973 _odb_path = tb.odb.path;
1974 file_picker("", "*.json", loadFile);
1975}
1976
1977function loadFile(filename) {
1978 file_load_ascii(filename, (text) => pasteBuffer(undefined, text, filename));
1979}
1980
1981function pasteBuffer(e, text, filename) {
1982 let tb;
1983 let path;
1984
1985 if (e === undefined)
1986 path = _odb_path;
1987 else {
1988 tb = getOdbTb(e);
1989 path = tb.odb.path;
1990 }
1991
1992 let odbJson;
1993 try {
1994 odbJson = JSON.parse(text);
1995 } catch (error) {
1996 dlgAlert(error);
1997 return;
1998 }
1999 // delete /MIDAS version, /filename etc.
2000 for (let [name, value] of Object.entries(odbJson)) {
2001 if (name[0] === '/')
2002 delete odbJson[name];
2003 }
2004 mjsonrpc_db_paste([path], [odbJson]).then(rpc =>
2005 dlgAlert("File \"" + filename + "\" successfully loaded to ODB at " + path)).catch(error =>
2006 mjsonrpc_error_alert(error));
2007}
2008
2009function make_dialog_scroll(tb) {
2010 let d = tb.parentElement.parentElement.parentElement.parentElement;
2011
2012 if (tb.odb.picker)
2013 d.style.position = "absolute"; // remove "fixed"
2014}
2015
2016function subdir_open_click(event, e) {
2017 event.stopPropagation();
2018 let tr = e.parentNode;
2019 while (tr.tagName !== 'TR')
2020 tr = tr.parentNode;
2021 let path = tr.odbPath;
2022
2023 subdir_open(event.target, path);
2024}
2025
2026// user clicks on subdirectory, so open it
2027function subdir_open(tr, path) {
2028 while (tr.tagName !== 'TR')
2029 tr = tr.parentNode;
2030 let tb = getOdbTb(tr);
2031 let odb = tb.odb;
2032
2033 // don't open subdirs if in reorder mode
2034 if (odb.handleColumn)
2035 return;
2036
2037 unselect_all_keys(tb);
2038 unselect_all_array_elements(tb);
2039 make_dialog_scroll(tb);
2040
2041 // find key belonging to 'path'
2042 tr.key.subdir_open = !tr.key.subdir_open;
2043 tb.odb.skip_yellow = true;
2044 odb_update(tb);
2045}
2046
2047function subdir_goto_click(event, e) {
2048 event.stopPropagation();
2049 let tr = e.parentNode;
2050 while (tr.tagName !== 'TR')
2051 tr = tr.parentNode;
2052 let path = tr.odbPath;
2053
2054 // don't open subdir if we are in rename mode
2055 if (tr.childNodes[1].childNodes[2].odbParam &&
2056 tr.childNodes[1].childNodes[2].odbParam.inEdit)
2057 return;
2058
2059 subdir_goto(event.target, path);
2060}
2061
2062function subdir_goto(e, path) {
2063 let tb = getOdbTb(e);
2064 let odb = tb.odb;
2065
2066 unselect_all_keys(tb);
2067 unselect_all_array_elements(tb);
2068
2069 // kill old timer
2070 if (odb.updateTimer !== undefined)
2071 window.clearTimeout(odb.updateTimer);
2072
2073 // update URL
2074 if (!odb.picker) {
2075 let url = window.location.href;
2076 if (url.search("&odb_path") !== -1)
2077 url = url.slice(0, url.search("&odb_path"));
2078 url += "&odb_path=" + encodeURIComponent(path); // convert spaces to %20 etc
2079 let skip = false;
2080
2081 // "cmd=ODB" vs. "cmd=ODB&odb_path=/"
2082 if (window.location.href.indexOf('&') === -1 &&
2083 path === '/')
2084 skip = true;
2085
2086 if (url !== window.location.href && !skip)
2087 window.history.pushState({'path': path}, '', url);
2088 }
2089
2090 // modify title
2091 if (document.title.indexOf("ODB") !== -1) {
2092 if (path === '/')
2093 document.title = "ODB";
2094 else
2095 document.title = "ODB " + path;
2096 }
2097
2098 // update ODB object
2099 odb.path = path;
2100 odb.level = 0;
2101 odb.dataIndex = 0;
2102 odb.key = [];
2103 odb.skip_yellow = true;
2104
2105 odb_update(tb);
2106}
2107
2108// push all paths of open subdirectories recursively for a following db_ls
2109function push_paths(paths, odb, path) {
2110 odb.dataIndex = paths.length;
2111 paths.push(path);
2112 for (let i=0 ; i< odb.key.length ; i++) {
2113 if (odb.key[i].type === TID_KEY && odb.key[i].subdir_open) {
2114 if (odb.key[i].value.id === undefined) {
2115 odb.key[i].value = {
2116 id: odb.id,
2117 path: odb.path === '/' ? '/' + odb.key[i].name : odb.path + '/' + odb.key[i].name,
2118 level: odb.level + 1,
2119 dataIndex: paths.length,
2120 key: []
2121 };
2122 }
2123 push_paths(paths, odb.key[i].value, odb.key[i].value.path);
2124 }
2125 }
2126}
2127
2128// update overall ODB display
2129function odb_update(tb) {
2130
2131 // console.log("odb_update" + new Date());
2132 let odb;
2133 if (tb === undefined)
2134 tb = document.getElementById("odbTable").firstChild;
2135 odb = tb.odb;
2136
2137 if (odb.updateTimer !== undefined)
2138 window.clearTimeout(odb.updateTimer);
2139 let row = { i:4, nvk:0 };
2140
2141 // show clickable ODB path in top row
2142 let dirs;
2143 if (odb.path === '/')
2144 dirs = ['/'];
2145 else
2146 dirs = odb.path.split('/');
2147 let path = '';
2148 let s = "<a href=\"#\" onclick=\"subdir_goto(this, '/');return false;\" title=\"Root directory\">" +
2149 "<img src=\"icons/slash-square.svg\" style=\"border:none; height: 16px; vertical-align: middle;margin-bottom: 2px;\">" +
2150 "</a>&nbsp;";
2151 for (let i=1 ; i<dirs.length ; i++) {
2152 path += "/" + dirs[i];
2153 s += "<a href=\"#\" onclick=\"subdir_goto(this, '" + escapeHTML(path) + "');return false;\">"+escapeHTML(dirs[i])+"</a>";
2154 if (i < dirs.length - 1)
2155 s += "&nbsp;/&nbsp;";
2156 }
2157 if (s !== tb.childNodes[1].firstChild.innerHTML)
2158 tb.childNodes[1].firstChild.innerHTML = s;
2159
2160 // request ODB data
2161 let paths = [];
2162 push_paths(paths, odb, odb.path);
2163
2164 mjsonrpc_db_ls(paths).then(rpc => {
2165 odb_extract(odb, rpc.result.data);
2166 odb_print(tb, row, tb.odb);
2167 odb.skip_yellow = false;
2168
2169 // call timer in one second to update again
2170 odb.updateTimer = window.setTimeout(odb_update, 1000, tb);
2171 }).catch();
2172}
2173
2174// extract keys and put them into odb tree from db_ls result in dataArray
2175function odb_extract(odb, dataArray) {
2176 let data = dataArray[odb.dataIndex];
2177 let n = 0;
2178 for (const item in data) {
2179 if (item.indexOf('/') !== -1)
2180 continue;
2181
2182 let key = {};
2183 key.name = item;
2184 key.value = data[item];
2185 let linkFlag = data[item + '/key'] !== undefined && data[item + '/key'].link !== undefined;
2186 if (!linkFlag && typeof key.value === 'object' && Object.keys(key.value).length === 0) {
2187 key.type = TID_KEY;
2188 key.subdir_open = false;
2189 key.num_values = 1;
2190 key.item_size = 0;
2191 key.last_written = 0;
2192 key.access_mode = 0;
2193 } else {
2194 key.type = data[item + '/key'].type;
2195 key.link = data[item + '/key'].link;
2196 key.num_values = data[item + '/key'].num_values;
2197 if (key.num_values === undefined)
2198 key.num_values = 1;
2199 if (data[item + '/key'].item_size !== undefined)
2200 key.item_size = data[item + '/key'].item_size;
2201 else
2202 key.item_size = tid_size[key.type];
2203 key.last_written = data[item + '/key'].last_written;
2204 key.access_mode = data[item + '/key'].access_mode;
2205 }
2206 key.options = (key.name.substring(0, 7) === "Options");
2207
2208 if (odb.key.length <= n) {
2209 key.expanded = (key.num_values > 1 && key.num_values <= 10);
2210 if (odb.picker)
2211 key.expanded = false;
2212 odb.key.push(key);
2213 } else {
2214 if (odb.key[n].subdir_open) {
2215 key.subdir_open = true;
2216 key.value = odb.key[n].value;
2217 }
2218 if (odb.key[n].expanded !== undefined) {
2219 key.expanded = odb.key[n].expanded;
2220 }
2221 odb.key[n] = key;
2222 }
2223 n++;
2224
2225 // if current key is a subdirectory, call us recursively
2226 if (key.type === TID_KEY && key.subdir_open) {
2227 odb.key[n-1].value.picker = odb.picker;
2228 odb_extract(odb.key[n-1].value, dataArray);
2229 }
2230 }
2231
2232 // if local data remaining, ODB key must have been deleted, so delete also local data
2233 if (odb.key.length > n) {
2234 odb.key = odb.key.slice(0, n);
2235 odb.skip_yellow = true;
2236 }
2237}
2238
2239function odb_print_all(tb) {
2240 let odb = tb.odb;
2241 let row = { i:4, nvk:0 };
2242 let nValueKeys = 0;
2243 odb.skip_yellow = true;
2244 odb_print(tb, row, tb.odb);
2245}
2246
2247function odb_print(tb, row, odb) {
2248
2249 // first print all directories
2250 if (odb.subdirFirst) {
2251 for (const key of odb.key) {
2252 if (key.type !== TID_KEY)
2253 continue;
2254
2255 // Print current key
2256 odb_print_key(tb, row, odb.path, key, odb.level);
2257 row.i++;
2258
2259 // Print whole subdirectory if open
2260 if (key.subdir_open) {
2261
2262 // Propagate flags to all subkeys
2263 key.value.skip_yellow = odb.skip_yellow;
2264 key.value.subdirFirst = odb.subdirFirst;
2265
2266 // Print whole subdirectory
2267 odb_print(tb, row, key.value);
2268 }
2269 }
2270 }
2271
2272 // now print all keys
2273 for (const key of odb.key) {
2274
2275 if (odb.subdirFirst)
2276 if (key.type === TID_KEY)
2277 continue;
2278
2279 // search for "Options <key>"
2280 let options = odb.key.find(o => o.name === 'Options ' + key.name);
2281
2282 // Print current key
2283 odb_print_key(tb, row, odb.path, key, odb.level, options ? options.value : undefined);
2284 row.i++;
2285 row.nvk++;
2286 }
2287
2288 // Hide 'value' if only directories are listed
2289 let ds = document.getElementsByName('valueHeader');
2290 for (d of ds)
2291 if (tb.contains(d))
2292 d.style.display = row.nvk > 0 ? 'table-cell' : 'none';
2293
2294 // At the end, remove old rows if subdirectory has been closed
2295 if (odb.level === 0)
2296 while (tb.childNodes.length > row.i)
2297 tb.removeChild(tb.childNodes[tb.childNodes.length-1]);
2298}
2299
2300function find_input_element(e) {
2301 if (e === undefined)
2302 return false;
2303 for (const c of e.childNodes) {
2304 if (c.tagName === 'INPUT')
2305 return true;
2306 if (c.tagName === 'SELECT' && e.inEdit)
2307 return true;
2308 if (c.childNodes.length > 0)
2309 if (find_input_element(c))
2310 return true;
2311 }
2312 return false;
2313}
2314
2315function bnToHex(val) {
2316 let hex;
2317 if (val < 0) {
2318 hex = ((1n << 64n) + BigInt(val)).toString(16).toUpperCase();
2319 hex = hex.padStart(16, '0');
2320 } else {
2321 hex = val.toString(16).toUpperCase();
2322 if (hex.length % 2) {
2323 hex = '0' + hex;
2324 }
2325 }
2326
2327 return hex;
2328}
2329
2330function odb_print_key(tb, row, path, key, level, options) {
2331 let odb = tb.odb;
2332
2333 // ful path to key
2334 let keyPath = (path === '/' ? '/' + key.name : path + '/' + key.name);
2335
2336 // create empty row
2337 let tr = document.createElement('TR');
2338
2339 // Handle column
2340 let td = document.createElement('TD');
2341 td.setAttribute('name', 'odbHandle');
2342 td.style.display = odb.handleColumn ? 'table-cell' : 'none';
2343 td.style.width = "10px";
2344 tr.appendChild(td);
2345 td.innerHTML = '<img style="cursor: all-scroll; height: 13px; padding: 0; border: 0" src="icons/menu.svg">';
2346
2347 td.childNodes[0].setAttribute('draggable', false);
2348
2349 // Key name
2350 td = document.createElement('TD');
2351 tr.appendChild(td);
2352
2353 // Add three spaces of indent for each level
2354 let indent = "<span>";
2355 for (let i = 0; i < level; i++)
2356 indent += "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;";
2357 indent += "</span>";
2358
2359 // Remember indent level for key rename
2360 tr.level = odb.level;
2361
2362 // Remember key (need to toggle expanded flag)
2363 tr.key = key;
2364
2365 // Remember key path
2366 tr.odbPath = keyPath;
2367
2368 // Set special class for "Options ..." keys
2369 if (key.options && !odb.detailsColumn)
2370 tr.style.display = 'none';
2371
2372 // Print subdir with open subdir handler
2373 if (key.type === TID_KEY) {
2374 if (odb.handleColumn) {
2375 // do not show any links in re-order mode
2376 td.innerHTML = indent + "&nbsp;\u25B8&nbsp;" + escapeHTML(key.name);
2377 } else {
2378 let handler = "onclick=\"subdir_open_click(event, this);return false;\" ";
2379
2380 if (key.subdir_open)
2381 td.innerHTML = indent + "<a href='#' " +
2382 handler +
2383 ">&nbsp;\u25BE&nbsp;</a>";
2384 else
2385 td.innerHTML = indent + "<a href='#' " +
2386 handler +
2387 ">&nbsp;\u25B8&nbsp;</a>";
2388
2389 handler = "onclick=\"subdir_goto_click(event, this);return false;\" ";
2390 td.innerHTML += "<a href='#' " + handler + ">" + escapeHTML(key.name) + "</a>";
2391 }
2392
2393 if (key.link) {
2394 if (odb.picker)
2395 td.innerHTML += ' &rarr; <span>' + key.link + '</span>';
2396 else
2397 td.innerHTML += ' &rarr; <span>'+ '<a href="#" ' +
2398 'onclick="inline_edit(event, this.parentNode, \'' +
2399 key.link + '\', odb_setlink, undefined, \'' + keyPath +
2400 '\'); return false;" ' +
2401 ' title="Change value">' + key.link +
2402 '</a></span>';
2403 }
2404
2405 } else if (key.link === undefined) {
2406 td.innerHTML = indent + '<span>' + escapeHTML(key.name) + '</span>';
2407 } else {
2408 td.innerHTML = indent + '<span>' + escapeHTML(key.name) + '</span>';
2409 if (odb.picker)
2410 td.innerHTML += ' &rarr; <span>' + escapeHTML(key.link) + '</span>';
2411 else
2412 td.innerHTML += ' &rarr; <span>'+ '<a href="#" ' +
2413 'onclick="inline_edit(event, this.parentNode, \'' +
2414 key.link + '\', odb_setlink, undefined, \'' + keyPath +
2415 '\'); return false;" ' +
2416 ' title="Change value">' + escapeHTML(key.link) +
2417 '</a></span>';
2418 }
2419
2420 // Subdirs occupy all 8 columns
2421 if (key.type === TID_KEY) {
2422 td.colSpan = "8";
2423
2424 if (tb.childNodes.length > row.i) {
2425 let selected = tb.childNodes[row.i].odbSelected;
2426 if (selected) {
2427 tr.style.backgroundColor = '#004CBD';
2428 change_color(tr, '#FFFFFF');
2429 }
2430
2431 if (!tb.childNodes[row.i].isEqualNode(tr)) {
2432 // check for edit mode
2433 let inlineEdit = find_input_element(tb.childNodes[row.i]);
2434 if (!inlineEdit) {
2435 if (tb.childNodes[row.i].innerHTML != tr.innerHTML)
2436 tb.childNodes[row.i].innerHTML = tr.innerHTML;
2437 }
2438 }
2439 } else
2440 tb.appendChild(tr);
2441
2442 // Remove and install mouse click handlers
2443 if (tb.childNodes[row.i].childNodes[2] !== undefined) {
2444 tb.childNodes[row.i].childNodes[2].removeEventListener('mousedown', select_array_element);
2445 tb.childNodes[row.i].childNodes[2].removeEventListener('mousemove', select_array_element);
2446 }
2447 tb.childNodes[row.i].addEventListener('mousedown', select_key);
2448 if (!odb.picker) {
2449 tb.childNodes[row.i].addEventListener('mousemove', select_key);
2450 tb.childNodes[row.i].addEventListener('contextmenu', context_menu);
2451 }
2452
2453 // Remember ODB path in row
2454 tb.childNodes[row.i].odbPath = keyPath;
2455 tb.childNodes[row.i].key = key;
2456
2457 // Remove and odbOptions class or invisibility
2458 tb.childNodes[row.i].className = "";
2459 tb.childNodes[row.i].style.display = 'table-row';
2460
2461 return;
2462 }
2463
2464 // Value
2465 td = document.createElement('TD');
2466 tr.appendChild(td);
2467
2468 if (Array.isArray(key.value)) {
2469 if (odb.picker)
2470 td.innerHTML =
2471 '<div style="display: inline;"></div>';
2472 else {
2473 if (key.type === TID_BOOL) {
2474 td.innerHTML =
2475 '<div style="display: inline;">' +
2476 '<select title="Set array elements to same value" ' +
2477 'onchange="odb_setall_key(this, \'' + keyPath + '\', this.value);">' +
2478 '<option value=""></option>' +
2479 '<option value="0">No</option>' +
2480 '<option value="1">Yes</option>' +
2481 '</select>' +
2482 '</div>';
2483 } else
2484 td.innerHTML =
2485 '<div style="display: inline;"><a href="#" ' +
2486 'onclick="inline_edit(event, this.parentNode, \'' +
2487 key.value[0] + '\', odb_setall, 10, \'' + keyPath +
2488 '\');return false;" ' +
2489 'title="Set array elements to same value">*</a></div>';
2490 }
2491
2492 if (key.expanded === false)
2493 td.innerHTML +=
2494 '<div style="display: inline;float: right">' +
2495 '<a href="#"' +
2496 'onclick="toggle_expanded(this.parentNode);return false;" ' +
2497 'title="Show array elements">\u25B8</a>' +
2498 '</div>';
2499 else
2500 td.innerHTML +=
2501 '<div style="display: inline;float: right">' +
2502 '<a href="#"' +
2503 'onclick="toggle_expanded(this.parentNode);return false;" ' +
2504 'title="Hide array elements">\u25BE</a>' +
2505 '</div>';
2506 } else {
2507
2508 if (key.type === TID_BOOL) {
2509
2510 let h = "<select onchange='odb_setoption(this)' " +
2511 " onfocus='option_edit(event, this, true)'" +
2512 " onblur='option_edit(event, this, false)'" +
2513 "'>\n";
2514
2515 if (key.value === false) {
2516 h += "<option selected value='" + 0 + "'>No</option>";
2517 h += "<option value='" + 1 + "'>Yes</option>";
2518 } else {
2519 h += "<option value='" + 0 + "'>No</option>";
2520 h += "<option selected value='" + 1 + "'>Yes</option>";
2521 }
2522 h += "</select>\n";
2523 td.innerHTML = h;
2524
2525 } else if (options) {
2526
2527 let oc = options.slice(); // make copy of all elements
2528
2529 let h = "<select onchange='odb_setoption(this)' " +
2530 " onfocus='option_edit(event, this, true)'" +
2531 " onblur='option_edit(event, this, false)'" +
2532 "'>\n";
2533
2534 // check if current value is in options, add it if not
2535 if (!oc.includes(key.value))
2536 oc.unshift(key.value);
2537
2538 for (const o of oc) {
2539 if (key.value === o)
2540 h += "<option selected value='" + o + "'>" + o + "</option>";
2541 else
2542 h += "<option value='" + o + "'>" + o + "</option>";
2543 }
2544 h += "</select>\n";
2545 td.innerHTML = h;
2546
2547 } else {
2548
2549 let edit = '<a href="#" onclick="this.parentNode.dataset.format=\'\';' +
2550 'this.parentNode.refreshPage=odb_update;' +
2551 'ODBInlineEdit(this.parentNode, \'' + keyPath + '\');return false;" ' +
2552 'onfocus="ODBInlineEdit(this.parentNode, \'' + keyPath + '\')" title="Change value">';
2553 let editHex = '<a href="#" onclick="this.parentNode.dataset.format=\'x\';' +
2554 'this.parentNode.refreshPage=odb_update;' +
2555 'ODBInlineEdit(this.parentNode, \'' + keyPath + '\');return false;" ' +
2556 'onfocus="ODBInlineEdit(this.parentNode, \'' + keyPath + '\')" title="Change hex value">';
2557
2558 let v = escapeHTML(key.value.toString());
2559 let vHex = "";
2560 let hexValue = false;
2561 if (key.type === TID_STRING && v === "")
2562 v = "(empty)";
2563 else if (key.type === TID_STRING && v.trim() === "")
2564 v = "(spaces)";
2565 else if (key.type === TID_LINK)
2566 v = '<span style="color:red;background-color:yellow">(cannot resolve link)</span>';
2567 else if (v.substring(0, 2) === "0x") {
2568 hexValue = true;
2569 vHex = "0x" + v.substring(2).toUpperCase();
2570 if (key.type === TID_UINT64)
2571 v = BigInt(key.value).toString();
2572 else
2573 v = parseInt(key.value);
2574 } else if (key.type !== TID_STRING && key.type !== TID_LINK && key.type !== TID_FLOAT && key.type !== TID_DOUBLE) {
2575 if (key.type === TID_INT8)
2576 vHex = "0x" + (key.value & 0xFF).toString(16).toUpperCase();
2577 else if (key.type === TID_INT16)
2578 vHex = "0x" + (key.value & 0xFFFF).toString(16).toUpperCase();
2579 else if (key.type === TID_INT32)
2580 vHex = "0x" + (key.value >>> 0).toString(16).toUpperCase();
2581 else if (key.type === TID_INT64)
2582 vHex = "0x" + bnToHex(BigInt(key.value));
2583 else
2584 vHex = "0x" + key.value.toString(16).toUpperCase();
2585 }
2586 if (odb.picker || odb.handleColumn)
2587 td.innerHTML = v + vHex;
2588 else {
2589 if (vHex === "")
2590 td.innerHTML = edit + v + '</a>';
2591 else {
2592 if (hexValue)
2593 td.innerHTML = editHex + vHex + '</a> ' + ' (' + edit + v + '</a>)';
2594 else
2595 td.innerHTML = edit + v + '</a> ' + ' (' + editHex + vHex + '</a>)';
2596 }
2597 }
2598 }
2599 }
2600
2601 // Type
2602 td = document.createElement('TD');
2603 td.setAttribute('name', 'odbExt');
2604 td.style.display = odb.detailsColumn ? 'table-cell' : 'none';
2605 tr.appendChild(td);
2606 td.appendChild(document.createTextNode(tid_name[key.type]));
2607
2608 // #Val
2609 td = document.createElement('TD');
2610 td.setAttribute('name', 'odbExt');
2611 td.style.display = odb.detailsColumn ? 'table-cell' : 'none';
2612 tr.appendChild(td);
2613 td.innerHTML = '<a href="#" onclick="resize_array(event, \''+keyPath+'\','+key.num_values+');return false;">'
2614 + key.num_values + '</a>';
2615
2616 // Size
2617 td = document.createElement('TD');
2618 td.setAttribute('name', 'odbExt');
2619 td.style.display = odb.detailsColumn ? 'table-cell' : 'none';
2620 tr.appendChild(td);
2621 if (key.type === TID_STRING)
2622 td.innerHTML = '<a href="#" onclick="resize_string(event, \''+keyPath+'\','+key.item_size+','+key.num_values+');return false;">'
2623 + key.item_size + '</a>';
2624 else
2625 td.innerHTML = key.item_size;
2626
2627 // Written
2628 td = document.createElement('TD');
2629 td.setAttribute('name', 'odbExt');
2630 td.style.display = odb.detailsColumn ? 'table-cell' : 'none';
2631 tr.appendChild(td);
2632 let s = Math.floor(0.5 + new Date().getTime() / 1000 - key.last_written);
2633 let t;
2634 if (s < 60)
2635 t = s + 's';
2636 else if (s < 60 * 60)
2637 t = Math.floor(0.5 + s / 60) + 'm';
2638 else if (s < 60 * 60 * 24)
2639 t = Math.floor(0.5 + s / 60 / 60) + 'h';
2640 else if (s < 60 * 60 * 24 * 99)
2641 t = Math.floor(0.5 + s / 60 / 60 / 24) + 'd';
2642 else
2643 t = ">99d";
2644 td.appendChild(document.createTextNode(t));
2645
2646 // Mode
2647 td = document.createElement('TD');
2648 td.setAttribute('name', 'odbExt');
2649 td.style.display = odb.detailsColumn ? 'table-cell' : 'none';
2650 tr.appendChild(td);
2651 let mode = key.access_mode;
2652 let m = "";
2653 if (mode & MODE_READ)
2654 m += "R";
2655 if (mode & MODE_WRITE)
2656 m += "W";
2657 if (mode & MODE_DELETE)
2658 m += "D";
2659 if (mode & MODE_EXCLUSIVE)
2660 m += "X";
2661 if (mode & MODE_WATCH)
2662 m += "W";
2663 td.appendChild(document.createTextNode(m));
2664
2665 // invert color if selected
2666 if (tb.childNodes.length > row.i) {
2667 let selected = tb.childNodes[row.i].odbSelected;
2668 if (selected) {
2669 tr.style.backgroundColor = '#004CBD';
2670 change_color(tr, '#FFFFFF');
2671 }
2672 }
2673
2674 if (row.i >= tb.childNodes.length) {
2675 // append new row if nothing exists
2676 tb.appendChild(tr);
2677 } else {
2678 // replace current row if it differs
2679 if (!tb.childNodes[row.i].isEqualNode(tr)) {
2680 if (tb.childNodes[row.i].childNodes[1].colSpan !== 1)
2681 tb.childNodes[row.i].childNodes[1].colSpan = "1";
2682 for (let i = 0; i < 8; i++) {
2683 let yellowBg = false;
2684
2685 // check for data change in value column
2686 if (i === 2) {
2687 let oldElem = tb.childNodes[row.i].childNodes[i];
2688 if (oldElem === undefined)
2689 oldElem = "";
2690 else {
2691 while (oldElem.childNodes[0] !== undefined &&
2692 (oldElem.tagName === 'TD' || oldElem.tagName === 'A' || oldElem.tagName === 'DIV'))
2693 oldElem = oldElem.childNodes[0]; // get into <td> <a> <div> elements
2694 oldElem = oldElem.parentNode.innerHTML;
2695 }
2696 let newElem = tr.childNodes[i];
2697 if (newElem === undefined)
2698 newElem = "";
2699 else {
2700 while (newElem.childNodes[0] !== undefined &&
2701 (newElem.tagName === 'TD' || newElem.tagName === 'A' || newElem.tagName === 'DIV'))
2702 newElem = newElem.childNodes[0]; // get into <td> <a> <div> elements
2703 newElem = newElem.parentNode.innerHTML;
2704 }
2705
2706 if (oldElem !== newElem && // value changed
2707 !odb.skip_yellow && // skip if globally disabled
2708 // skip if edit just finished
2709 !(oldElem.indexOf('(') == -1 && newElem.indexOf('(') !== -1) &&
2710 // skip '*' of arrays
2711 newElem.indexOf('>*</a>') === -1)
2712 yellowBg = true;
2713 else
2714 yellowBg = false;
2715 }
2716
2717 // check for edit mode
2718 let inlineEdit = find_input_element(tb.childNodes[row.i].childNodes[i]);
2719
2720 if (tb.childNodes[row.i].childNodes[i] === undefined)
2721 tb.childNodes[row.i].appendChild(tr.childNodes[i].cloneNode(true));
2722 else if (!inlineEdit) {
2723 let e = tb.childNodes[row.i].childNodes[i];
2724
2725 if (tb.childNodes[row.i].childNodes[i].innerHTML !== tr.childNodes[i].innerHTML)
2726 tb.childNodes[row.i].childNodes[i].innerHTML = tr.childNodes[i].innerHTML;
2727
2728 if (yellowBg) {
2729 e.style.backgroundColor = 'var(--myellow)';
2730 e.style.setProperty("-webkit-transition", "", "");
2731 e.style.setProperty("transition", "", "");
2732 e.age = new Date() / 1000;
2733 }
2734
2735 if (e.age !== undefined && new Date() / 1000 > e.age + 1) {
2736 e.style.setProperty("-webkit-transition", "background-color 1s", "");
2737 e.style.setProperty("transition", "background-color 1s", "");
2738 e.style.backgroundColor = "";
2739 }
2740 }
2741 }
2742 }
2743 }
2744
2745 // Install mouse click handler
2746 if (tb.childNodes[row.i].childNodes[2] !== undefined) {
2747 tb.childNodes[row.i].childNodes[2].removeEventListener('mousedown', select_array_element);
2748 tb.childNodes[row.i].childNodes[2].removeEventListener('mousemove', select_array_element);
2749 }
2750 tb.childNodes[row.i].addEventListener('mousedown', select_key);
2751 if (!odb.picker) {
2752 tb.childNodes[row.i].addEventListener('mousemove', select_key);
2753 tb.childNodes[row.i].addEventListener('contextmenu', context_menu);
2754 }
2755
2756 // Remember ODB path and key in row
2757 tb.childNodes[row.i].odbPath = keyPath;
2758 tb.childNodes[row.i].key = key;
2759
2760 // Copy visibility for odbOptions
2761 tb.childNodes[row.i].style.display = tr.style.display;
2762
2763 // Print array values
2764 if (Array.isArray(key.value)) {
2765 if (key.expanded === false) {
2766 // do nothing
2767 } else for (let i=0 ; i<key.value.length ; i++) {
2768 row.i++;
2769
2770 // return if in edit mode
2771 if (tb.childNodes[row.i] !== undefined &&
2772 tb.childNodes[row.i].childNodes[2] !== undefined &&
2773 tb.childNodes[row.i].childNodes[2].inEdit)
2774 continue;
2775
2776 // create empty row
2777 let tr = document.createElement('TR');
2778
2779 // store key path
2780 tr.odbPath = keyPath + '[' + i + ']';
2781
2782 // hide option keys if not in detailed column mode
2783 if (key.options && !odb.detailsColumn)
2784 tr.style.display = 'none';
2785
2786 // Handle column (empty for array values)
2787 let td = document.createElement('TD');
2788 td.setAttribute('name', 'odbHandle');
2789 td.style.display = odb.handleColumn ? 'table-cell' : 'none';
2790 td.style.width = "10px";
2791 tr.appendChild(td);
2792
2793 // Key name
2794 td = document.createElement('TD');
2795 tr.appendChild(td);
2796
2797 // Key value
2798 td = document.createElement('TD');
2799 tr.appendChild(td);
2800 let p = (path === '/' ? '/' + key.name : path + '/' + key.name);
2801 p += '['+i+']';
2802
2803 if (key.type === TID_BOOL) {
2804
2805 let h = "<select onchange='odb_setoption(this," + i + ")' " +
2806 " onfocus='option_edit(event, this, true)'" +
2807 " onblur='option_edit(event, this, false)'" +
2808 "'>\n";
2809
2810 if (key.value[i] === false) {
2811 h += "<option selected value='" + 0 + "'>No</option>";
2812 h += "<option value='" + 1 + "'>Yes</option>";
2813 } else {
2814 h += "<option value='" + 0 + "'>No</option>";
2815 h += "<option selected value='" + 1 + "'>Yes</option>";
2816 }
2817 h += "</select>\n";
2818 td.innerHTML = "[" + i + "]" + h;
2819
2820 } else if (options) {
2821
2822 let oc = options.slice(); // make copy of all elements
2823
2824 let h = "<select onchange='odb_setoption(this," + i + ")' " +
2825 " onfocus='option_edit(event, this, true)'" +
2826 " onblur='option_edit(event, this, false)'" +
2827 "'>\n";
2828
2829 // check if current value is in options, add it if not
2830 if (!oc.includes(key.value[i]))
2831 oc.unshift(key.value[i]);
2832
2833 for (const o of oc) {
2834 if (key.value[i] === o)
2835 h += "<option selected value='" + o + "'>" + o + "</option>";
2836 else
2837 h += "<option value='" + o + "'>" + o + "</option>";
2838 }
2839 h += "</select>\n";
2840 td.innerHTML = h;
2841
2842 } else {
2843 let edit = '<a href="#" onclick="ODBInlineEdit(this.parentNode, \'' + p + '\');return false;" ' +
2844 'onfocus="ODBInlineEdit(this.parentNode, \'' + p + '\')" title="Change array element">';
2845
2846 let v = escapeHTML(key.value[i].toString());
2847 if (key.type === TID_STRING && v === "")
2848 v = "(empty)";
2849 else if (key.type === TID_STRING && v.trim() === "")
2850 v = "(spaces)";
2851 else if (key.type === TID_LINK)
2852 v = '<span style="color:red;background-color:yellow">(cannot resolve link)</span>';
2853 else if (v.substring(0, 2) === "0x") {
2854 v = "0x" + v.substring(2).toUpperCase();
2855 if (key.type === TID_UINT64)
2856 v += " (" + BigInt(key.value[i]).toString() + ')';
2857 else
2858 v += " (" + parseInt(key.value[i]) + ')';
2859 } else if (key.type !== TID_STRING && key.type !== TID_LINK && key.type !== TID_FLOAT && key.type !== TID_DOUBLE) {
2860 if (key.type === TID_INT8)
2861 v += " (0x" + (key.value[i] & 0xFF).toString(16).toUpperCase() + ')';
2862 else if (key.type === TID_INT16)
2863 v += " (0x" + (key.value[i] & 0xFFFF).toString(16).toUpperCase() + ')';
2864 else if (key.type === TID_INT32)
2865 v += " (0x" + (key.value[i] & 0xFFFFFFFF).toString(16).toUpperCase() + ')';
2866 else if (key.type === TID_INT64)
2867 v += " (0x" + bnToHex(BigInt(key.value[i])) + ')';
2868 else
2869 v += " (0x" + key.value[i].toString(16).toUpperCase() + ')';
2870 }
2871
2872 if (odb.picker)
2873 td.innerHTML = '[' + i + '] ' + v;
2874 else if (odb.handlecolumn)
2875 td.innerHTML = v;
2876 else
2877 td.innerHTML = '[' + i + '] ' + edit + v + '</a>';
2878 }
2879
2880 // Empty fill cells
2881 for (let i=0 ; i<5 ; i++) {
2882 td = document.createElement('TD');
2883 td.setAttribute('name', 'odbExt');
2884 td.style.display = odb.detailsColumn ? 'table-cell' : 'none';
2885 tr.appendChild(td);
2886 }
2887
2888 if (row.i >= tb.childNodes.length) {
2889 tb.appendChild(tr);
2890 } else {
2891 if (!tb.childNodes[row.i].isEqualNode(tr)) {
2892 if (tr.childNodes.length === 1) { // Subdir
2893 tb.childNodes[row.i].replaceWith(tr);
2894 } else if (tr.childNodes.length === 8) { // Key
2895 let odbSelected = tb.childNodes[row.i].odbSelected;
2896 let odbLastSelected = tb.childNodes[row.i].odbLastSelected;
2897 if (tb.childNodes[row.i].childNodes[1].colSpan !== 1)
2898 tb.childNodes[row.i].childNodes[1].colSpan = "1";
2899 for (let i = 0; i < 8; i++) {
2900 let changed = false;
2901 let oldValue;
2902 let newValue;
2903
2904 if (i === 2) {
2905 let select = (tr.childNodes[2].childNodes[0] &&
2906 tr.childNodes[2].childNodes[0].tagName == 'SELECT');
2907
2908 if (tb.childNodes[row.i].childNodes[2] !== undefined) {
2909 if (select)
2910 oldValue = tb.childNodes[row.i].childNodes[2].childNodes[0].value;
2911 else if (tb.childNodes[row.i].childNodes[2].childNodes[1] !== undefined)
2912 oldValue = tb.childNodes[row.i].childNodes[2].childNodes[1].innerHTML;
2913 else if (tb.childNodes[row.i].childNodes[2].childNodes[0] !== undefined)
2914 oldValue = tb.childNodes[row.i].childNodes[2].childNodes[0].innerHTML
2915 else
2916 oldValue = tb.childNodes[row.i].childNodes[2].innerHTML;
2917 }
2918 if (select)
2919 newValue = tr.childNodes[2].childNodes[0].value;
2920 else {
2921 if (tr.childNodes[2].childNodes[1] === undefined)
2922 newValue = tr.childNodes[2].childNodes[0].data;
2923 else
2924 newValue = tr.childNodes[2].childNodes[1].innerHTML;
2925 }
2926 }
2927
2928 if (oldValue !== undefined &&
2929 oldValue !== newValue && // value changed
2930 i === 2 && // we are in value column
2931 !odb.skip_yellow && // skip if globally disabled
2932 // skip if edit just finished
2933 !(oldValue.indexOf('(') == -1 && newValue.indexOf('(') !== -1) &&
2934 // skip '*' of arrays
2935 newValue.indexOf('>*</a>') === -1 &&
2936 tb.childNodes[row.i].odbSelected !== true)
2937 changed = true;
2938
2939 if (tb.childNodes[row.i].childNodes[i] === undefined)
2940 tb.childNodes[row.i].appendChild(tr.childNodes[i].cloneNode(true));
2941 else {
2942 // preserve color if key is selected
2943 let c = tb.childNodes[row.i].childNodes[i].style.color;
2944 tb.childNodes[row.i].childNodes[i].innerHTML = tr.childNodes[i].innerHTML;
2945 change_color(tb.childNodes[row.i].childNodes[i], c);
2946 }
2947
2948 let e = tb.childNodes[row.i].childNodes[i];
2949 if (changed) {
2950 e.style.backgroundColor = 'var(--myellow)';
2951 e.style.setProperty("-webkit-transition", "", "");
2952 e.style.setProperty("transition", "", "");
2953 e.age = new Date() / 1000;
2954 }
2955
2956 if (e.age !== undefined && new Date() / 1000 > e.age + 1) {
2957 e.style.setProperty("-webkit-transition", "background-color 1s", "");
2958 e.style.setProperty("transition", "background-color 1s", "");
2959 e.style.backgroundColor = "";
2960 }
2961 if (e.age !== undefined && new Date() / 1000 > e.age + 2) {
2962 e.style.cssText = "";
2963 e.removeAttribute("style");
2964 e.age = undefined;
2965 }
2966 }
2967
2968 tb.childNodes[row.i].odbSelected = odbSelected;
2969 tb.childNodes[row.i].odbLastSelected = odbLastSelected;
2970 }
2971 }
2972 }
2973
2974 // remove/install mouse click handlers
2975 tb.childNodes[row.i].removeEventListener('mousedown', select_key);
2976 if (!odb.picker) {
2977 tb.childNodes[row.i].removeEventListener('mousemove', select_key);
2978 tb.childNodes[row.i].removeEventListener('contextmenu', context_menu);
2979 }
2980
2981 tb.childNodes[row.i].childNodes[2].addEventListener('mousedown', select_array_element);
2982 tb.childNodes[row.i].childNodes[2].addEventListener('mousemove', select_array_element);
2983
2984 // catch all mouseup events to stop dragging
2985 document.addEventListener('mouseup', mouseStopDragging);
2986 tb.childNodes[row.i].odbPath = tr.odbPath;
2987 tb.childNodes[row.i].key = undefined;
2988
2989 tb.childNodes[row.i].style.display = tr.style.display;
2990
2991 }
2992 }
2993}
2994
2995function resize_array(event, path, n) {
2996 event.stopPropagation(); // do not select row
2997 dlgQuery("Enter size of array:", n, do_resize_array, path);
2998}
2999
3000function do_resize_array(n, path) {
3001 mjsonrpc_db_resize([path],[parseInt(n)]).then(rpc => {
3002 }).catch(error => mjsonrpc_error_alert(error));
3003}
3004
3005function resize_string(event, path, size, num_values) {
3006 event.stopPropagation(); // do not select row
3007 dlgQuery("Enter new string size:", size, do_resize_string, { "path": path, "num_values": num_values });
3008}
3009
3010function do_resize_string(size, p) {
3011 mjsonrpc_call("db_resize_string",
3012 { "paths": [p.path],
3013 "new_lengths": [parseInt(p.num_values)],
3014 "new_string_lengths": [parseInt(size)]}).then(rpc => {
3015 }).catch(error => mjsonrpc_error_alert(error));
3016}
3017
3018function dlgCreateKeyDown(event, inp) {
3019 let keyCode = ('which' in event) ? event.which : event.keyCode;
3020
3021 if (keyCode === 27) {
3022 // cancel editing
3023 dlgHide('dlgCreate');
3024 return false;
3025 }
3026
3027 if (keyCode === 13) {
3028 // finish editing
3029 if (do_new_key(event.target))
3030 dlgHide('dlgCreate');
3031 return false;
3032 }
3033
3034 return true;
3035}
3036
3037function dlgCreateLinkKeyDown(event, inp) {
3038 let keyCode = ('which' in event) ? event.which : event.keyCode;
3039
3040 if (keyCode === 27) {
3041 // cancel editing
3042 dlgHide('dlgCreateLink');
3043 return false;
3044 }
3045
3046 if (keyCode === 13) {
3047 // finish editing
3048 if (do_new_link(this))
3049 dlgHide('dlgCreateLink');
3050 return false;
3051 }
3052
3053 return true;
3054}
3055
3056function drag_start(event) {
3057 let tr = event.target;
3058 while (tr.tagName !== 'TR')
3059 tr = tr.parentNode;
3060 let tb = getOdbTb(tr);
3061
3062 tb.dragTargetRow = Array.from(tb.children).indexOf(tr);
3063 tb.dragSourceRow = tb.dragTargetRow;
3064 tb.dragSource = tr.odbPath;
3065 tb.dragRowContent = tr.cloneNode(true);
3066
3067 window.setTimeout(() => {
3068 window.clearTimeout(tb.odb.updateTimer);
3069 let w = window.getComputedStyle(tr).width;
3070 let h = window.getComputedStyle(tr).height;
3071 tr.innerHTML = '<td colspan="8" style="background-color:white; border: 2px dashed #6bb28c"></td>';
3072 tr.style.height = parseInt(h) + "px";
3073 tr.style.width = parseInt(w) + "px";
3074 }, 10);
3075
3076}
3077
3078function drag_move(event, td) {
3079 event.preventDefault();
3080
3081 let tr = event.target;
3082 while (tr.tagName !== 'TR')
3083 tr = tr.parentNode;
3084 let tb = getOdbTb(tr);
3085 let children = Array.from(tb.children);
3086
3087 if (children.indexOf(tr) > tb.dragTargetRow)
3088 tr.after(tb.childNodes[tb.dragTargetRow]);
3089 else if (children.indexOf(tr) < tb.dragTargetRow)
3090 tr.before(tb.childNodes[tb.dragTargetRow]);
3091
3092 tb.dragTargetRow = children.indexOf(tr);
3093}
3094
3095function drag_end(event, td) {
3096 let tr = event.target;
3097 while (tr.tagName !== 'TR')
3098 tr = tr.parentNode;
3099 let tb = getOdbTb(tr);
3100
3101 let ttr = tb.childNodes[tb.dragTargetRow];
3102 ttr.innerHTML = tb.dragRowContent.innerHTML;
3103 ttr.style.height = "";
3104 ttr.style.width = "";
3105
3106 if (tb.dragSourceRow !== tb.dragTargetRow) {
3107 mjsonrpc_call("db_reorder", {"paths": [tb.dragSource], "indices": [tb.dragTargetRow - 4]}).then(rpc => {
3108 tb.odb.updateTimer = window.setTimeout(odb_update, 10, tb);
3109 }).catch(error => mjsonrpc_error_alert(error));
3110 } else
3111 tb.odb.updateTimer = window.setTimeout(odb_update, 10, tb);
3112}
3113
3114function show_open_records(odb) {
3115 let title = "ODB open records under \"" + odb.path + "\"";
3116
3117 let d = document.createElement("div");
3118 d.className = "dlgFrame";
3119 d.style.zIndex = "30";
3120 d.style.minWidth = "400px";
3121 d.shouldDestroy = true;
3122
3123 d.innerHTML = "<div class=\"dlgTitlebar\" id=\"dlgMessageTitle\">" + title + "</div>" +
3124 "<div class=\"dlgPanel\" style=\"padding: 2px;\">" +
3125 "<div id=\"dlgSOR\"></div>" +
3126 "<button class=\"dlgButton\" id=\"dlgMessageButton\" +" +
3127 "type=\"button\" " +
3128 " onClick=\"dlgMessageDestroy(this)\">Close</button>" +
3129 "</div>";
3130
3131 document.body.appendChild(d);
3132
3133 update_open_records(odb);
3134 dlgShow(d, false);
3135}
3136
3137function update_open_records(odb) {
3138 let path = odb.path;
3139 mjsonrpc_call("db_sor", {"path": path}).then (rpc => {
3140 let sor = rpc.result.sor;
3141 let paths = {};
3142 for (const s of sor) {
3143 if (paths[s.path])
3144 paths[s.path] += ', ' + s.name;
3145 else
3146 paths[s.path] = s.name;
3147 }
3148 let sorted_paths = Object.keys(paths).sort();
3149 let html = '<table class="mtable" style="width: 100%;margin-top: 0;margin-bottom:0"><tbody>';
3150 html += '<tr><th>ODB Path</th><th>Open by</th></tr>';
3151 for (const p of sorted_paths)
3152 html += '<tr><td style="padding: 4px">' + p + '</td>' +
3153 '<td style="padding: 4px">' + paths[p] + '</td></tr>';
3154 html += '</tbody></table>';
3155
3156 let d = document.getElementById('dlgSOR');
3157 if (d === null)
3158 return; // dialog has been closed
3159 if (d.innerHTML !== html) {
3160 d.innerHTML = html;
3161 dlgCenter(d.parentElement.parentElement);
3162 }
3163 window.setTimeout(update_open_records, 1000, odb);
3164
3165 }).catch( error => mjsonrpc_error_alert(error));
3166}
3167
3168function show_open_clients(e) {
3169 let odb = getOdbTb(e).odb;
3170 let title = "ODB clients";
3171
3172 let d = document.createElement("div");
3173 d.className = "dlgFrame";
3174 d.style.zIndex = "30";
3175 d.style.minWidth = "400px";
3176 d.shouldDestroy = true;
3177
3178 d.innerHTML = "<div class=\"dlgTitlebar\" id=\"dlgMessageTitle\">" + title + "</div>" +
3179 "<div class=\"dlgPanel\" style=\"padding: 2px;\">" +
3180 "<div id=\"dlgSCL\"></div>" +
3181 "<button class=\"dlgButton\" id=\"dlgMessageButton\" +" +
3182 "type=\"button\" " +
3183 " onClick=\"dlgMessageDestroy(this)\">Close</button>" +
3184 "</div>";
3185
3186 document.body.appendChild(d);
3187
3188 update_open_clients(odb);
3189 dlgShow(d, false);
3190}
3191
3192function update_open_clients(odb) {
3193 let path = odb.path;
3194 mjsonrpc_call("db_scl").then (rpc => {
3195 let scl = rpc.result.scl.clients;
3196
3197 let html = '<table class="mtable" style="width: 100%;margin-top: 0;margin-bottom:0"><tbody>';
3198 html += '<tr><th>Name</th><th>Host</th><th>Slot</th><th>PID</th><th>Timeout</th><th>Last active [ms]</th></tr>';
3199 for (const s of scl) {
3200 html += '<tr><td style="padding: 4px"><a href="?cmd=odb&odb_path=/System/Clients/"'
3201 + s.pid + '">' + s.name + '</td>';
3202 html += '<td style="padding: 4px">'+ s.host + '</td>';
3203 html += '<td style="padding: 4px">'+ s.slot + '</td>';
3204 html += '<td style="padding: 4px">'+ s.pid + '</td>';
3205 html += '<td style="padding: 4px">'+ s.watchdog_timeout_millisec + '</td>';
3206 html += '<td style="padding: 4px">'+ s.last_activity_millisec + '</td>';
3207 html += '</tr>';
3208 }
3209 html += '</tbody></table>';
3210
3211 let d = document.getElementById('dlgSCL');
3212 if (d === null)
3213 return; // dialog has been closed
3214 if (d.innerHTML !== html) {
3215 d.innerHTML = html;
3216 dlgCenter(d.parentElement.parentElement);
3217 }
3218 window.setTimeout(update_open_clients, 1000, odb);
3219
3220 }).catch( error => mjsonrpc_error_alert(error));
3221}