|
|
|
const interactableSelectors = [ |
|
'.interactable', |
|
'.custom_interactable', |
|
'.menu_button', |
|
'.right_menu_button', |
|
'.drawer-icon', |
|
'.inline-drawer-icon', |
|
'.paginationjs-pages li a', |
|
'.group_select, .character_select, .bogus_folder_select', |
|
'.avatar-container', |
|
'.tag .tag_remove', |
|
'.bg_example', |
|
'.bg_example .bg_button', |
|
'#options a', |
|
'.mes_buttons .mes_button', |
|
'.extraMesButtons>div:not(.mes_button)', |
|
'.swipe_left, .swipe_right', |
|
'.stscript_btn', |
|
'.select2_choice_clickable+span.select2-container .select2-selection__choice__display', |
|
'.avatar_load_preview', |
|
]; |
|
|
|
if (CSS.supports('selector(:has(*))')) { |
|
|
|
interactableSelectors.push('#extensionsMenu div:has(.extensionsMenuExtensionButton)'); |
|
} |
|
|
|
export const INTERACTABLE_CONTROL_CLASS = 'interactable'; |
|
export const CUSTOM_INTERACTABLE_CONTROL_CLASS = 'custom_interactable'; |
|
|
|
export const NOT_FOCUSABLE_CONTROL_CLASS = 'not_focusable'; |
|
export const DISABLED_CONTROL_CLASS = 'disabled'; |
|
|
|
|
|
|
|
|
|
|
|
const observer = new MutationObserver(mutations => { |
|
mutations.forEach(mutation => { |
|
if (mutation.type === 'childList') { |
|
mutation.addedNodes.forEach(handleNodeChange); |
|
} |
|
if (mutation.type === 'attributes') { |
|
const target = mutation.target; |
|
if (mutation.attributeName === 'class' && target instanceof Element) { |
|
handleNodeChange(target); |
|
} |
|
} |
|
}); |
|
}); |
|
|
|
|
|
|
|
|
|
|
|
function handleNodeChange(node) { |
|
if (node.nodeType === Node.ELEMENT_NODE && node instanceof Element) { |
|
|
|
if (isKeyboardInteractable(node)) { |
|
makeKeyboardInteractable(node); |
|
} |
|
initializeInteractables(node); |
|
|
|
|
|
if (node.classList.contains('scroll-reset-container')) { |
|
applyScrollResetBehavior(node); |
|
} |
|
initializeScrollResetBehaviors(node); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function registerInteractableType(interactableSelector, { disabledByDefault = false, notFocusableByDefault = false } = {}) { |
|
interactableSelectors.push(interactableSelector); |
|
|
|
const interactables = document.querySelectorAll(interactableSelector); |
|
|
|
if (disabledByDefault || notFocusableByDefault) { |
|
interactables.forEach(interactable => { |
|
if (disabledByDefault) interactable.classList.add(DISABLED_CONTROL_CLASS); |
|
if (notFocusableByDefault) interactable.classList.add(NOT_FOCUSABLE_CONTROL_CLASS); |
|
}); |
|
} |
|
|
|
makeKeyboardInteractable(...interactables); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function isKeyboardInteractable(control) { |
|
|
|
return interactableSelectors.some(selector => control.matches(selector)); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function makeKeyboardInteractable(...interactables) { |
|
interactables.forEach(interactable => { |
|
|
|
if (!isKeyboardInteractable(interactable)) { |
|
interactable.classList.add(CUSTOM_INTERACTABLE_CONTROL_CLASS); |
|
} |
|
|
|
|
|
if (!interactable.classList.contains(INTERACTABLE_CONTROL_CLASS)) { |
|
interactable.classList.add(INTERACTABLE_CONTROL_CLASS); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
const hasDisabledOrNotFocusableAncestor = (el) => { |
|
while (el) { |
|
if (el.classList.contains(NOT_FOCUSABLE_CONTROL_CLASS) || el.classList.contains(DISABLED_CONTROL_CLASS)) { |
|
return true; |
|
} |
|
el = el.parentElement; |
|
} |
|
return false; |
|
}; |
|
|
|
|
|
if (!hasDisabledOrNotFocusableAncestor(interactable)) { |
|
if (!interactable.hasAttribute('tabindex')) { |
|
const tabIndex = interactable.getAttribute('data-original-tabindex') ?? '0'; |
|
interactable.setAttribute('tabindex', tabIndex); |
|
} |
|
} else { |
|
interactable.setAttribute('data-original-tabindex', interactable.getAttribute('tabindex')); |
|
interactable.removeAttribute('tabindex'); |
|
} |
|
}); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
function initializeInteractables(element = document) { |
|
const interactables = getAllInteractables(element); |
|
makeKeyboardInteractable(...interactables); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function getAllInteractables(element) { |
|
|
|
return [].concat(...interactableSelectors.map(selector => Array.from(element.querySelectorAll(`${selector}`)))); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
const applyScrollResetBehavior = (container) => { |
|
container.addEventListener('focusout', (e) => { |
|
setTimeout(() => { |
|
const focusedElement = document.activeElement; |
|
if (!container.contains(focusedElement)) { |
|
container.scrollTop = 0; |
|
container.scrollLeft = 0; |
|
} |
|
}, 0); |
|
}); |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
function initializeScrollResetBehaviors(element = document) { |
|
const scrollResetContainers = element.querySelectorAll('.scroll-reset-container'); |
|
scrollResetContainers.forEach(container => applyScrollResetBehavior(container)); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
function handleGlobalKeyDown(event) { |
|
if (event.key === 'Enter') { |
|
if (!(event.target instanceof HTMLElement)) |
|
return; |
|
|
|
|
|
if (event.altKey || event.ctrlKey || event.shiftKey) |
|
return; |
|
|
|
|
|
let target = event.target; |
|
while (target && !isKeyboardInteractable(target)) { |
|
target = target.parentElement; |
|
} |
|
|
|
|
|
if (target && !target.classList.contains(DISABLED_CONTROL_CLASS)) { |
|
console.debug('Triggering click on keyboard-focused interactable control via Enter', target); |
|
target.click(); |
|
} |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
export function initKeyboard() { |
|
|
|
observer.observe(document.body, { |
|
childList: true, |
|
subtree: true, |
|
attributes: true, |
|
attributeFilter: ['class'], |
|
}); |
|
|
|
|
|
initializeInteractables(); |
|
initializeScrollResetBehaviors(); |
|
|
|
|
|
document.addEventListener('keydown', handleGlobalKeyDown); |
|
} |
|
|