(() => {
/** @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;
})();