App-Netdisco

 view release on metacpan or  search on metacpan

share/public/javascripts/netdisco.js  view on Meta::CPAN

    landing.focus();
    return;
  }

  // On a category, up and down step between the entries of the menu holding it.
  // Bootstrap walks every visible focusable descendant instead, and the list a
  // category holds open is one, so it descends into that list and the category
  // below is never reached. Sideways is how the list is entered.
  if (event.target === category
      && (event.key === 'ArrowUp' || event.key === 'ArrowDown')) {
    var peers = Array.prototype.slice.call(
      row.parentElement.querySelectorAll(':scope > li > .dropdown-item'));
    var peer = nd_submenu_focus_index(
      peers.length, peers.indexOf(category), event.key);
    event.preventDefault();
    event.stopPropagation();
    if (peer >= 0) { peers[peer].focus() }
    return;
  }

  if (!submenu) { return }

  if (event.key === 'Escape') {
    event.stopPropagation();
    var toggle = event.target.closest('.nav-item.dropdown');
    toggle = toggle && toggle.querySelector(':scope > .dropdown-toggle');
    if (!toggle) { return }
    toggle.focus();
    bootstrap.Dropdown.getOrCreateInstance(toggle).hide();
    return;
  }

  if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') { return }
  event.preventDefault();
  event.stopPropagation();

  var items = Array.prototype.slice.call(
    submenu.querySelectorAll(':scope > li > .dropdown-item'));
  var wanted = nd_submenu_focus_index(
    items.length, items.indexOf(event.target), event.key);

  // Leaving the submenu upward lands on the category that opened it, which is
  // in the menu Bootstrap can drive itself.
  var target = (wanted < 0
    ? submenu.parentElement.querySelector(':scope > .dropdown-toggle')
    : items[wanted]);
  if (target) { target.focus() }
}, true);

// htmx takes the indicator down when the response arrives, but the fragment's
// own script builds its table from a ready callback afterwards, so the raw
// full-length table would paint with no indicator until that finishes.
//
// Quiet DOM rather than a table library's own event. Quiet is not enough on
// its own: the build has gaps of several hundred milliseconds where nothing
// changes because the thread is busy computing, and revealing in one of
// those shows a table that is still moving.
// A frame that took far longer than a frame should is the evidence of that, so
// both conditions have to hold, twice running.
//
// The deadline is an escape hatch for a pane that never goes quiet rather than
// a budget: it starts at the swap, so it covers only the browser's own work,
// never the fetch.
function holdUntilSettled(pane, indicator) {
  if (!pane) return;
  // This is the only thing that reveals a pane, so declining to hold one cannot
  // mean leaving it hidden: whatever hid it is waiting on this.
  if (!indicator) { pane.classList.remove('nd_pane-settling'); return }
  pane.classList.add('nd_pane-settling');
  indicator.classList.add('nd_indicator-held');

  var SMOOTH_FRAME = 50; // ms; a 60Hz frame is 16, and a busy one runs to 800
  var settledFrames = 0;
  var mutated = false;
  var previousFrame = null;
  var deadline = Date.now() + 60000;
  var watcher = new MutationObserver(function () { mutated = true });
  watcher.observe(pane, { childList: true, subtree: true, attributes: true });

  requestAnimationFrame(function frame(now) {
    var smooth = (previousFrame !== null) && ((now - previousFrame) < SMOOTH_FRAME);
    previousFrame = now;
    settledFrames = (smooth && !mutated) ? (settledFrames + 1) : 0;
    mutated = false;

    if (settledFrames < 2 && Date.now() < deadline) { requestAnimationFrame(frame); return }
    watcher.disconnect();
    pane.classList.remove('nd_pane-settling');
    indicator.classList.remove('nd_indicator-held');
  });
}

document.addEventListener('DOMContentLoaded', function() {
  // sidebar form fields should change colour and have bin/copy icon
  document.querySelectorAll('.nd_field-copy-icon').forEach(function (el) { el.style.display = 'none' });
  hideWithTooltip('.nd_field-clear-icon');

  // activate tooltips and popovers, delegated from a container so that
  // content injected later is covered without re-initialising. bootstrap
  // stores one instance per element whatever the component, so the popover
  // has to delegate from a different container than the tooltip.
  new bootstrap.Tooltip(document.body, { selector: '[rel=tooltip]' });
  new bootstrap.Popover(document.documentElement, { selector: '[rel=popover]' });

  // Dismiss a tooltip when the pointer leaves, even if the element still holds
  // focus. Both frameworks trigger on "hover focus", but the previous one hid
  // unconditionally on leave while the replacement keeps the tip up until blur,
  // stranding it over the content beside a sidebar field. Deliberately not
  // solved by dropping "focus" from the trigger, which would also stop a tooltip
  // appearing for someone tabbing through the form.
  document.body.addEventListener('mouseleave', function (event) {
    var eventTarget = event.target;
    var t = eventTarget instanceof Element ? eventTarget.closest('[rel=tooltip]') : null;
    // Native mouseenter/mouseleave fire separately at every ancestor whose own
    // boundary the pointer crossed, not just the delegate match, so a move
    // between an element's own children must not retrigger this.
    if (!t || !document.body.contains(t) || event.target !== t) return;
    var instance = bootstrap.Tooltip.getInstance(t);
    if (instance) { instance.hide(); }
  }, true);



( run in 1.306 second using v1.01-cache-2.11-cpan-54e63673c56 )