UNPKG

device-navigation

Version:

Navigate HTML elements in two dimensions with non-pointer devices.

241 lines (240 loc) 8.14 kB
import { assert, assertWrap } from '@augment-vir/assert'; import { makeWritable } from '@augment-vir/common'; import { isElementFocused } from '@augment-vir/web'; import { css, unsafeCSS } from 'element-vir'; import { NavAction } from '../nav-controller/navigate.js'; /** * Values for the nav attribute that `nav` applies to elements. * * @category Internal */ export var NavValue; (function (NavValue) { NavValue["Disabled"] = "disabled"; NavValue["Group"] = "group"; NavValue["Focused"] = "focused"; NavValue["Active"] = "active"; })(NavValue || (NavValue = {})); /** * The attribute which the `nav` directive applies to elements with a value of {@link NavValue}. * * @category Internal */ export const navAttribute = { /** Name of the attribute. */ name: 'data-nav', /** * Use this to generate a query selector string for the attribute, to be within with JavaScript * queries. */ js( /** * Omit this or set to `undefined` or `''` to generate a selector for all elements with the * nav attribute. */ value) { if (value) { return `[${navAttribute.name}*="${value}"]`; } else { return `[${navAttribute.name}]`; } }, /** Use this to generate a selector for the attribute in CSS. */ css({ baseSelector = '', navValue, } = {}) { return css ` ${unsafeCSS(baseSelector)}${unsafeCSS(navAttribute.js(navValue))} `; }, }; /** * The property with which {@link NavEntry} instances are attached to elements. * * @category Internal */ export const navEntryPropertyKey = 'navEntry'; /** * Checks if the given element has a {@link NavEntry} instance attached to it via * {@link navEntryPropertyKey} * * @category Internal */ export function hasNavEntry(element) { return navEntryPropertyKey in element; } /** * Extracts a {@link NavEntry} instance attached to the given element, if it exists, by checking the * {@link navEntryPropertyKey} property. * * @category Internal */ export function extractNavEntry(element) { if (hasNavEntry(element)) { const navEntry = element[navEntryPropertyKey]; return assertWrap.instanceOf(navEntry, NavEntry, 'Invalid nav entry'); } else { return undefined; } } function createEventListener(navEntry) { return (event) => { if (navEntry.navParams.group || navEntry.navParams.disabled || navEntry.navController.locked) { return; } else if ((event.type === 'mousedown' && !navEntry.navController.options.activateOnMouseUp) || (event.type === 'mouseup' && navEntry.navController.options.activateOnMouseUp)) { if (event.target === navEntry.element) { navEntry.activate(true); } } else if (event.type === 'mouseup' || event.type === 'focus') { if (event.target === navEntry.element) { navEntry.focus(true); } } else if (event.type === 'mousemove') { if (event.target === navEntry.element && navEntry.navValue !== NavValue.Active) { navEntry.focus(true); } } else if (event.type === 'blur' || event.type === 'mouseleave') { // eslint-disable-next-line unicorn/no-lonely-if if (event.target === navEntry.element) { navEntry.activate(false); navEntry.focus(false); } } }; } /** * A class that is attached to all navigable elements. It is attached to the * {@link navEntryPropertyKey} property. * * @category Internal */ export class NavEntry { element; navParams; navTreeNode; navValue; /** Use a singular event listener for all events so it can be removed. */ eventListener = createEventListener(this); constructor(element, navController, navParams) { this.element = element; this.navParams = navParams; this.attachListeners(); this.navController = navController; } /** Set the {@link NavController} and add this instance to it. */ set navController(navController) { if (this._navController !== navController) { this._navController?.removeNavEntry(this); this._navController = navController; navController.addNavEntry(this); } } /** Set the current {@link NavController}. */ get navController() { assert.isDefined(this._navController, 'this.navController has not been set in NavEntry constructor yet.'); return this._navController; } /** Clear all nav values from the element, just leave the plain attribute (without a value). */ clearNavValue() { if (this.navParams.group || this.navController.locked) { return; } makeWritable(this).navValue = undefined; this.element.setAttribute(navAttribute.name, ''); if (isElementFocused(this.element)) { this.element.blur(); } } /** Focus or blur the element. */ focus( /** * - `true` to focus * - `false` to unfocus (or "blur") */ enabled, skipListener) { const previousNavValue = this.navValue; const alreadySet = enabled === (previousNavValue === NavValue.Focused); if (this.navParams.group || this.navController.locked || alreadySet || (!enabled && this.navController.options.alwaysRequireFocused)) { return; } if (enabled) { this.setNavValue(NavValue.Focused); if (!isElementFocused(this.element)) { this.element.focus(); } } else { this.removeNavValue(NavValue.Focused); if (isElementFocused(this.element)) { this.element.blur(); } } if (!skipListener) { void this.navParams.listeners?.focus?.({ element: this.element, navEntry: this, enabled, previousNavValue, }); } return this.navController.triggerNavEntry(this, enabled, NavAction.Focus); } /** Activate or deactivate the element. */ activate( /** * - `true` to activate * - `false` to deactivate */ enabled) { const previousNavValue = this.navValue; const alreadySet = enabled === (previousNavValue === NavValue.Active); if (this.navParams.group || this.navController.locked || alreadySet) { return; } this.focus(enabled, true); if (enabled) { this.setNavValue(NavValue.Active); } else { this.setNavValue(NavValue.Focused); } void this.navParams.listeners?.activate?.({ element: this.element, navEntry: this, enabled, previousNavValue, }); return this.navController.triggerNavEntry(this, enabled, NavAction.Activate); } /** Set the given {@link NavValue} on the element. */ setNavValue(navValue) { makeWritable(this).navValue = navValue; this.element.setAttribute(navAttribute.name, navValue); } /** Remove the given {@link NavValue}, if it exists, from the element. */ removeNavValue(navValue) { if (this.navValue === navValue) { makeWritable(this).navValue = undefined; this.element.setAttribute(navAttribute.name, ''); } } /** Attach default mouse and focus listeners. */ attachListeners() { this.element.addEventListener('mousemove', this.eventListener, true); this.element.addEventListener('mouseleave', this.eventListener, true); this.element.addEventListener('mousedown', this.eventListener, true); this.element.addEventListener('mouseup', this.eventListener, true); this.element.addEventListener('focus', this.eventListener, true); this.element.addEventListener('blur', this.eventListener, true); } }