Private/Get-DhJsTreeTable.ps1

function Get-DhJsTreeTable {
    # Fragment of the dashboard runtime JS (see Get-DhJsContent). v2.5.0.
    return @'
  /* =========================================================================
     TREE TABLE (v2.5.0) - Add-DhTreeTable
     A directory explorer: tree on the left, the selected object on the right
     with Properties / Members / Member of / Nesting tabs. The membership
     functions below are pure (data in, data out) so they can be tested in node.
     Rows are drawn flat, only when their parent is open.
     ========================================================================= */

  /* links = [[groupId, memberId], ...] -> { membersOf:{group:[member...]}, memberOf:{member:[group...]} } */
  function dhTtLinks(links) {
    var membersOf = {}, memberOf = {};
    (links || []).forEach(function (l) {
      (membersOf[l[0]] = membersOf[l[0]] || []).push(l[1]);
      (memberOf[l[1]] = memberOf[l[1]] || []).push(l[0]);
    });
    return { membersOf: membersOf, memberOf: memberOf };
  }

  /* Every path from `id` up to a group that is in no other group: [[id, g1, g2, ...], ...].
     A group that appears twice on one path is a circular nesting: that branch stops and
     `circular` is set. At most `cap` paths are returned (`more` counts the rest). */
  function dhTtPaths(memberOf, id, cap) {
    var paths = [], circular = false, more = 0;
    (function walk(path) {
      var ups = memberOf[path[path.length - 1]] || [], loops = false;
      ups.forEach(function (g) {
        if (path.indexOf(g) !== -1) { circular = true; loops = true; return; }
        path.push(g); walk(path); path.pop();
      });
      /* a path ends at a group that is in no other group, or where a loop was cut */
      if ((!ups.length || loops) && path.length > 1) { if (paths.length < cap) paths.push(path.slice()); else more++; }
    })([id]);
    return { paths: paths, circular: circular, more: more };
  }

  /* Groups nested below group `gid`, depth first: [{ id, depth, circular }] (cycle-safe, depth-capped). */
  function dhTtNested(membersOf, typeOf, gid, maxDepth) {
    var out = [];
    (function walk(id, depth, trail) {
      (membersOf[id] || []).forEach(function (m) {
        if (typeOf[m] !== 'group') return;
        var circ = trail.indexOf(m) !== -1;
        out.push({ id: m, depth: depth, circular: circ });
        if (!circ && depth < maxDepth) walk(m, depth + 1, trail.concat([m]));
      });
    })(gid, 1, [gid]);
    return out;
  }

  var DH_TT_GLYPH = { ou: 'OU', group: 'G', user: 'U', computer: 'PC', container: 'C', other: '•' };
  var DH_TT_TYPE = { ou: 'OU', group: 'Group', user: 'User', computer: 'Computer', container: 'Container', other: 'Other' };

  function renderTreeTable(container, block) {
    container.className = 'treetable-section';
    var rows = block.rows || [], byId = {}, kids = {}, roots = [], depth = {}, order = [], typeOf = {};
    rows.forEach(function (r) { byId[r.id] = r; kids[r.id] = []; typeOf[r.id] = r.type; });
    rows.forEach(function (r) { if (r.parent && byId[r.parent]) kids[r.parent].push(r); else roots.push(r); });
    (function walk(list, d) { list.forEach(function (r) { depth[r.id] = d; order.push(r); walk(kids[r.id], d + 1); }); })(roots, 0);
    var lk = dhTtLinks(block.links), membersOf = lk.membersOf, memberOf = lk.memberOf;
    var maxPaths = block.maxPaths || 50;

    var typesPresent = [];
    rows.forEach(function (r) { if (typesPresent.indexOf(r.type) === -1) typesPresent.push(r.type); });
    var expanded = {}, selectedId = null, focusId = null, findText = '', typeSel = {}, tab = 'props';
    order.forEach(function (r) { if (kids[r.id].length && depth[r.id] < (block.openDepth || 0)) expanded[r.id] = true; });

    container.innerHTML =
      (block.title ? '<h3 class="treetable-title">' + esc(block.title) + '</h3>' : '') +
      '<div class="tt" style="height:' + (parseInt(block.height, 10) || 520) + 'px">' +
        '<div class="tt-left">' +
          '<div class="tt-toolbar">' +
            '<input type="search" class="treefilter-find tt-find" placeholder="Find…" aria-label="Find in ' + esc(block.title || 'directory') + '" autocomplete="off">' +
            '<button type="button" class="treefilter-btn" data-tt="expand">Expand all</button>' +
            '<button type="button" class="treefilter-btn" data-tt="collapse">Collapse all</button>' +
            '<span class="tt-chips" role="group" aria-label="Show types">' + typesPresent.map(function (t) {
              return '<button type="button" class="nm-chip tt-chip" data-tt-type="' + esc(t) + '" aria-pressed="false">' + esc(DH_TT_TYPE[t] || t) + '</button>';
            }).join('') + '</span>' +
          '</div>' +
          '<div class="tt-tree dh-tree" role="tree" aria-label="' + esc(block.title || 'Directory') + '"></div>' +
          '<div class="treefilter-empty" hidden>No match.</div>' +
        '</div>' +
        '<div class="tt-right">' +
          '<div class="tt-head"></div>' +
          '<div class="tt-tabs" role="tablist" aria-label="Object details"></div>' +
          '<div class="tt-body" role="tabpanel"></div>' +
        '</div>' +
      '</div>';

    var tree = container.querySelector('.tt-tree'), find = container.querySelector('.tt-find');
    var empty = container.querySelector('.treefilter-empty'), head = container.querySelector('.tt-head');
    var tabsEl = container.querySelector('.tt-tabs'), body = container.querySelector('.tt-body');

    function childCount(r) {
      if (r.type === 'group') return (membersOf[r.id] || []).length;
      return kids[r.id].length;
    }
    function visibleRows() {
      var q = findText.toLowerCase(), anyType = Object.keys(typeSel).length;
      var match = null, force = {};
      if (q || anyType) {
        match = {};
        rows.forEach(function (r) {
          if (anyType && !typeSel[r.type]) return;
          if (q && (r.label + ' ' + r.type).toLowerCase().indexOf(q) === -1) return;
          match[r.id] = true;
          var p = r.parent;
          while (p && byId[p]) { force[p] = true; p = byId[p].parent; }
        });
      }
      var out = [];
      (function walk(list) {
        list.forEach(function (r) {
          if (match && !match[r.id] && !force[r.id]) return;
          out.push(r);
          var open = match ? !!force[r.id] : !!expanded[r.id];
          if (kids[r.id].length && open) walk(kids[r.id]);
        });
      })(roots);
      return out;
    }

    function rowHtml(r, tabId) {
      var has = kids[r.id].length > 0, filtering = !!(findText || Object.keys(typeSel).length);
      var open = filtering ? true : !!expanded[r.id], st = secStatus(r.status), n = childCount(r);
      return '<div class="dh-tree-row' + (selectedId === r.id ? ' dh-tree-sel' : '') + '" role="treeitem" aria-level="' + (depth[r.id] + 1) + '"' +
        ' aria-selected="' + (selectedId === r.id ? 'true' : 'false') + '"' + (has ? ' aria-expanded="' + (open ? 'true' : 'false') + '"' : '') +
        ' data-id="' + esc(r.id) + '" tabindex="' + (r.id === tabId ? '0' : '-1') + '" style="padding-left:' + (6 + depth[r.id] * 16) + 'px">' +
        '<span class="dh-tree-chev"' + (has ? ' data-act="toggle"' : '') + '>' + (has ? (open ? '&#9662;' : '&#9656;') : '') + '</span>' +
        '<span class="tt-type tt-type-' + esc(r.type) + '" title="' + esc(DH_TT_TYPE[r.type] || r.type) + '">' + esc(DH_TT_GLYPH[r.type] || '') + '</span>' +
        (st ? '<span class="dh-tree-dot dh-st-' + st + '" title="' + esc(SEC_LABEL[st] || '') + '"></span>' : '') +
        '<span class="dh-tree-label">' + esc(r.label) + '</span>' +
        (r.flags || []).map(function (f) { return '<span class="dh-tree-flag">' + esc(f) + '</span>'; }).join('') +
        (n ? '<span class="dh-tree-count">' + esc(String(n)) + '</span>' : '') + '</div>';
    }

    function renderTree(refocus) {
      var list = visibleRows();
      var tabId = (focusId && list.some(function (r) { return r.id === focusId; })) ? focusId : (list[0] && list[0].id);
      tree.innerHTML = list.map(function (r) { return rowHtml(r, tabId); }).join('');
      empty.hidden = list.length > 0;
      if (refocus) { var el = rowEl(refocus); if (el) el.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 pathLabel(r) {
      var parts = [], p = r;
      while (p) { parts.unshift(p.label); p = p.parent ? byId[p.parent] : null; }
      return parts.join(' / ');
    }
    function chip(id) {
      var r = byId[id], st = secStatus(r.status);
      return '<button type="button" class="tt-link" data-goto="' + esc(id) + '"><span class="tt-type tt-type-' + esc(r.type) + '">' + esc(DH_TT_GLYPH[r.type] || '') + '</span>' +
             (st ? '<span class="dh-tree-dot dh-st-' + st + '"></span>' : '') + esc(r.label) + '</button>';
    }
    function tabsFor(r) {
      var t = [{ k: 'props', l: 'Properties' }];
      if (r.type === 'group') t.push({ k: 'members', l: 'Members (' + (membersOf[r.id] || []).length + ')' });
      if ((memberOf[r.id] || []).length) t.push({ k: 'memberof', l: 'Member of (' + memberOf[r.id].length + ')' });
      var nested = r.type === 'group' && dhTtNested(membersOf, typeOf, r.id, 6).length;
      if (nested || (memberOf[r.id] || []).length) t.push({ k: 'nesting', l: 'Nesting' });
      return t;
    }
    function list(ids, cap) {
      if (!ids.length) return '<p class="dh-text">None.</p>';
      var shown = ids.slice(0, cap || 500);
      return '<div class="tt-list">' + shown.map(chip).join('') + '</div>' +
             (ids.length > shown.length ? '<p class="dh-more">+' + (ids.length - shown.length) + ' more</p>' : '');
    }

    function tabHtml(r) {
      if (tab === 'members') {
        var ms = membersOf[r.id] || [], c = { user: 0, computer: 0, group: 0 };
        ms.forEach(function (m) { if (c[typeOf[m]] !== undefined) c[typeOf[m]]++; });
        return '<p class="dh-text">' + ms.length + ' member' + (ms.length === 1 ? '' : 's') + ': ' + c.user + ' users, ' + c.computer + ' computers, ' + c.group + ' groups.</p>' + list(ms);
      }
      if (tab === 'memberof') return list(memberOf[r.id] || []);
      if (tab === 'nesting') {
        var h = '';
        if (r.type === 'group') {
          var nest = dhTtNested(membersOf, typeOf, r.id, 6);
          h += '<h4 class="dh-sec-title">Groups nested below</h4>';
          h += nest.length ? '<div class="tt-nest">' + nest.map(function (n) {
            return '<div style="padding-left:' + ((n.depth - 1) * 18) + 'px">' + chip(n.id) + (n.circular ? ' <span class="tt-circ">circular nesting</span>' : '') + '</div>';
          }).join('') + '</div>' : '<p class="dh-text">No groups are nested in this group.</p>';
        }
        var eff = dhTtPaths(memberOf, r.id, maxPaths);
        h += '<h4 class="dh-sec-title">Effective membership: every path up</h4>';
        if (!eff.paths.length) h += '<p class="dh-text">Not a member of any group.</p>';
        else {
          h += '<div class="tt-nest">' + eff.paths.map(function (p) {
            return '<div class="tt-path">' + p.map(function (id, i) { return (i ? '<span class="tt-arrow">→</span>' : '') + chip(id); }).join('') + '</div>';
          }).join('') + '</div>';
          if (eff.more) h += '<p class="dh-more">+' + eff.more + ' more paths</p>';
        }
        if (eff.circular) h += '<p class="tt-circ-note">A group is (directly or indirectly) a member of itself; the loop was not followed.</p>';
        return h;
      }
      var items = [{ label: 'Type', value: DH_TT_TYPE[r.type] || r.type }, { label: 'Location', value: pathLabel(r) }];
      (r.props || []).forEach(function (p) { items.push({ label: p.label, value: p.value }); });
      var secs = [{ kind: 'kv', title: '', columns: 2, items: items }];
      if (r.detail && r.detail.sections) secs = secs.concat(r.detail.sections);
      return renderSections(secs);
    }

    function renderDetail() {
      var r = selectedId && byId[selectedId];
      if (!r) { head.innerHTML = ''; tabsEl.innerHTML = ''; body.innerHTML = '<p class="dh-text">Select an object on the left.</p>'; return; }
      var st = secStatus(r.status);
      head.innerHTML = '<span class="tt-type tt-type-' + esc(r.type) + '">' + esc(DH_TT_GLYPH[r.type] || '') + '</span>' +
        '<span class="tt-head-label">' + esc(r.label) + '</span>' + (st ? '<span class="dh-pill dh-st-' + st + '">' + esc(SEC_LABEL[st]) + '</span>' : '') +
        (r.flags || []).map(function (f) { return '<span class="dh-tree-flag">' + esc(f) + '</span>'; }).join('');
      var ts = tabsFor(r);
      if (!ts.some(function (x) { return x.k === tab; })) tab = 'props';
      tabsEl.innerHTML = ts.map(function (x) {
        return '<button type="button" class="tt-tab" role="tab" data-tab="' + x.k + '" aria-selected="' + (x.k === tab ? 'true' : 'false') + '" tabindex="' + (x.k === tab ? '0' : '-1') + '">' + esc(x.l) + '</button>';
      }).join('');
      body.innerHTML = tabHtml(r);
    }

    function select(id, keepTab) {
      if (!byId[id]) return;
      selectedId = id; focusId = id;
      if (!keepTab) tab = 'props';
      renderTree(); renderDetail();
    }
    function goTo(id) { /* a link in a tab: make the row visible, then select it */
      var p = byId[id] && byId[id].parent;
      while (p && byId[p]) { expanded[p] = true; p = byId[p].parent; }
      var hidden = !visibleRows().some(function (r) { return r.id === id; });
      if (hidden) { findText = ''; find.value = ''; typeSel = {}; container.querySelectorAll('.tt-chip').forEach(function (c) { c.setAttribute('aria-pressed', 'false'); }); }
      select(id);
      var el = rowEl(id); if (el && el.scrollIntoView) el.scrollIntoView({ block: 'nearest' });
    }

    tree.addEventListener('click', function (e) {
      var rowNode = e.target.closest('.dh-tree-row'); if (!rowNode) return;
      var id = rowNode.getAttribute('data-id'), act = e.target.closest('[data-act]');
      if (act && act.getAttribute('data-act') === 'toggle') { expanded[id] = !expanded[id]; focusId = id; renderTree(id); return; }
      select(id);
    });
    tree.addEventListener('keydown', function (e) {
      var rowNode = e.target.closest && e.target.closest('.dh-tree-row'); if (!rowNode || e.target !== rowNode) return;
      var id = rowNode.getAttribute('data-id'), r = byId[id], list2 = visibleRows(), i = list2.indexOf(r), key = e.key, to = null;
      var filtering = !!(findText || Object.keys(typeSel).length);
      if (key === 'ArrowDown') to = list2[Math.min(i + 1, list2.length - 1)];
      else if (key === 'ArrowUp') to = list2[Math.max(i - 1, 0)];
      else if (key === 'Home') to = list2[0];
      else if (key === 'End') to = list2[list2.length - 1];
      else if (key === 'ArrowRight') {
        if (kids[id].length && !expanded[id] && !filtering) { expanded[id] = true; focusId = id; e.preventDefault(); renderTree(id); return; }
        if (kids[id].length) to = kids[id][0];
      } else if (key === 'ArrowLeft') {
        if (kids[id].length && expanded[id] && !filtering) { expanded[id] = false; focusId = id; e.preventDefault(); renderTree(id); return; }
        to = r.parent ? byId[r.parent] : null;
      } else if (key === 'Enter' || key === ' ') { e.preventDefault(); select(id); var el0 = rowEl(id); if (el0) el0.focus(); return; }
      else return;
      e.preventDefault();
      if (to) { focusId = to.id; renderTree(to.id); }
    });
    find.addEventListener('input', function () { findText = find.value; renderTree(); });
    container.querySelector('.tt-toolbar').addEventListener('click', function (e) {
      var tb = e.target.closest('[data-tt]');
      if (tb) {
        if (tb.getAttribute('data-tt') === 'expand') order.forEach(function (r) { if (kids[r.id].length) expanded[r.id] = true; });
        else expanded = {};
        renderTree(); return;
      }
      var ch = e.target.closest('[data-tt-type]');
      if (ch) {
        var t = ch.getAttribute('data-tt-type');
        if (typeSel[t]) delete typeSel[t]; else typeSel[t] = true;
        ch.setAttribute('aria-pressed', typeSel[t] ? 'true' : 'false');
        renderTree();
      }
    });
    tabsEl.addEventListener('click', function (e) {
      var b = e.target.closest('[data-tab]'); if (!b) return;
      tab = b.getAttribute('data-tab'); renderDetail();
    });
    tabsEl.addEventListener('keydown', function (e) {
      if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight' && e.key !== 'Home' && e.key !== 'End') return;
      var bs = Array.prototype.slice.call(tabsEl.querySelectorAll('[data-tab]')), i = bs.indexOf(document.activeElement);
      if (i < 0) return;
      e.preventDefault();
      var n = e.key === 'Home' ? 0 : e.key === 'End' ? bs.length - 1 : (i + (e.key === 'ArrowRight' ? 1 : -1) + bs.length) % bs.length;
      tab = bs[n].getAttribute('data-tab'); renderDetail();
      var nb = tabsEl.querySelector('[data-tab="' + tab + '"]'); if (nb) nb.focus();
    });
    body.addEventListener('click', function (e) {
      var g = e.target.closest('[data-goto]'); if (g) goTo(g.getAttribute('data-goto'));
    });

    if (roots.length) select(roots[0].id); else { renderTree(); renderDetail(); }
  }
'@

}