123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460 |
- /*************************************************************
- *
- * Copyright (c) 2009-2022 The MathJax Consortium
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * @fileoverview Explorers based on keyboard events.
- *
- * @author v.sorge@mathjax.org (Volker Sorge)
- */
- import {A11yDocument, Region} from './Region.js';
- import {Explorer, AbstractExplorer} from './Explorer.js';
- import Sre from '../sre.js';
- /**
- * Interface for keyboard explorers. Adds the necessary keyboard events.
- * @interface
- * @extends {Explorer}
- */
- export interface KeyExplorer extends Explorer {
- /**
- * Function to be executed on key down.
- * @param {KeyboardEvent} event The keyboard event.
- */
- KeyDown(event: KeyboardEvent): void;
- /**
- * Function to be executed on focus in.
- * @param {KeyboardEvent} event The keyboard event.
- */
- FocusIn(event: FocusEvent): void;
- /**
- * Function to be executed on focus out.
- * @param {KeyboardEvent} event The keyboard event.
- */
- FocusOut(event: FocusEvent): void;
- }
- /**
- * @constructor
- * @extends {AbstractExplorer}
- *
- * @template T The type that is consumed by the Region of this explorer.
- */
- export abstract class AbstractKeyExplorer<T> extends AbstractExplorer<T> implements KeyExplorer {
- /**
- * Flag indicating if the explorer is attached to an object.
- */
- public attached: boolean = false;
- /**
- * The attached Sre walker.
- * @type {Walker}
- */
- protected walker: Sre.walker;
- private eventsAttached: boolean = false;
- /**
- * @override
- */
- protected events: [string, (x: Event) => void][] =
- super.Events().concat(
- [['keydown', this.KeyDown.bind(this)],
- ['focusin', this.FocusIn.bind(this)],
- ['focusout', this.FocusOut.bind(this)]]);
- /**
- * The original tabindex value before explorer was attached.
- * @type {boolean}
- */
- private oldIndex: number = null;
- /**
- * @override
- */
- public abstract KeyDown(event: KeyboardEvent): void;
- /**
- * @override
- */
- public FocusIn(_event: FocusEvent) {
- }
- /**
- * @override
- */
- public FocusOut(_event: FocusEvent) {
- this.Stop();
- }
- /**
- * @override
- */
- public Update(force: boolean = false) {
- if (!this.active && !force) return;
- this.highlighter.unhighlight();
- let nodes = this.walker.getFocus(true).getNodes();
- if (!nodes.length) {
- this.walker.refocus();
- nodes = this.walker.getFocus().getNodes();
- }
- this.highlighter.highlight(nodes as HTMLElement[]);
- }
- /**
- * @override
- */
- public Attach() {
- super.Attach();
- this.attached = true;
- this.oldIndex = this.node.tabIndex;
- this.node.tabIndex = 1;
- this.node.setAttribute('role', 'application');
- }
- /**
- * @override
- */
- public AddEvents() {
- if (!this.eventsAttached) {
- super.AddEvents();
- this.eventsAttached = true;
- }
- }
- /**
- * @override
- */
- public Detach() {
- if (this.active) {
- this.node.tabIndex = this.oldIndex;
- this.oldIndex = null;
- this.node.removeAttribute('role');
- }
- this.attached = false;
- }
- /**
- * @override
- */
- public Stop() {
- if (this.active) {
- this.highlighter.unhighlight();
- this.walker.deactivate();
- }
- super.Stop();
- }
- }
- /**
- * Explorer that pushes speech to live region.
- * @constructor
- * @extends {AbstractKeyExplorer}
- */
- export class SpeechExplorer extends AbstractKeyExplorer<string> {
- private static updatePromise = Promise.resolve();
- /**
- * The Sre speech generator associated with the walker.
- * @type {SpeechGenerator}
- */
- public speechGenerator: Sre.speechGenerator;
- /**
- * The name of the option used to control when this is being shown
- * @type {string}
- */
- public showRegion: string = 'subtitles';
- private init: boolean = false;
- /**
- * Flag in case the start method is triggered before the walker is fully
- * initialised. I.e., we have to wait for Sre. Then region is re-shown if
- * necessary, as otherwise it leads to incorrect stacking.
- * @type {boolean}
- */
- private restarted: boolean = false;
- /**
- * @constructor
- * @extends {AbstractKeyExplorer}
- */
- constructor(public document: A11yDocument,
- protected region: Region<string>,
- protected node: HTMLElement,
- private mml: string) {
- super(document, region, node);
- this.initWalker();
- }
- /**
- * @override
- */
- public Start() {
- if (!this.attached) return;
- let options = this.getOptions();
- if (!this.init) {
- this.init = true;
- SpeechExplorer.updatePromise = SpeechExplorer.updatePromise.then(async () => {
- return Sre.sreReady()
- .then(() => Sre.setupEngine({locale: options.locale}))
- .then(() => {
- // Important that both are in the same block so speech explorers
- // are restarted sequentially.
- this.Speech(this.walker);
- this.Start();
- });
- })
- .catch((error: Error) => console.log(error.message));
- return;
- }
- super.Start();
- this.speechGenerator = Sre.getSpeechGenerator('Direct');
- this.speechGenerator.setOptions(options);
- this.walker = Sre.getWalker(
- 'table', this.node, this.speechGenerator, this.highlighter, this.mml);
- this.walker.activate();
- this.Update();
- if (this.document.options.a11y[this.showRegion]) {
- SpeechExplorer.updatePromise.then(
- () => this.region.Show(this.node, this.highlighter));
- }
- this.restarted = true;
- }
- /**
- * @override
- */
- public Update(force: boolean = false) {
- super.Update(force);
- let options = this.speechGenerator.getOptions();
- // This is a necessary in case speech options have changed via keypress
- // during walking.
- if (options.modality === 'speech') {
- this.document.options.sre.domain = options.domain;
- this.document.options.sre.style = options.style;
- this.document.options.a11y.speechRules =
- options.domain + '-' + options.style;
- }
- SpeechExplorer.updatePromise = SpeechExplorer.updatePromise.then(async () => {
- return Sre.sreReady()
- .then(() => Sre.setupEngine({modality: options.modality,
- locale: options.locale}))
- .then(() => this.region.Update(this.walker.speech()));
- });
- }
- /**
- * Computes the speech for the current expression once Sre is ready.
- * @param {Walker} walker The sre walker.
- */
- public Speech(walker: Sre.walker) {
- SpeechExplorer.updatePromise.then(() => {
- walker.speech();
- this.node.setAttribute('hasspeech', 'true');
- this.Update();
- if (this.restarted && this.document.options.a11y[this.showRegion]) {
- this.region.Show(this.node, this.highlighter);
- }
- });
- }
- /**
- * @override
- */
- public KeyDown(event: KeyboardEvent) {
- const code = event.keyCode;
- this.walker.modifier = event.shiftKey;
- if (code === 27) {
- this.Stop();
- this.stopEvent(event);
- return;
- }
- if (this.active) {
- this.Move(code);
- if (this.triggerLink(code)) return;
- this.stopEvent(event);
- return;
- }
- if (code === 32 && event.shiftKey || code === 13) {
- this.Start();
- this.stopEvent(event);
- }
- }
- /**
- * Programmatically triggers a link if the focused node contains one.
- * @param {number} code The keycode of the last key pressed.
- */
- protected triggerLink(code: number) {
- if (code !== 13) {
- return false;
- }
- let node = this.walker.getFocus().getNodes()?.[0];
- let focus = node?.
- getAttribute('data-semantic-postfix')?.
- match(/(^| )link($| )/);
- if (focus) {
- node.parentNode.dispatchEvent(new MouseEvent('click'));
- return true;
- }
- return false;
- }
- /**
- * @override
- */
- public Move(key: number) {
- this.walker.move(key);
- this.Update();
- }
- /**
- * Initialises the Sre walker.
- */
- private initWalker() {
- this.speechGenerator = Sre.getSpeechGenerator('Tree');
- let dummy = Sre.getWalker(
- 'dummy', this.node, this.speechGenerator, this.highlighter, this.mml);
- this.walker = dummy;
- }
- /**
- * Retrieves the speech options to sync with document options.
- * @return {{[key: string]: string}} The options settings for the speech
- * generator.
- */
- private getOptions(): {[key: string]: string} {
- let options = this.speechGenerator.getOptions();
- let sreOptions = this.document.options.sre;
- if (options.modality === 'speech' &&
- (options.locale !== sreOptions.locale ||
- options.domain !== sreOptions.domain ||
- options.style !== sreOptions.style)) {
- options.domain = sreOptions.domain;
- options.style = sreOptions.style;
- options.locale = sreOptions.locale;
- this.walker.update(options);
- }
- return options;
- }
- }
- /**
- * Explorer that magnifies what is currently explored. Uses a hover region.
- * @constructor
- * @extends {AbstractKeyExplorer}
- */
- export class Magnifier extends AbstractKeyExplorer<HTMLElement> {
- /**
- * @constructor
- * @extends {AbstractKeyExplorer}
- */
- constructor(public document: A11yDocument,
- protected region: Region<HTMLElement>,
- protected node: HTMLElement,
- private mml: string) {
- super(document, region, node);
- this.walker = Sre.getWalker(
- 'table', this.node, Sre.getSpeechGenerator('Dummy'),
- this.highlighter, this.mml);
- }
- /**
- * @override
- */
- public Update(force: boolean = false) {
- super.Update(force);
- this.showFocus();
- }
- /**
- * @override
- */
- public Start() {
- super.Start();
- if (!this.attached) return;
- this.region.Show(this.node, this.highlighter);
- this.walker.activate();
- this.Update();
- }
- /**
- * Shows the nodes that are currently focused.
- */
- private showFocus() {
- let node = this.walker.getFocus().getNodes()[0] as HTMLElement;
- this.region.Show(node, this.highlighter);
- }
- /**
- * @override
- */
- public Move(key: number) {
- let result = this.walker.move(key);
- if (result) {
- this.Update();
- }
- }
- /**
- * @override
- */
- public KeyDown(event: KeyboardEvent) {
- const code = event.keyCode;
- this.walker.modifier = event.shiftKey;
- if (code === 27) {
- this.Stop();
- this.stopEvent(event);
- return;
- }
- if (this.active && code !== 13) {
- this.Move(code);
- this.stopEvent(event);
- return;
- }
- if (code === 32 && event.shiftKey || code === 13) {
- this.Start();
- this.stopEvent(event);
- }
- }
- }
|