UNPKG

lazy-widgets

Version:

Typescript retained mode GUI for the HTML canvas API

213 lines 9.88 kB
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; import { KeyboardDriver } from './KeyboardDriver.js'; import { getTabKeyHelper } from '../helpers/TabKeyHelper.js'; import { TabSelectEvent } from '../events/TabSelectEvent.js'; /** * Unpack a KeyboardEvent into a 4-tuple containing the event's key and modifier * key state. The 4-tuple contains, respectively, the key * {@link https://developer.mozilla.org/docs/Web/API/KeyboardEvent/key | KeyboardEvent.key} * of the event, whether shift is being held, whether ctrl is being held, and * whether alt is being held * * @category Driver */ function unpackKeyboardEvent(event) { return [event.key, event.shiftKey, event.ctrlKey, event.altKey]; } /** * A {@link KeyboardDriver} which listens for key events from HTML DOM elements. * * Note that if a DOM element is unfocused in the DOM to an unbound DOM element, * the root focus is cleared. If this creates issues, other DOM elements can be * bound without listening for key events. * * @category Driver */ export class DOMKeyboardDriver extends KeyboardDriver { constructor() { super(); // Get tab helper. This will be used for checking if tab is pressed in // the "focus" event handler this.tabKeyHelper = getTabKeyHelper(); } /** * Calls preventDefault and stopImmediatePropagation on a keyboard event if * needed. * * @param captureList - List of events that were **maybe** captured by a Root * @param event - The keyboard event that can be preventDefault'ed/stopImmediatePropagation'ed */ maybePreventDefault(captureList, event) { let captured = false; for (const [_event, eventCaptured] of captureList) { if (eventCaptured) { captured = true; break; } } if (captured) { event.preventDefault(); event.stopImmediatePropagation(); } } /** * Check if the {@link KeyboardDriver#focus | root focus} should be cleared * given that the HTML DOM focus has been lost to another HTML DOM element * * @param newTarget - The HTML DOM element to which the focus has been lost to */ shouldClearFocus(newTarget) { if (newTarget === null) { return true; } for (const group of this.groups) { // XXX even if the group is not selectable, the focus should still // not be cleared when a non-selectable group's DOM element is // focused, since it can be focused by clicking with the mouse if (group.domElem === newTarget) { return false; } } return true; } createGroup(options) { var _a, _b; // assert that the DOM element isn't already assigned const domElem = options.domElem; // TODO is this (listenToKeys) still useful? i no longer see a use-case // for this. need to investigate const listenToKeys = !!((_a = options.listenToKeys) !== null && _a !== void 0 ? _a : true); const selectable = !!((_b = options.selectable) !== null && _b !== void 0 ? _b : true); if (!domElem || !(domElem instanceof HTMLElement)) { throw new Error('DOM element is not valid'); } for (const group of this.groups) { if (group.domElem === domElem) { throw new Error('DOM element is already assigned to a group'); } } // make group const group = super.createGroup(options); group.domElem = domElem; // change tabIndex of DOM element group.origTabIndex = domElem.tabIndex; if (!selectable) { // XXX even if an html element is not focusable by default, tabindex // still needs to be set to -1 if it's not meant to be focusable. // this is because there are a lot of edge cases where the tabindex // is reported as -1 if it's not set, but the element is actually // focusable because, for example, contenteditable is true domElem.tabIndex = -1; } else if (domElem.tabIndex < 0) { domElem.tabIndex = 0; } // add listeners group.focusListen = (focusEvent) => __awaiter(this, void 0, void 0, function* () { if (!selectable || group.tabbableRoots.length === 0) { return; } // HACK only auto-send tab event if the focus event was caused by // pressing tab. there is no api for this, but we can monkey-patch // it: // 1. check relatedTarget. if null, then focus was caused by calling // the `.focus()` method // 2. check if tab key is down. focus direction can be determined by // checking if shift key is down // there is also no api for checking if a key is pressed, so we have // to use a global key listener in the page. // the keyboard state is invalid when focusing the window, so an // extra check is also needed for that. if ((focusEvent.relatedTarget && this.tabKeyHelper.pressed) || (yield this.tabKeyHelper.isTabInitiatedFocus())) { // BUG if the focus is caused by the window itself getting // focus, then it's impossible to tell the direction of the tab // since no keydown event is ever dispatched. this means that // tabbing into a window/iframe without pressing shift will have // the correct behaviour, but SHIFT-tabbing into a window will // not: // 1. the last root will be selected (correct) // 2. shift will be detected as not pressed (incorrect) // 3. the first widget will be tabselected instead of the last // (incorrect) // a way to work around this bug would be to detect if there are // any elements with tabindex BEFORE the domElem, only if this // focus is caused by focusing the window, but this won't work // if the bound DOM element is the only element in the page with // a tabindex, and it's very expensive to query the entire DOM // every time the user tabs into a window/iframe // XXX must check enabled roots again because // isTabInitiatedFocus is async, to avoid data races const rootCount = group.tabbableRoots.length; if (rootCount === 0) { return; } const directionReversed = this.tabKeyHelper.directionReversed; const delta = directionReversed ? -1 : 1; let i = directionReversed ? (rootCount - 1) : 0; for (; i >= 0 && i < rootCount; i += delta) { const captureList = group.tabbableRoots[i].dispatchEvent(new TabSelectEvent(null, directionReversed)); for (const [event, captured] of captureList) { if (captured && event.isa(TabSelectEvent)) { return; } } } } }); group.blurListen = (event) => { // XXX should the HTMLElement cast be done? if (this.shouldClearFocus(event.relatedTarget)) { this.clearFocus(); } }; domElem.addEventListener('focus', group.focusListen); domElem.addEventListener('blur', group.blurListen); if (listenToKeys) { group.keydownListen = (event) => { this.maybePreventDefault(this.keyDown(...unpackKeyboardEvent(event), false), event); }; group.keyupListen = (event) => { this.maybePreventDefault(this.keyUp(...unpackKeyboardEvent(event), false), event); }; domElem.addEventListener('keydown', group.keydownListen); domElem.addEventListener('keyup', group.keyupListen); } else { group.keydownListen = null; group.keyupListen = null; } // reference tab helper if this is the first group if (this.groups.length === 1) { this.tabKeyHelper.ref(this); } return group; } deleteGroup(group) { // delete group super.deleteGroup(group); // unreference tab helper if there are no groups anymore if (this.groups.length === 0) { this.tabKeyHelper.unref(this); } // clean up tabIndex group.domElem.tabIndex = group.origTabIndex; // clean up listeners group.domElem.removeEventListener('focus', group.focusListen); group.domElem.removeEventListener('blur', group.blurListen); if (group.keydownListen) { group.domElem.removeEventListener('keydown', group.keydownListen); } if (group.keyupListen) { group.domElem.removeEventListener('keyup', group.keyupListen); } } } //# sourceMappingURL=DOMKeyboardDriver.js.map