(() => { /** @typedef {{ key: string; elem: HTMLElement; labelElem: HTMLElement | null }} Match */ const addRoot = () => { const root = document.createElement('div'); document.body.appendChild(root); return root; } /** * @param match {Match} * @return {Promise} */ const createLabelElem = (match) => new Promise((resolve, reject) => { const label = Object.assign(document.createElement('div'), { className: '__nullbrowser_findlabel', textContent: match.key, }); requestAnimationFrame(() => { const rect = match.elem.getBoundingClientRect(); if (rect.top < 0 | rect.top > window.innerHeight) return reject(null); if (rect.left < 0 | rect.left > window.innerWidth) return reject(null); resolve(label); requestAnimationFrame(() => { Object.assign(label.style, { position: 'fixed', zIndex: 999999999, // maybe a few more 9s will do top: `${rect.top}px`, left: `${rect.left}px`, backgroundColor: 'yellow', border: '1px solid #000', boxShadow: '0 0 3px #555', color: '#222', padding: '1px 2px', fontSize: '10px', fontWeight: 'bold', fontFamily: 'monospace', }); }); }); }); const hints = { /** @type {string} */ keys: '', /** @type {string} */ searchText: '', /** @type {Array} */ matches: [], /** @type {HTMLElement | null} */ labelsRoot: null, /** @type {boolean} */ openInNewView: false, /** @type {Match | null} */ currentMatch: null, stop() { hints.matches = []; hints.keys = ''; hints.searchText = ''; [...(hints.labelsRoot?.children ?? [])].forEach(child => { child.remove(); }); }, /** * @param selector {string} * @param new_view {boolean} */ start(selector, new_view) { hints.stop() hints.labelsRoot ??= addRoot(); hints.createMatches(selector); hints.openInNewView = new_view; }, /** * @param {string} char * @return {boolean} */ filterOutByKey(char) { if (char.length !== 1) return false; const isDigit = /[0-9]+/.test(char); if (isDigit) { hints.keys = hints.keys + char; } else { hints.searchText = hints.searchText + char.toLowerCase(); } hints.matches = hints.matches.filter(m => { const isKeyMatch = m.key.startsWith(hints.keys); const texts = [ m.elem.textContent || '', m.elem.getAttribute('title') || '', m.elem.getAttribute('label') || '', m.elem.getAttribute('aria-label') || '', ]; const isTextMatch = texts.some(text => text.toLowerCase()?.includes(hints.searchText)); const isMatch = isKeyMatch && isTextMatch; if (!isMatch) { m.labelElem?.remove(); return false } if (m.labelElem) { const text = m.labelElem.textContent const start = text.slice(0, hints.keys.length) const end = text.slice(hints.keys.length) m.labelElem.innerHTML = `${start}${end}` } return true }); if (hints.matches.length === 0) { hints.stop() return false } if (hints.matches.length === 1) { hints.currentMatch = hints.matches[0] hints.stop(); return true } return null }, /** @param selector {string} */ createMatches(selector) { hints.matches = []; const elements = document.querySelectorAll(selector); const matches = [...elements] .filter(el => el.checkVisibility()) .map(async (elem, index) => { /** @type {Match} */ const match = { elem, key: `${index}`.padStart(`${elements.length}`.length, '0'), labelElem: null, } match.labelElem = await createLabelElem(match); return match }); Promise.allSettled(matches).then(matches => { matches.forEach(result => { if (result.status !== 'fulfilled') return; if (result.value.labelElem) hints.labelsRoot.appendChild(result.value.labelElem) hints.matches.push(result.value) }); }); }, }; window._nullbrowser ||= {}; window._nullbrowser.hints ||= hints; })();