Private/Get-DhJsPrint.ps1

function Get-DhJsPrint {
    # Fragment of the dashboard runtime JS (see Get-DhJsContent). v2.2.0.
    return @'
  /* =========================================================================
     PRINT (v2.2.0)

     The interactive page shows one nav view at a time, folds things away and
     pages its tables. A printout needs the opposite, so between beforeprint
     and afterprint the page is temporarily rearranged:

       - light theme (dark backgrounds waste toner and print badly);
       - every nav view, in menu order, each under a heading with its path;
       - every table shows its filtered rows up to PRINT_CONFIG.maxRows, with
         "Showing N of M rows" when some were left out;
       - an optional cover page (New-DhDashboard -PrintCover).

     Expanding folded content (collapsibles, findings, tab panels, table
     sections) is done in CSS (@media print), so it also works for a printer
     that never runs these handlers. Everything here is undone on afterprint.

     Headless browsers printing to PDF (Edge/Chrome --print-to-pdf, used by
     report pipelines) fire beforeprint too, so they get the same layout.
     ========================================================================= */
  var _printState = null;

  function _navLabelPath(tree, path) {
    var labels = [], node = tree;
    for (var i = 0; i < path.length; i++) {
      node = node && node.children ? node.children[path[i]] : null;
      labels.push(node && node.label ? node.label : path[i]);
    }
    return labels;
  }

  /* Menu order: depth-first over the nav tree, children in the order the
     menu shows them. Returns pathKey -> rank. */
  function _navOrderIndex(tree) {
    var rank = {}, n = 0;
    (function walk(node) {
      rank[JSON.stringify(node.path || [])] = n++;
      (node.order || Object.keys(node.children || {})).forEach(function (k) {
        if (node.children[k]) walk(node.children[k]);
      });
    })(tree);
    return rank;
  }

  function _cssString(s) {
    return '"' + String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/[\r\n]+/g, ' ') + '"';
  }

  function dhBeforePrint() {
    if (_printState) return; /* already arranged */
    var st = _printState = { theme: themeMode, moved: [], added: [], engines: {} };

    /* 1. light theme, without recording it as the reader's choice */
    if (themeMode !== 'light') applyThemeMode('light');
    document.documentElement.classList.add('dh-printing');

    /* 2. page header/footer text: title + page numbers (CSS @page boxes) */
    var pageCss = document.createElement('style');
    pageCss.setAttribute('data-dh-print', '');
    pageCss.textContent = '@page { @top-left { content: ' + _cssString(PRINT_CONFIG.title || document.title) +
      '; font-size: 8pt; color: #666; } @bottom-right { content: "Page " counter(page) " of " counter(pages); font-size: 8pt; color: #666; } }';
    document.head.appendChild(pageCss);
    st.added.push(pageCss);

    /* 3. every nav view, in menu order, each under its path */
    var main = document.querySelector('main.report-body');
    var tree = (typeof buildNavTree === 'function') ? buildNavTree() : null;
    if (main && tree) {
      var rank = _navOrderIndex(tree);
      var secs = Array.prototype.slice.call(main.querySelectorAll(':scope > [data-navpath]'));
      var withPath = secs.map(function (s, i) {
        var p = navPathOf(s);
        var scope = (s.dataset.navscope || 'exact').toLowerCase();
        return { el: s, path: p, global: scope === 'global' || !p.length, r: rank[JSON.stringify(p)], seq: parseInt(s.dataset.navseq || i, 10), i: i };
      }).filter(function (x) { return !x.global; });
      withPath.sort(function (a, b) {
        var ra = (a.r === undefined ? 1e9 : a.r), rb = (b.r === undefined ? 1e9 : b.r);
        return (ra - rb) || (a.seq - b.seq) || (a.i - b.i);
      });
      var lastKey = null;
      withPath.forEach(function (x) {
        st.moved.push({ el: x.el, parent: x.el.parentNode, next: x.el.nextSibling });
        var key = JSON.stringify(x.path);
        if (key !== lastKey) {
          var h = document.createElement('h2');
          h.className = 'dh-print-path';
          h.textContent = _navLabelPath(tree, x.path).join(' \u203A ');
          main.appendChild(h);
          st.added.push(h);
          lastKey = key;
        }
        main.appendChild(x.el); /* move, in menu order */
      });
    }

    /* 4. tables: all filtered rows up to maxRows */
    var maxRows = PRINT_CONFIG.maxRows || 200;
    Object.keys(engines).forEach(function (id) {
      var eng = engines[id];
      var total = eng._getFiltered().length;
      st.engines[id] = true;
      eng._printRows = Math.max(1, maxRows);
      eng.render();
      if (total > maxRows) {
        var sec = document.getElementById('section-' + id);
        var wrap = sec && sec.querySelector('.table-wrapper');
        if (wrap) {
          var note = document.createElement('p');
          note.className = 'dh-print-note';
          note.textContent = 'Showing ' + maxRows + ' of ' + total + ' rows. Full data in the interactive report.';
          wrap.parentNode.insertBefore(note, wrap.nextSibling);
          st.added.push(note);
        }
      }
    });

    /* 5. cover page */
    if (PRINT_CONFIG.cover) {
      var cover = document.createElement('section');
      cover.className = 'dh-print-cover';
      var hdr = document.querySelector('.report-header');
      if (hdr) {
        var logo = hdr.querySelector('.report-logo');
        if (logo) cover.appendChild(logo.cloneNode(true));
        var t = hdr.querySelector('.report-title');
        if (t) { var h1 = document.createElement('h1'); h1.textContent = t.textContent; cover.appendChild(h1); }
        var sub = hdr.querySelector('.report-subtitle');
        if (sub) cover.appendChild(sub.cloneNode(true));
        var info = hdr.querySelector('.info-fields-grid');
        if (info) cover.appendChild(info.cloneNode(true));
        var gen = hdr.querySelector('.header-meta');
        if (gen) cover.appendChild(gen.cloneNode(true));
      }
      var verdict = document.querySelector('.tile-verdict');
      if (verdict) {
        var vwrap = document.createElement('div');
        vwrap.className = 'dh-print-cover-verdict';
        vwrap.appendChild(verdict.cloneNode(true));
        cover.appendChild(vwrap);
      }
      document.body.insertBefore(cover, document.body.firstChild);
      st.added.push(cover);
    }
  }

  function dhAfterPrint() {
    var st = _printState;
    if (!st) return;
    _printState = null;
    /* put sections back exactly where they were, last-moved first */
    for (var i = st.moved.length - 1; i >= 0; i--) {
      var m = st.moved[i];
      m.parent.insertBefore(m.el, m.next && m.next.parentNode === m.parent ? m.next : null);
    }
    st.added.forEach(function (el) { if (el.parentNode) el.parentNode.removeChild(el); });
    Object.keys(st.engines).forEach(function (id) {
      if (engines[id]) { engines[id]._printRows = 0; engines[id].render(); }
    });
    document.documentElement.classList.remove('dh-printing');
    if (st.theme !== themeMode) applyThemeMode(st.theme);
  }

  function initPrint() {
    window.addEventListener('beforeprint', dhBeforePrint);
    window.addEventListener('afterprint', dhAfterPrint);
    var btn = document.getElementById('btn-print');
    if (btn) btn.addEventListener('click', function () { window.print(); });
  }
  window._dhBeforePrint = dhBeforePrint; /* for pipelines that print without events */
  window._dhAfterPrint = dhAfterPrint;

'@

}