/* * Tab trail 1.0, https://matjazkorosec.com/tools/tab-trail * * MIT License * * Copyright (c) 2026 Matjaž Korošec * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ (function(){ /* Tab trail 1.0. Copyright (c) 2026 Matjaž Korošec. MIT License. https://matjazkorosec.com/tools/tab-trail */ var VERSION = "1.0"; var ID = "tab-trail-host"; var prev = document.getElementById(ID); if (prev) { prev.__close(); return; } var NS = "http://www.w3.org/2000/svg"; var FOCUSABLE = "a[href],area[href],button,input,select,textarea,iframe,summary,audio[controls],video[controls],[contenteditable]:not([contenteditable='false']),[tabindex]"; var ROLES = /^(button|link|tab|menuitem|menuitemcheckbox|menuitemradio|checkbox|radio|switch|option|combobox|slider|treeitem)$/; function walk(root, out) { var list = root.querySelectorAll("*"); for (var i = 0; i < list.length; i++) { if (list[i].id === ID && (!skip || skip.checked)) continue; if (host && root === host.shadowRoot && (!panel.contains(list[i]) || (logList && logList.contains(list[i])))) continue; out.push(list[i]); if (list[i].shadowRoot) walk(list[i].shadowRoot, out); } return out; } function shown(el) { if (el.checkVisibility) return el.checkVisibility({ visibilityProperty: true, opacityProperty: false }); var s = getComputedStyle(el); return s.display !== "none" && s.visibility !== "hidden" && el.getClientRects().length > 0; } function tabbable(el) { if (!el.matches(FOCUSABLE)) return false; if (el.tabIndex < 0 || el.disabled || el.closest("[inert]")) return false; if (el.tagName === "INPUT" && el.type === "hidden") return false; if (el.tagName === "SUMMARY" && (!el.parentElement || el.parentElement.tagName !== "DETAILS" || el.parentElement.querySelector("summary") !== el)) return false; return shown(el); } var stops = [], stopSet = new Set(), upSet = new Set(), missed = []; var GROUP = "[role=tablist],[role=menu],[role=menubar],[role=listbox],[role=radiogroup],[role=grid],[role=treegrid],[role=tree],[role=toolbar]"; var ITEM = /^(tab|menuitem|menuitemcheckbox|menuitemradio|option|radio|gridcell|row|treeitem)$/; function nearTab(el) { for (var p = el.parentElement; p; p = p.parentElement) if (stopSet.has(p)) return true; return upSet.has(el); } function roving(el) { var g = el.closest(GROUP); if (!g || (!ITEM.test(el.getAttribute("role") || "") && !(el.hasAttribute("tabindex") && el.tabIndex < 0))) return false; if (stopSet.has(g) || upSet.has(g)) return true; var id = g.id; return !!id && stops.some(function (s) { return (" " + (s.getAttribute("aria-controls") || "") + " " + (s.getAttribute("aria-owns") || "") + " ").indexOf(" " + id + " ") >= 0; }); } function modal() { try { var l = document.querySelectorAll("dialog:modal"); return l.length ? l[l.length - 1] : null; } catch (e) { return null; } } function within(m, e) { for (var n = e; n; n = n.parentNode || n.host) if (n === m) return true; return false; } function scan() { var all = walk(document, []); var top = modal(); if (top) all = all.filter(function (e) { return within(top, e); }); var found = all.filter(tabbable); var groups = {}; found = found.filter(function (el) { if (el.tagName !== "INPUT" || el.type !== "radio" || !el.name) return true; var key = (el.form ? "f" : "d") + el.name; var g = groups[key] || (groups[key] = found.filter(function (x) { return x.tagName === "INPUT" && x.type === "radio" && x.name === el.name && x.form === el.form; })); var picked = g.filter(function (x) { return x.checked; })[0] || g[0]; return picked === el; }); var pos = found.filter(function (el) { return el.tabIndex > 0; }).sort(function (a, b) { return a.tabIndex - b.tabIndex; }); stops = pos.concat(found.filter(function (el) { return el.tabIndex === 0; })); stopSet = new Set(stops); upSet = new Set(); stops.forEach(function (s) { for (var p = s.parentElement; p && !upSet.has(p); p = p.parentElement) upSet.add(p); }); var miss = all.filter(function (el) { if (stopSet.has(el) || el.closest("#" + ID) || (host && el.getRootNode() === host.shadowRoot)) return false; var t = el.tagName; if (t === "HTML" || t === "BODY" || t === "LABEL" || t === "OPTION" || t === "OPTGROUP") return false; if (el.closest("label") || el.disabled || el.closest("[inert]")) return false; var role = el.getAttribute("role") || ""; var looks = el.hasAttribute("onclick") || ROLES.test(role); var s = getComputedStyle(el); if (s.pointerEvents === "none") return false; if (!looks) { if (s.cursor !== "pointer") return false; var ps = el.parentElement && getComputedStyle(el.parentElement); if (ps && ps.cursor === "pointer") return false; } if (!shown(el)) return false; var r = el.getBoundingClientRect(); if (r.width < 8 || r.height < 8) return false; return !nearTab(el) && !roving(el); }); var missSet = new Set(miss); missed = miss.filter(function (el) { for (var p = el.parentElement; p; p = p.parentElement) if (missSet.has(p)) return false; return true; }); hoverScan(); } var hovers = [], hoverOk = [], hovRule = new Map(); var HOVER = ":hover", FOC = ":focus-within|:focus-visible|:focus"; function parts(sel) { var out = [], depth = 0, start = 0; for (var i = 0; i < sel.length; i++) { var ch = sel.charAt(i); if (ch === "(") depth++; else if (ch === ")") depth--; else if (ch === "," && !depth) { out.push(sel.slice(start, i).trim()); start = i + 1; } } out.push(sel.slice(start).trim()); return out; } function bare(sel, re) { return sel.replace(new RegExp("(^|[\\s>+~(])(" + re + ")", "g"), "$1*").replace(new RegExp(re, "g"), ""); } function reveals(st) { var d = st.getPropertyValue("display"), v = st.getPropertyValue("visibility"), o = st.getPropertyValue("opacity"), mh = st.getPropertyValue("max-height"); return (d !== "" && d !== "none") || v === "visible" || (o !== "" && parseFloat(o) > 0) || (mh !== "" && mh !== "0" && mh !== "0px"); } var cssRules = 0, cssSheets = 0, cssBlocked = []; function nest(sel, up) { if (!up) return sel; return parts(sel).map(function (q) { return q.indexOf("&") >= 0 ? q.split("&").join(":is(" + up + ")") : ":is(" + up + ") " + q; }).join(", "); } function depth(p, k) { var d = 0; for (var i = 0; i < k; i++) { var ch = p.charAt(i); if (ch === "\\") i++; else if (ch === "(") d++; else if (ch === ")") d--; } return d; } function compound(p, k) { var d = 0, i = k - 1; for (; i >= 0; i--) { var ch = p.charAt(i); if (i > 0 && p.charAt(i - 1) === "\\") { i--; continue; } if (ch === ")") d++; else if (ch === "(") { if (!d) break; d--; } else if (!d && /[\s>+~,]/.test(ch)) break; } var c = p.slice(i + 1, k).trim(); return c ? bare(c, HOVER) : ""; } function selfFocus(p) { var m = p.match(new RegExp(FOC)), k = m ? m.index + m[0].length : -1; if (k < 0 || depth(p, k)) return false; for (var d = 0; k < p.length; k++) { var ch = p.charAt(k); if (ch === "\\") k++; else if (ch === "(") d++; else if (ch === ")") d--; else if (!d && /[\s>+~]/.test(ch)) return false; } return true; } var sheetMemo = new WeakMap(); function readRules() { var hov = [], foc = []; cssRules = 0; cssSheets = 0; cssBlocked = []; function take(sel, src) { parts(sel).forEach(function (p) { if (/:focus/.test(p)) return foc.push([bare(p, FOC), selfFocus(p)]); var k = p.indexOf(HOVER), on; if (k < 0) return; if (depth(p, k)) on = compound(p, k); else { var pre = p.slice(0, k); if (!pre.trim() || /[\s>+~]$/.test(pre)) return; on = bare(pre, HOVER); } if (on) hov.push([bare(p, HOVER), on, p, src]); }); } function go(list, src, up) { for (var i = 0; i < list.length; i++) { var r = list[i], sel = up, st = null; if (r.selectorText !== undefined) { sel = nest(r.selectorText, up); st = r.style; } else if (up && r.style && !r.cssRules) st = r.style; if (st) { cssRules++; if (/:hover|:focus/.test(sel) && reveals(st)) take(sel, src); } if (r.cssRules && r.cssRules.length) go(r.cssRules, src, sel); } } var sheets = Array.prototype.slice.call(document.styleSheets).concat(document.adoptedStyleSheets || []); sheets.forEach(function (sh) { var list; try { list = sh.cssRules; } catch (e) { return cssBlocked.push(sh.href || "a style sheet"); } var memo = sheetMemo.get(sh); if (!memo || memo.list !== list || memo.n !== list.length || memo.node !== sh.ownerNode) { var h0 = hov, f0 = foc, c0 = cssRules; hov = []; foc = []; cssRules = 0; try { go(list, sh.href || (sh.ownerNode ? "an inline