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 )