Private/Get-DhJsTree.ps1
|
function Get-DhJsTree { # Fragment of the dashboard runtime JS (see Get-DhJsContent). v2.4.0. return @' /* ========================================================================= TREE FILTER (v2.4.0) - Add-DhTreeFilter A tree of nodes (OUs, folders, sites) that filters one table by path. The filter itself lives in TableEngine (setPathFilter / pathFilter), so the table's own clear button, the URL state and the filter badge all work; this file only draws the tree and tells the engine what was chosen. Rows are drawn flat (aria-level) so an expanded 5,000-node tree is one pass. ========================================================================= */ /* Lower-case, trim, and drop blanks around the separator ("OU=a, OU=b"). */ function dhPathNorm(s, sep) { s = String(s).toLowerCase().trim(); if (sep && s.indexOf(' ') !== -1) s = s.split(sep).map(function (x) { return x.trim(); }).join(sep); return s; } /* Does a row's path value sit at or below node path p? Prefix: root-first paths ('a/b/c'); Suffix: distinguished names ('ou=c,ou=b,dc=a'). The separator is part of the test, so 'HQ' never matches 'HQ2'. */ function dhPathMatch(value, p, sep, mode) { if (value === null || value === undefined) return false; sep = sep || '/'; var v = dhPathNorm(value, sep), q = dhPathNorm(p, sep); if (v === q) return true; if (String(mode || 'prefix').toLowerCase() === 'suffix') { return v.length > q.length && v.slice(v.length - q.length - sep.length) === sep + q; } return v.indexOf(q + sep) === 0; } var TreeFilter = { trees: {}, /* The engine changed its path filter (or cleared it): redraw every tree on that table. */ sync: function (tableId) { var self = this; Object.keys(this.trees).forEach(function (k) { if (self.trees[k].block.tableId === tableId) self.trees[k].syncSelection(); }); } }; /* Rows at or below each node, from the table: every row is credited to the DEEPEST node its path reaches, then counts roll up to the parents. */ function dhTreeCounts(block, nodes, byId, order) { var eng = engines[block.tableId], sep = block.sep || '/'; var suffix = String(block.match || '').toLowerCase() === 'suffix'; var total = {}, pathMap = {}; nodes.forEach(function (n) { total[n.id] = 0; pathMap[dhPathNorm(n.path, sep)] = n.id; }); if (!eng) return total; eng.allData.forEach(function (row) { var raw = row[block.pathField]; if (raw === null || raw === undefined || raw === '') return; var parts = dhPathNorm(raw, sep).split(sep), acc = '', hit = null, i; if (suffix) { for (i = parts.length - 1; i >= 0; i--) { acc = acc ? parts[i] + sep + acc : parts[i]; if (pathMap[acc] !== undefined) hit = pathMap[acc]; } } else { for (i = 0; i < parts.length; i++) { acc = i ? acc + sep + parts[i] : parts[i]; if (pathMap[acc] !== undefined) hit = pathMap[acc]; } } if (hit !== null) total[hit]++; }); for (var k = order.length - 1; k >= 0; k--) { var n = order[k]; if (n.parent && byId[n.parent]) total[n.parent] += total[n.id]; } return total; } function renderTreeFilter(container, block) { container.className = 'treefilter-section'; var nodes = block.nodes || [], byId = {}, kids = {}, roots = [], depth = {}, order = []; nodes.forEach(function (n) { byId[n.id] = n; kids[n.id] = []; }); nodes.forEach(function (n) { if (n.parent && byId[n.parent]) kids[n.parent].push(n); else roots.push(n); }); (function walk(list, d) { list.forEach(function (n) { depth[n.id] = d; order.push(n); walk(kids[n.id], d + 1); }); })(roots, 0); var counts = {}; if (block.countFromTable) counts = dhTreeCounts(block, nodes, byId, order); else nodes.forEach(function (n) { if (n.count !== undefined && n.count !== null) counts[n.id] = n.count; }); var expanded = {}, selectedId = null, focusId = null, findText = ''; order.forEach(function (n) { if (kids[n.id].length && depth[n.id] < (block.openDepth || 0)) expanded[n.id] = true; }); var uid = 'tf-' + block.id; container.innerHTML = (block.title ? '<h3 class="treefilter-title">' + esc(block.title) + '</h3>' : '') + '<div class="treefilter-toolbar">' + '<input type="search" class="treefilter-find" id="' + uid + '-find" placeholder="Find…" aria-label="Find in ' + esc(block.title || 'tree') + '" autocomplete="off">' + '<button type="button" class="treefilter-btn" data-tf="expand">Expand all</button>' + '<button type="button" class="treefilter-btn" data-tf="collapse">Collapse all</button>' + '<button type="button" class="treefilter-btn" data-tf="clear" hidden>Clear filter</button>' + '</div>' + '<div class="treefilter-scroll" style="max-height:' + (parseInt(block.height, 10) || 420) + 'px">' + '<div class="dh-tree" role="tree" aria-label="' + esc(block.title || 'Tree') + '"></div></div>' + '<div class="treefilter-empty" hidden>No match.</div>'; var tree = container.querySelector('.dh-tree'); var find = container.querySelector('.treefilter-find'); var clear = container.querySelector('[data-tf="clear"]'); var empty = container.querySelector('.treefilter-empty'); function visibleRows() { var q = findText.toLowerCase(), match = null, force = {}; if (q) { match = {}; nodes.forEach(function (n) { if ((n.label + ' ' + n.path).toLowerCase().indexOf(q) === -1) return; match[n.id] = true; var p = n.parent; while (p && byId[p]) { force[p] = true; p = byId[p].parent; } }); } var out = []; (function walk(list) { list.forEach(function (n) { if (q && !match[n.id] && !force[n.id]) return; out.push(n); var open = q ? !!force[n.id] : !!expanded[n.id]; if (kids[n.id].length && open) walk(kids[n.id]); }); })(roots); return out; } function rowHtml(n, rows, tabId) { var has = kids[n.id].length > 0; var open = findText ? true : !!expanded[n.id]; var st = secStatus(n.status); var cnt = counts[n.id]; return '<div class="dh-tree-row' + (selectedId === n.id ? ' dh-tree-sel' : '') + '" role="treeitem"' + ' aria-level="' + (depth[n.id] + 1) + '" aria-selected="' + (selectedId === n.id ? 'true' : 'false') + '"' + (has ? ' aria-expanded="' + (open ? 'true' : 'false') + '"' : '') + ' data-id="' + esc(n.id) + '" tabindex="' + (n.id === tabId ? '0' : '-1') + '"' + ' style="padding-left:' + (6 + depth[n.id] * 16) + 'px">' + '<span class="dh-tree-chev"' + (has ? ' data-act="toggle"' : '') + '>' + (has ? (open ? '▾' : '▸') : '') + '</span>' + (st ? '<span class="dh-tree-dot dh-st-' + st + '" title="' + esc(SEC_LABEL[st] || '') + '"></span>' : '<span class="dh-tree-dot dh-tree-dot-none"></span>') + '<span class="dh-tree-label" title="' + esc(n.path) + '">' + esc(n.label) + '</span>' + (n.flags || []).map(function (f) { return '<span class="dh-tree-flag">' + esc(f) + '</span>'; }).join('') + (cnt !== undefined ? '<span class="dh-tree-count">' + esc(Number(cnt).toLocaleString()) + '</span>' : '') + (n.detail ? '<button type="button" class="dh-tree-info" data-act="info" aria-label="Details for ' + esc(n.label) + '">i</button>' : '') + '</div>'; } function render(refocus) { var rows = visibleRows(); var tabId = (focusId && rows.some(function (r) { return r.id === focusId; })) ? focusId : (rows[0] && rows[0].id); tree.innerHTML = rows.map(function (n) { return rowHtml(n, rows, tabId); }).join(''); empty.hidden = rows.length > 0; clear.hidden = !selectedId; if (refocus) rowEl(refocus) && rowEl(refocus).focus(); } function rowEl(id) { var els = tree.querySelectorAll('.dh-tree-row'); for (var i = 0; i < els.length; i++) if (els[i].getAttribute('data-id') === id) return els[i]; return null; } function select(n) { var eng = engines[block.tableId]; if (!eng) return; if (selectedId === n.id) { eng.setPathFilter(null); return; } eng.setPathFilter({ field: block.pathField, value: n.path, sep: block.sep, mode: block.match, treeId: block.id, nodeId: n.id, label: n.label, title: block.title }); } function openDetail(n, opener) { var d = n.detail; if (!d) return; dhDrawerOpen({ title: d.title || n.label, subtitle: d.subtitle || n.path, sections: d.sections }, { opener: opener }); } function syncSelection() { var eng = engines[block.tableId], pf = eng && eng.pathFilter; selectedId = (pf && pf.treeId === block.id && byId[pf.nodeId]) ? pf.nodeId : null; if (selectedId) { /* make the chosen node visible */ var p = byId[selectedId].parent; while (p && byId[p]) { expanded[p] = true; p = byId[p].parent; } } render(); } tree.addEventListener('click', function (e) { var rowNode = e.target.closest('.dh-tree-row'); if (!rowNode) return; var n = byId[rowNode.getAttribute('data-id')]; if (!n) return; var act = e.target.closest('[data-act]'); focusId = n.id; if (act && act.getAttribute('data-act') === 'toggle') { expanded[n.id] = !expanded[n.id]; render(n.id); return; } if (act && act.getAttribute('data-act') === 'info') { openDetail(n, act); return; } select(n); }); tree.addEventListener('keydown', function (e) { var rowNode = e.target.closest && e.target.closest('.dh-tree-row'); if (!rowNode || e.target !== rowNode) return; var n = byId[rowNode.getAttribute('data-id')], rows = visibleRows(), i = rows.indexOf(n), key = e.key, to = null; if (key === 'ArrowDown') to = rows[Math.min(i + 1, rows.length - 1)]; else if (key === 'ArrowUp') to = rows[Math.max(i - 1, 0)]; else if (key === 'Home') to = rows[0]; else if (key === 'End') to = rows[rows.length - 1]; else if (key === 'ArrowRight') { if (kids[n.id].length && !expanded[n.id] && !findText) { expanded[n.id] = true; focusId = n.id; e.preventDefault(); render(n.id); return; } if (kids[n.id].length) to = kids[n.id][0]; } else if (key === 'ArrowLeft') { if (kids[n.id].length && expanded[n.id] && !findText) { expanded[n.id] = false; focusId = n.id; e.preventDefault(); render(n.id); return; } to = n.parent ? byId[n.parent] : null; } else if (key === 'Enter' || key === ' ') { e.preventDefault(); select(n); return; } else return; e.preventDefault(); if (to) { focusId = to.id; render(to.id); } }); find.addEventListener('input', function () { findText = find.value; render(); }); container.querySelector('.treefilter-toolbar').addEventListener('click', function (e) { var b = e.target.closest('[data-tf]'); if (!b) return; var a = b.getAttribute('data-tf'); if (a === 'expand') { order.forEach(function (n) { if (kids[n.id].length) expanded[n.id] = true; }); render(); } if (a === 'collapse') { expanded = {}; render(); } if (a === 'clear') { var eng = engines[block.tableId]; if (eng) eng.setPathFilter(null); } }); var inst = { block: block, syncSelection: syncSelection }; TreeFilter.trees[block.id] = inst; /* Restore a selection saved in the URL hash: { tableId: [treeId, nodeId] }. */ var saved = (URLState.load().pathFilters || {})[block.tableId]; if (saved && saved[0] === block.id && byId[saved[1]]) select(byId[saved[1]]); else render(); if (!tree.firstChild && roots.length) render(); /* select() returns early when the table is missing */ } '@ } |