Refactor keyboard controls to name "interactable"
This commit is contained in:
parent
e9d4a982c0
commit
55a95c910f
|
@ -4487,7 +4487,7 @@
|
||||||
<div id="rm_PinAndTabs">
|
<div id="rm_PinAndTabs">
|
||||||
<div id="right-nav-panel-tabs" class="">
|
<div id="right-nav-panel-tabs" class="">
|
||||||
<div id="rm_button_selected_ch">
|
<div id="rm_button_selected_ch">
|
||||||
<h2 class="selectable"></h2>
|
<h2 class="interactable"></h2>
|
||||||
</div>
|
</div>
|
||||||
<div id="result_info" class="flex-container" style="display: none;">
|
<div id="result_info" class="flex-container" style="display: none;">
|
||||||
<div id="result_info_text" title="Token counts may be inaccurate and provided just for reference." data-i18n="[title]Token counts may be inaccurate and provided just for reference.">
|
<div id="result_info_text" title="Token counts may be inaccurate and provided just for reference." data-i18n="[title]Token counts may be inaccurate and provided just for reference.">
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
/* All selectors that should act as keyboard buttons by default */
|
/* All selectors that should act as interactables / keyboard buttons by default */
|
||||||
const buttonSelectors = ['.menu_button', '.right_menu_button', '.custom_selectable_button', '.selectable_button'];
|
const interactableSelectors = ['.menu_button', '.right_menu_button', '.custom_interactable', '.interactable'];
|
||||||
|
|
||||||
const SELECTABLE_BUTTON_CLASS = 'selectable_button';
|
export const INTERACTABLE_CONTROL_CLASS = 'interactable';
|
||||||
const CUSTOM_SELECTABE_BUTTON_CLASS = 'custom_selectable_button';
|
export const CUSTOM_INTERACTABLE_CONTROL_CLASS = 'custom_interactable';
|
||||||
|
|
||||||
const NOT_FOCUSABLE_CLASS = 'not_focusable';
|
export const NOT_FOCUSABLE_CONTROL_CLASS = 'not_focusable';
|
||||||
const DISABLED_CLASS = 'disabled';
|
export const DISABLED_CONTROL_CLASS = 'disabled';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An observer that will check if any new buttons are added to the body
|
* An observer that will check if any new interactables are added to the body
|
||||||
* @type {MutationObserver}
|
* @type {MutationObserver}
|
||||||
*/
|
*/
|
||||||
const observer = new MutationObserver(mutations => {
|
const observer = new MutationObserver(mutations => {
|
||||||
|
@ -16,20 +16,20 @@ const observer = new MutationObserver(mutations => {
|
||||||
if (mutation.type === 'childList') {
|
if (mutation.type === 'childList') {
|
||||||
mutation.addedNodes.forEach(node => {
|
mutation.addedNodes.forEach(node => {
|
||||||
if (node.nodeType === Node.ELEMENT_NODE && node instanceof Element) {
|
if (node.nodeType === Node.ELEMENT_NODE && node instanceof Element) {
|
||||||
// Check if the node itself is a button
|
// Check if the node itself is an interactable
|
||||||
if (isKeyboardButton(node)) {
|
if (isKeyboardInteractable(node)) {
|
||||||
enableKeyboardButton(node);
|
makeKeyboardInteractable(node);
|
||||||
}
|
}
|
||||||
// Check for any descendants that might be buttons
|
// Check for any descendants that might be an interactable
|
||||||
const newButtons = getAllButtons(node);
|
const interactables = getAllInteractables(node);
|
||||||
enableKeyboardButton(...newButtons);
|
makeKeyboardInteractable(...interactables);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else if (mutation.type === 'attributes') {
|
} else if (mutation.type === 'attributes') {
|
||||||
const target = mutation.target;
|
const target = mutation.target;
|
||||||
if (mutation.attributeName === 'class' && target instanceof Element) {
|
if (mutation.attributeName === 'class' && target instanceof Element) {
|
||||||
if (isKeyboardButton(target)) {
|
if (isKeyboardInteractable(target)) {
|
||||||
enableKeyboardButton(target);
|
makeKeyboardInteractable(target);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,94 +37,94 @@ const observer = new MutationObserver(mutations => {
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers a button class (for example for an exatension) and makes it keyboard-selectable.
|
* Registers an interactable class (for example for an extension) and makes it keyboard interactable.
|
||||||
* Optionally apply the 'not_focusable' and 'disabled' classes if needed.
|
* Optionally apply the 'not_focusable' and 'disabled' classes if needed.
|
||||||
*
|
*
|
||||||
* @param {string} buttonSelector - The CSS selector for the button (Supports class combinations, chained via dots like <c>tag.actionable</c>, and sub selectors)
|
* @param {string} interactableSelector - The CSS selector for the interactable (Supports class combinations, chained via dots like <c>tag.actionable</c>, and sub selectors)
|
||||||
* @param {object} [options={}] - Optional settings for the button class
|
* @param {object} [options={}] - Optional settings for the interactable
|
||||||
* @param {boolean} [options.disabledByDefault=false] - Whether buttons of this class should be disabled by default
|
* @param {boolean} [options.disabledByDefault=false] - Whether interactables of this class should be disabled by default
|
||||||
* @param {boolean} [options.notFocusableByDefault=false] - Whether buttons of this class should not be focusable by default
|
* @param {boolean} [options.notFocusableByDefault=false] - Whether interactables of this class should not be focusable by default
|
||||||
*/
|
*/
|
||||||
export function registerKeyboardButtonClass(buttonSelector, { disabledByDefault = false, notFocusableByDefault = false } = {}) {
|
export function registerInteractableType(interactableSelector, { disabledByDefault = false, notFocusableByDefault = false } = {}) {
|
||||||
buttonSelectors.push(buttonSelector);
|
interactableSelectors.push(interactableSelector);
|
||||||
|
|
||||||
const buttons = document.querySelectorAll(buttonSelector);
|
const interactables = document.querySelectorAll(interactableSelector);
|
||||||
|
|
||||||
if (disabledByDefault || notFocusableByDefault) {
|
if (disabledByDefault || notFocusableByDefault) {
|
||||||
buttons.forEach(button => {
|
interactables.forEach(interactable => {
|
||||||
if (disabledByDefault) button.classList.add(DISABLED_CLASS);
|
if (disabledByDefault) interactable.classList.add(DISABLED_CONTROL_CLASS);
|
||||||
if (notFocusableByDefault) button.classList.add(NOT_FOCUSABLE_CLASS);
|
if (notFocusableByDefault) interactable.classList.add(NOT_FOCUSABLE_CONTROL_CLASS);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
enableKeyboardButton(...buttons);
|
makeKeyboardInteractable(...interactables);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the given button is a keyboard-enabled button.
|
* Checks if the given control is a keyboard-enabled interactable.
|
||||||
*
|
*
|
||||||
* @param {Element} button - The button element to check
|
* @param {Element} control - The control element to check
|
||||||
* @returns {boolean} Returns true if the button is a keyboard button, false otherwise
|
* @returns {boolean} Returns true if the control is a keyboard interactable, false otherwise
|
||||||
*/
|
*/
|
||||||
export function isKeyboardButton(button) {
|
export function isKeyboardInteractable(control) {
|
||||||
// Check if this button matches any of the selectors
|
// Check if this control matches any of the selectors
|
||||||
return buttonSelectors.some(selector => button.matches(selector));
|
return interactableSelectors.some(selector => control.matches(selector));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets a
|
* Makes all the given controls keyboard interactable and sets their state.
|
||||||
* Adds the 'tabindex' attribute to buttons that are not marked as 'not_focusable' or 'disabled'
|
* If the control doesn't have any of the classes, it will be set to a custom-enabled keyboard interactable.
|
||||||
*
|
*
|
||||||
* @param {Element[]} buttons - The buttons to add the 'tabindex' attribute to
|
* @param {Element[]} interactables - The controls to make interactable and set their state
|
||||||
*/
|
*/
|
||||||
export function enableKeyboardButton(...buttons) {
|
export function makeKeyboardInteractable(...interactables) {
|
||||||
buttons.forEach(button => {
|
interactables.forEach(interactable => {
|
||||||
// If this button doesn't have any of the classes, lets say the caller knows this and wants this to be a custom-enabled keyboard button.
|
// If this control doesn't have any of the classes, lets say the caller knows this and wants this to be a custom-enabled keyboard control.
|
||||||
if (!isKeyboardButton(button)) {
|
if (!isKeyboardInteractable(interactable)) {
|
||||||
button.classList.add(CUSTOM_SELECTABE_BUTTON_CLASS);
|
interactable.classList.add(CUSTOM_INTERACTABLE_CONTROL_CLASS);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Just for CSS styling and future reference, every keyboard selectable control should have a common class
|
// Just for CSS styling and future reference, every keyboard interactable control should have a common class
|
||||||
if (!button.classList.contains(SELECTABLE_BUTTON_CLASS)) {
|
if (!interactable.classList.contains(INTERACTABLE_CONTROL_CLASS)) {
|
||||||
button.classList.add(SELECTABLE_BUTTON_CLASS);
|
interactable.classList.add(INTERACTABLE_CONTROL_CLASS);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set/remove the tabindex accordingly to the classes. Remembering if it had a custom value.
|
// Set/remove the tabindex accordingly to the classes. Remembering if it had a custom value.
|
||||||
if (!button.classList.contains(NOT_FOCUSABLE_CLASS) && !button.classList.contains(DISABLED_CLASS)) {
|
if (!interactable.classList.contains(NOT_FOCUSABLE_CONTROL_CLASS) && !interactable.classList.contains(DISABLED_CONTROL_CLASS)) {
|
||||||
if (!button.hasAttribute('tabindex')) {
|
if (!interactable.hasAttribute('tabindex')) {
|
||||||
const tabIndex = button.getAttribute('data-original-tabindex') ?? '0';
|
const tabIndex = interactable.getAttribute('data-original-tabindex') ?? '0';
|
||||||
button.setAttribute('tabindex', tabIndex);
|
interactable.setAttribute('tabindex', tabIndex);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
button.setAttribute('data-original-tabindex', button.getAttribute('tabindex'));
|
interactable.setAttribute('data-original-tabindex', interactable.getAttribute('tabindex'));
|
||||||
button.removeAttribute('tabindex');
|
interactable.removeAttribute('tabindex');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the focusability of buttons on the given element or the document
|
* Initializes the focusability of controls on the given element or the document
|
||||||
*
|
*
|
||||||
* @param {Element|Document} [element=document] - The element on which to initialize the button focus. Defaults to the document
|
* @param {Element|Document} [element=document] - The element on which to initialize the interactable state. Defaults to the document.
|
||||||
*/
|
*/
|
||||||
function initializeButtonFocus(element = document) {
|
function initializeInteractables(element = document) {
|
||||||
const buttons = getAllButtons(element);
|
const interactables = getAllInteractables(element);
|
||||||
enableKeyboardButton(...buttons);
|
makeKeyboardInteractable(...interactables);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Queries all buttons within the given element based on the button selectors and returns them as an array
|
* Queries all interactables within the given element based on the given selectors and returns them as an array
|
||||||
*
|
*
|
||||||
* @param {Element|Document} element - The element within which to query the buttons
|
* @param {Element|Document} element - The element within which to query the interactables
|
||||||
* @returns {HTMLElement[]} An array containing all the buttons that match the button selectors
|
* @returns {HTMLElement[]} An array containing all the interactables that match the given selectors
|
||||||
*/
|
*/
|
||||||
function getAllButtons(element) {
|
function getAllInteractables(element) {
|
||||||
// Query each selecter individually and combine all to a big array to return
|
// Query each selector individually and combine all to a big array to return
|
||||||
return [].concat(...buttonSelectors.map(selector => Array.from(element.querySelectorAll(`${selector}`))));
|
return [].concat(...interactableSelectors.map(selector => Array.from(element.querySelectorAll(`${selector}`))));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles keydown events on the document to trigger click on Enter key press for buttons
|
* Handles keydown events on the document to trigger click on Enter key press for interactables
|
||||||
*
|
*
|
||||||
* @param {KeyboardEvent} event - The keyboard event
|
* @param {KeyboardEvent} event - The keyboard event
|
||||||
*/
|
*/
|
||||||
|
@ -133,26 +133,26 @@ function handleGlobalKeyDown(event) {
|
||||||
if (!(event.target instanceof HTMLElement))
|
if (!(event.target instanceof HTMLElement))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Only count enter on this button if no modifier key is pressed
|
// Only count enter on this interactable if no modifier key is pressed
|
||||||
if (event.altKey || event.ctrlKey || event.shiftKey)
|
if (event.altKey || event.ctrlKey || event.shiftKey)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Traverse up the DOM tree to find the actual button element
|
// Traverse up the DOM tree to find the actual interactable element
|
||||||
let target = event.target;
|
let target = event.target;
|
||||||
while (target && !isKeyboardButton(target)) {
|
while (target && !isKeyboardInteractable(target)) {
|
||||||
target = target.parentElement;
|
target = target.parentElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trigger click if a valid button is found and it's not disabled
|
// Trigger click if a valid interactable is found and it's not disabled
|
||||||
if (target && !target.classList.contains(DISABLED_CLASS)) {
|
if (target && !target.classList.contains(DISABLED_CONTROL_CLASS)) {
|
||||||
console.debug('Triggering click on keyboard-focused button via Enter', target);
|
console.debug('Triggering click on keyboard-focused interactable control via Enter', target);
|
||||||
target.click();
|
target.click();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes severial keyboard functionalities for ST
|
* Initializes several keyboard functionalities for ST
|
||||||
*/
|
*/
|
||||||
export function initKeyboard() {
|
export function initKeyboard() {
|
||||||
// Start observing the body for added elements and attribute changes
|
// Start observing the body for added elements and attribute changes
|
||||||
|
@ -163,8 +163,8 @@ export function initKeyboard() {
|
||||||
attributeFilter: ['class']
|
attributeFilter: ['class']
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialize tabindex for existing buttons
|
// Initialize interactable state for already existing controls
|
||||||
initializeButtonFocus();
|
initializeInteractables();
|
||||||
|
|
||||||
// Add a global keydown listener
|
// Add a global keydown listener
|
||||||
document.addEventListener('keydown', handleGlobalKeyDown);
|
document.addEventListener('keydown', handleGlobalKeyDown);
|
||||||
|
|
|
@ -22,7 +22,7 @@ import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '
|
||||||
import { isMobile } from './RossAscends-mods.js';
|
import { isMobile } from './RossAscends-mods.js';
|
||||||
import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from './popup.js';
|
import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from './popup.js';
|
||||||
import { debounce_timeout } from './constants.js';
|
import { debounce_timeout } from './constants.js';
|
||||||
import { registerKeyboardButtonClass } from './keyboard.js';
|
import { registerInteractableType } from './keyboard.js';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
TAG_FOLDER_TYPES,
|
TAG_FOLDER_TYPES,
|
||||||
|
@ -1910,7 +1910,7 @@ export function initTags() {
|
||||||
eventSource.on(event_types.CHARACTER_DUPLICATED, copyTags);
|
eventSource.on(event_types.CHARACTER_DUPLICATED, copyTags);
|
||||||
eventSource.makeFirst(event_types.CHAT_CHANGED, () => selected_group ? applyTagsOnGroupSelect() : applyTagsOnCharacterSelect());
|
eventSource.makeFirst(event_types.CHAT_CHANGED, () => selected_group ? applyTagsOnGroupSelect() : applyTagsOnCharacterSelect());
|
||||||
|
|
||||||
registerKeyboardButtonClass('.tag.actionable');
|
registerInteractableType('.tag.actionable');
|
||||||
|
|
||||||
$(document).on('input', '#tag_view_list input[name="auto_sort_tags"]', (evt) => {
|
$(document).on('input', '#tag_view_list input[name="auto_sort_tags"]', (evt) => {
|
||||||
const toggle = $(evt.target).is(':checked');
|
const toggle = $(evt.target).is(':checked');
|
||||||
|
|
|
@ -213,11 +213,11 @@ table.responsiveTable {
|
||||||
|
|
||||||
/* Keyboard/focus navigation styling */
|
/* Keyboard/focus navigation styling */
|
||||||
/** Mimic the outline of keyboard navigation for most most focusable controls */
|
/** Mimic the outline of keyboard navigation for most most focusable controls */
|
||||||
.selectable_button {
|
.interactable {
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selectable_button:focus {
|
.interactable:focus {
|
||||||
outline: 1px solid var(--white100);
|
outline: 1px solid var(--white100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue