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