/************************************************************* * * Copyright (c) 2019-2025 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. */ /** * @file Mixin that adds a context-menu to MathJax output * * @author dpvc@mathjax.org (Davide Cervone) */ import { mathjax } from '../../mathjax.js'; import { STATE, newState } from '../../core/MathItem.js'; import { MathDocumentConstructor } from '../../core/MathDocument.js'; import { Handler } from '../../core/Handler.js'; import { ComplexityMathDocument, ComplexityMathItem, } from '../../a11y/complexity.js'; import { AssistiveMmlMathDocument, AssistiveMmlMathItem, } from '../../a11y/assistive-mml.js'; import { SpeechMathDocument } from '../../a11y/speech.js'; import { expandable } from '../../util/Options.js'; import { Menu } from './Menu.js'; import '../../a11y/speech/SpeechMenu.js'; /*==========================================================================*/ /** * Generic constructor for Mixins */ export type Constructor = new (...args: any[]) => T; /** * Generic A11Y MathItem type */ export type A11yMathItem = ComplexityMathItem & AssistiveMmlMathItem; /** * Constructor for base MathItem for MenuMathItem */ export type A11yMathItemConstructor = { new (...args: any[]): A11yMathItem; }; /** * Generic A11Y MathDocument type */ export type A11yMathDocument = ComplexityMathDocument< HTMLElement, Text, Document > & SpeechMathDocument & AssistiveMmlMathDocument; /** * Constructor for base document for MenuMathDocument */ export type A11yDocumentConstructor = MathDocumentConstructor; /*==========================================================================*/ /** * Add STATE value for menus being added (after TYPESET and before INSERTED) */ newState('CONTEXT_MENU', 170); /** * The new function for MathItem that adds the context menu */ export interface MenuMathItem extends ComplexityMathItem< HTMLElement, Text, Document > { /** * @param {MenuMathDocument} document The document where the menu is being added * @param {boolean} force True if menu should be added even if enableMenu is false */ addMenu(document: MenuMathDocument, force?: boolean): void; /** * @param {MenuMathDocument} document The document where the menu is being added */ getMenus(document: MenuMathDocument): void; /** * @param {MenuMathDocument} document The document to check for if anything is being loaded */ checkLoading(document: MenuMathDocument): void; } /** * The mixin for adding context menus to MathItems * * @param {B} BaseMathItem The MathItem class to be extended * @returns {MenuMathItem} The extended MathItem class * * @template B The MathItem class to extend */ export function MenuMathItemMixin( BaseMathItem: B ): Constructor & B { return class extends BaseMathItem { /** * @param {MenuMathDocument} document The document where the menu is being added * @param {boolean} force True if menu should be added even if enableMenu is false */ public addMenu(document: MenuMathDocument, force: boolean = false) { if (this.state() >= STATE.CONTEXT_MENU) return; if (!this.isEscaped && (document.options.enableMenu || force)) { document.menu.addMenu(this); } this.state(STATE.CONTEXT_MENU); } /** * @param {MenuMathDocument} document The document where the menu is being added */ public getMenus(document: MenuMathDocument) { (document.menu.menu.store as any).sort(); } /** * @param {MenuMathDocument} document The document to check for if anything is being loaded */ public checkLoading(document: MenuMathDocument) { document.checkLoading(); } }; } /*==========================================================================*/ /** * The properties needed in the MathDocument for context menus */ export interface MenuMathDocument extends ComplexityMathDocument, SpeechMathDocument { /** * The menu associated with this document */ menu: Menu; /** * Add context menus to the MathItems in the MathDocument * * @returns {MenuMathDocument} The MathDocument (so calls can be chained) */ addMenu(): MenuMathDocument; /** * Checks if there are files being loaded by the menu, and cancels the typesetting if so. * * @returns {boolean} True if we need to wait for extensions */ checkLoading(): boolean; } /** * The mixin for adding context menus to MathDocuments * * @param {B} BaseDocument The MathDocument class to be extended * @returns {MenuMathDocument} The extended MathDocument class * * @template B The MathDocument class to extend */ export function MenuMathDocumentMixin( BaseDocument: B ): Constructor & B { return class extends BaseDocument { /** * @override */ public static OPTIONS = { // // These options are from the a11y extensions, which may not be loaded // initially, and so would cause "undefined option" error messages // if a user tries to configure them. So we include them here. // They are overridden by the options from the extensions when // those are loaded (via ...BaseDocument.OPTIONS). // enableEnrichment: true, enableComplexity: true, enableSpeech: true, enableBraille: true, enableExplorer: true, enableExplorerHelp: true, enrichSpeech: 'none', enrichError: (_doc: MenuMathDocument, _math: MenuMathItem, err: Error) => console.warn('Enrichment Error:', err), ...BaseDocument.OPTIONS, MenuClass: Menu, menuOptions: Menu.OPTIONS, enableMenu: true, sre: BaseDocument.OPTIONS.sre || expandable({}), a11y: BaseDocument.OPTIONS.a11y || expandable({}), renderActions: expandable({ ...BaseDocument.OPTIONS.renderActions, addMenu: [STATE.CONTEXT_MENU], getMenus: [STATE.INSERTED + 5, false], checkLoading: [ STATE.UNPROCESSED + 1, (doc: MenuMathDocument) => doc.checkLoading(), '', false, ], }), }; /** * The menu associated with this document */ public menu: Menu; /** * Extend the MathItem class used for this MathDocument * * @override * @class */ constructor(...args: any[]) { super(...args); this.menu = new this.options.MenuClass(this, this.options.menuOptions); const ProcessBits = (this.constructor as typeof BaseDocument).ProcessBits; if (!ProcessBits.has('context-menu')) { ProcessBits.allocate('context-menu'); } this.options.MathItem = MenuMathItemMixin( this.options.MathItem ); const settings = this.menu.settings; const options = this.options; const enrich = (options.enableEnrichment = settings.enrich); options.enableSpeech = settings.speech && enrich; options.enableBraille = settings.braille && enrich; options.enableComplexity = settings.collapsible && enrich; options.enableExplorer = enrich; } /** * Add context menus to the MathItems in the MathDocument * * @returns {MenuMathDocument} The MathDocument (so calls can be chained) */ public addMenu(): MenuMathDocument { if (!this.processed.isSet('context-menu')) { for (const math of this.math) { (math as MenuMathItem).addMenu(this); } this.processed.set('context-menu'); } return this; } /** * @override */ public getMenus() { (this.menu.menu.store as any).sort(); } /** * @override */ public checkLoading(): boolean { let result = true; try { this._checkLoading(); result = false; } catch (err) { if (!err.retry) { throw err; } } return result; } /** * Checks if there are files being loaded by the menu, and restarts the typesetting if so * * @returns {MenuMathDocument} The MathDocument (so calls can be chained) */ public _checkLoading(): MenuMathDocument { if (this.menu.isLoading) { mathjax.retryAfter( this.menu.loadingPromise.catch((err) => console.log(err)) ); } if (this.options.enableComplexity) { this.menu.checkComponent('a11y/complexity'); } if (this.options.enableExplorer) { this.menu.checkComponent('a11y/explorer'); } return this; } /** * @override */ public state(state: number, restore: boolean = false) { super.state(state, restore); if (state < STATE.CONTEXT_MENU) { this.processed.clear('context-menu'); } return this; } }; } /*==========================================================================*/ /** * Add context-menu support to a Handler instance * * @param {Handler} handler The Handler instance to enhance * @returns {Handler} The handler that was modified (for purposes of chaining extensions) */ export function MenuHandler( handler: Handler ): Handler { handler.documentClass = MenuMathDocumentMixin( handler.documentClass as any ); return handler; }