From dcc8bd499b07cbdf3d4550641d7122e0419898bc Mon Sep 17 00:00:00 2001
From: "Davide P. Cervone"
Date: Thu, 25 Sep 2025 05:08:33 -0400
Subject: [PATCH 01/17] Make dialogs movable and sizable.
---
components/mjs/core/config.json | 2 +
ts/a11y/explorer.ts | 74 +--
ts/a11y/explorer/KeyExplorer.ts | 299 ++++++------
ts/a11y/speech/SpeechMenu.ts | 34 +-
ts/ui/dialog/CopyDialog.ts | 77 +++
ts/ui/dialog/DraggableDialog.ts | 838 ++++++++++++++++++++++++++++++++
ts/ui/dialog/InfoDialog.ts | 43 ++
ts/ui/dialog/SelectionDialog.ts | 119 +++++
ts/ui/menu/AnnotationMenu.ts | 29 +-
ts/ui/menu/MJContextMenu.ts | 27 +-
ts/ui/menu/Menu.ts | 480 +++++++++---------
ts/ui/menu/SelectableInfo.ts | 85 ----
ts/ui/menu/mj-context-menu.ts | 1 -
13 files changed, 1533 insertions(+), 575 deletions(-)
create mode 100644 ts/ui/dialog/CopyDialog.ts
create mode 100644 ts/ui/dialog/DraggableDialog.ts
create mode 100644 ts/ui/dialog/InfoDialog.ts
create mode 100644 ts/ui/dialog/SelectionDialog.ts
delete mode 100644 ts/ui/menu/SelectableInfo.ts
diff --git a/components/mjs/core/config.json b/components/mjs/core/config.json
index 7f032dbc4..87e95dc00 100644
--- a/components/mjs/core/config.json
+++ b/components/mjs/core/config.json
@@ -4,6 +4,8 @@
"targets": [
"mathjax.ts",
"core", "util", "handlers",
+ "ui/dialog/DraggableDialog.ts",
+ "ui/dialog/InfoDialog.ts",
"adaptors/HTMLAdaptor.ts",
"adaptors/browserAdaptor.ts",
"components/global.ts"
diff --git a/ts/a11y/explorer.ts b/ts/a11y/explorer.ts
index 7964807c0..34af8d182 100644
--- a/ts/a11y/explorer.ts
+++ b/ts/a11y/explorer.ts
@@ -417,78 +417,6 @@ export function ExplorerMathDocumentMixin<
display: 'inline-flex',
'align-items': 'center',
},
-
- 'mjx-help-sizer': {
- position: 'fixed',
- width: '40%',
- 'max-width': '30em',
- top: '3em',
- left: '50%',
- },
- 'mjx-help-dialog': {
- position: 'absolute',
- width: '200%',
- left: '-100%',
- border: '3px outset',
- 'border-radius': '15px',
- color: 'black',
- 'background-color': '#DDDDDD',
- 'z-index': '301',
- 'text-align': 'right',
- 'font-style': 'normal',
- 'text-indent': 0,
- 'text-transform': 'none',
- 'line-height': 'normal',
- 'letter-spacing': 'normal',
- 'word-spacing': 'normal',
- 'word-wrap': 'normal',
- float: 'none',
- 'box-shadow': '0px 10px 20px #808080',
- outline: 'none',
- },
- 'mjx-help-dialog > h1': {
- 'font-size': '24px',
- 'text-align': 'center',
- margin: '.5em 0',
- },
- 'mjx-help-dialog > div': {
- margin: '0 1em',
- padding: '3px',
- overflow: 'auto',
- height: '20em',
- border: '2px inset black',
- 'background-color': 'white',
- 'text-align': 'left',
- },
- 'mjx-help-dialog > input': {
- margin: '.5em 2em',
- },
- 'mjx-help-dialog kbd': {
- display: 'inline-block',
- padding: '3px 5px',
- 'font-size': '11px',
- 'line-height': '10px',
- color: '#444d56',
- 'vertical-align': 'middle',
- 'background-color': '#fafbfc',
- border: 'solid 1.5px #c6cbd1',
- 'border-bottom-color': '#959da5',
- 'border-radius': '3px',
- 'box-shadow': 'inset -.5px -1px 0 #959da5',
- },
- 'mjx-help-dialog ul': {
- 'list-style-type': 'none',
- },
- 'mjx-help-dialog li': {
- 'margin-bottom': '.5em',
- },
- 'mjx-help-background': {
- position: 'fixed',
- top: 0,
- left: 0,
- right: 0,
- bottom: 0,
- },
};
/**
@@ -556,7 +484,7 @@ export function ExplorerMathDocumentMixin<
SVGNS
),
]);
- this.tmpFocus = this.adaptor.node('mjx-focus', {
+ this.tmpFocus = adaptor.node('mjx-focus', {
tabIndex: 0,
style: {
outline: 'none',
diff --git a/ts/a11y/explorer/KeyExplorer.ts b/ts/a11y/explorer/KeyExplorer.ts
index 943def94d..af0be2b07 100644
--- a/ts/a11y/explorer/KeyExplorer.ts
+++ b/ts/a11y/explorer/KeyExplorer.ts
@@ -30,6 +30,9 @@ import { MmlNode } from '../../core/MmlTree/MmlNode.js';
import { honk, SemAttr } from '../speech/SpeechUtil.js';
import { GeneratorPool } from '../speech/GeneratorPool.js';
import { context } from '../../util/context.js';
+import { InfoDialog } from '../../ui/dialog/InfoDialog.js';
+
+/**********************************************************************/
/**
* Interface for keyboard explorers. Adds the necessary keyboard events.
@@ -70,7 +73,7 @@ export interface KeyExplorer extends Explorer {
/**
* Type of function that implements a key press action
*/
-type keyMapping = (
+export type keyMapping = (
explorer: SpeechExplorer,
event: KeyboardEvent
) => boolean | void;
@@ -106,149 +109,160 @@ export function hasModifiers(
);
}
+/**********************************************************************/
/**********************************************************************/
/**
- * Creates a customized help dialog
+ * @class
+ * @augments {AbstractExplorer}
*
- * @param {string} title The title to use for the message
- * @param {string} select Additional ways to select the typeset math
- * @returns {string} The customized message
+ * @template T The type that is consumed by the Region of this explorer.
*/
-function helpMessage(title: string, select: string): string {
- return `
-Exploring expressions ${title}
+export class SpeechExplorer
+ extends AbstractExplorer
+ implements KeyExplorer
+{
+ /**
+ * Creates a customized help dialog
+ *
+ * @param {string} title The title to use for the message
+ * @param {string} select Additional ways to select the typeset math
+ * @returns {string} The customized message
+ */
+ protected static helpMessage(title: string, select: string): string {
+ return `
+ Exploring expressions ${title}
-The mathematics on this page is being rendered by MathJax, which
-generates both the text spoken by screen readers, as well as the
-visual layout for sighted users.
+ The mathematics on this page is being rendered by MathJax, which
+ generates both the text spoken by screen readers, as well as the
+ visual layout for sighted users.
-Expressions typeset by MathJax can be explored interactively, and
-are focusable. You can use the Tab key to move to a typeset
-expression${select}. Initially, the expression will be read in full,
-but you can use the following keys to explore the expression
-further:
+
Expressions typeset by MathJax can be explored interactively, and
+ are focusable. You can use the Tab key to move to a typeset
+ expression${select}. Initially, the expression will be read in full,
+ but you can use the following keys to explore the expression
+ further:
-
+
-- Down Arrow moves one level deeper into the expression to
-allow you to explore the current subexpression term by term.
+ - Down Arrow moves one level deeper into the
+ expression to allow you to explore the current subexpression term by
+ term.
-- Up Arrow moves back up a level within the expression.
+ - Up Arrow moves back up a level within the
+ expression.
-- Right Arrow moves to the next term in the current
-subexpression.
+ - Right Arrow moves to the next term in the
+ current subexpression.
-- Left Arrow moves to the next term in the current
-subexpression.
+ - Left Arrow moves to the next term in the
+ current subexpression.
-- Shift+Arrow moves to a neighboring cell within a table.
+
- Shift+Arrow moves to a
+ neighboring cell within a table.
-- 0-9+0-9 jumps to a cell by its index in the table, where 0 = 10.
+
- 0-9+0-9 jumps to a cell
+ by its index in the table, where 0 = 10.
-- Home takes you to the top of the expression.
+ - Home takes you to the top of the
+ expression.
-- Enter or Return clicks a link or activates an active
-subexpression.
+ - Enter or Return clicks a
+ link or activates an active subexpression.
-- Space opens the MathJax contextual menu where you can view
-or copy the source format of the expression, or modify MathJax's
-settings.
+ - Space opens the MathJax contextual menu
+ where you can view or copy the source format of the expression, or
+ modify MathJax's settings.
-- Escape exits the expression explorer.
+ - Escape exits the expression
+ explorer.
-- x gives a summary of the current subexpression.
+ - x gives a summary of the current
+ subexpression.
-- z gives the full text of a collapsed expression.
+ - z gives the full text of a collapsed
+ expression.
-- d gives the current depth within the expression.
+ - d gives the current depth within the
+ expression.
-- s starts or stops auto-voicing with synchronized highlighting.
+ - s starts or stops auto-voicing with
+ synchronized highlighting.
-- v marks the current position in the expression.
+ - v marks the current position in the
+ expression.
-- p cycles through the marked positions in the expression.
+ - p cycles through the marked positions in
+ the expression.
-- u clears all marked positions and returns to the starting position.
+ - u clears all marked positions and returns
+ to the starting position.
-- > cycles through the available speech rule sets
-(MathSpeak, ClearSpeak).
+ - > cycles through the available speech
+ rule sets (MathSpeak, ClearSpeak).
-- < cycles through the verbosity levels for the current
-rule set.
+ - < cycles through the verbosity levels
+ for the current rule set.
-- h produces this help listing.
-
+ - h produces this help listing.
+
-The MathJax contextual menu allows you to enable or disable speech
-or Braille generation for mathematical expressions, the language to
-use for the spoken mathematics, and other features of MathJax. In
-particular, the Explorer submenu allows you to specify how the
-mathematics should be identified in the page (e.g., by saying "math"
-when the expression is spoken), and whether or not to include a
-message about the letter "h" bringing up this dialog box.
+ The MathJax contextual menu allows you to enable or disable speech
+ or Braille generation for mathematical expressions, the language to
+ use for the spoken mathematics, and other features of MathJax. In
+ particular, the Explorer submenu allows you to specify how the
+ mathematics should be identified in the page (e.g., by saying "math"
+ when the expression is spoken), and whether or not to include a
+ message about the letter "h" bringing up this dialog box.
-The contextual menu also provides options for viewing or copying a
-MathML version of the expression or its original source format,
-creating an SVG version of the expression, and viewing various other
-information.
+ The contextual menu also provides options for viewing or copying a
+ MathML version of the expression or its original source format,
+ creating an SVG version of the expression, and viewing various other
+ information.
-For more help, see the MathJax accessibility documentation.
-`;
-}
+ For more help, see the MathJax accessibility documentation.
+ `;
+ }
-/**
- * Help for the different OS versions
- */
-const helpData: Map = new Map([
- [
- 'MacOS',
+ /**
+ * Help for the different OS versions
+ */
+ protected static helpData: Map = new Map([
[
- 'on MacOS and iOS using VoiceOver',
- ', or the VoiceOver arrow keys to select an expression',
+ 'MacOS',
+ [
+ 'on MacOS and iOS using VoiceOver',
+ ', or the VoiceOver arrow keys to select an expression',
+ ],
],
- ],
- [
- 'Windows',
[
- 'in Windows using NVDA or JAWS',
- `. The screen reader should enter focus or forms mode automatically
-when the expression gets the browser focus, but if not, you can toggle
-focus mode using NVDA+space in NVDA; for JAWS, Enter should start
-forms mode while Numpad Plus leaves it. Also note that you can use
-the NVDA or JAWS key plus the arrow keys to explore the expression
-even in browse mode, and you can use NVDA+shift+arrow keys to
-navigate out of an expression that has the focus in NVDA`,
+ 'Windows',
+ [
+ 'in Windows using NVDA or JAWS',
+ `. The screen reader should enter focus or forms mode automatically
+ when the expression gets the browser focus, but if not, you can toggle
+ focus mode using NVDA+space in NVDA; for JAWS, Enter should start
+ forms mode while Numpad Plus leaves it. Also note that you can use
+ the NVDA or JAWS key plus the arrow keys to explore the expression
+ even in browse mode, and you can use NVDA+shift+arrow keys to
+ navigate out of an expression that has the focus in NVDA`,
+ ],
],
- ],
- [
- 'Unix',
[
- 'in Unix using Orca',
- `, and Orca should enter focus mode automatically. If not, use the
-Orca+a key to toggle focus mode on or off. Also note that you can use
-Orca+arrow keys to explore expressions even in browse mode`,
+ 'Unix',
+ [
+ 'in Unix using Orca',
+ `, and Orca should enter focus mode automatically. If not, use the
+ Orca+a key to toggle focus mode on or off. Also note that you can use
+ Orca+arrow keys to explore expressions even in browse mode`,
+ ],
],
- ],
- ['unknown', ['with a Screen Reader.', '']],
-]);
-
-/**********************************************************************/
-/**********************************************************************/
+ ['unknown', ['with a Screen Reader.', '']],
+ ]);
-/**
- * @class
- * @augments {AbstractExplorer}
- *
- * @template T The type that is consumed by the Region of this explorer.
- */
-export class SpeechExplorer
- extends AbstractExplorer
- implements KeyExplorer
-{
/*
* The explorer key mapping
*/
@@ -933,40 +947,49 @@ export class SpeechExplorer
* Displays the help dialog.
*/
protected help() {
- const adaptor = this.document.adaptor;
- const helpBackground = adaptor.node('mjx-help-background');
- const close = (event: Event) => {
- helpBackground.remove();
- this.node.focus();
- this.stopEvent(event);
- };
- helpBackground.addEventListener('click', close);
- const helpSizer = adaptor.node('mjx-help-sizer', {}, [
- adaptor.node(
- 'mjx-help-dialog',
- { tabindex: 0, role: 'dialog', 'aria-labeledby': 'mjx-help-label' },
- [
- adaptor.node('h1', { id: 'mjx-help-label' }, [
- adaptor.text('MathJax Expression Explorer Help'),
- ]),
- adaptor.node('div'),
- adaptor.node('input', { type: 'button', value: 'Close' }),
- ]
- ),
- ]);
- helpBackground.append(helpSizer);
- const help = helpSizer.firstChild as HTMLElement;
- help.addEventListener('click', (event) => this.stopEvent(event));
- help.lastChild.addEventListener('click', close);
- help.addEventListener('keydown', (event: KeyboardEvent) => {
- if (event.code === 'Escape') {
- close(event);
- }
+ if (!this.document.options.enableExplorerHelp) {
+ return;
+ }
+ const CLASS = this.constructor as typeof SpeechExplorer;
+ const [title, select] = CLASS.helpData.get(context.os);
+ InfoDialog.post({
+ title: 'MathJax Expression Explorer Help',
+ message: CLASS.helpMessage(title, select),
+ node: this.node,
+ adaptor: this.document.adaptor,
+ styles: {
+ '.mjx-dialog': {
+ 'max-height': 'calc(min(35em, 90%))',
+ },
+ 'mjx-dialog mjx-title': {
+ 'font-size': '133%',
+ margin: '.5em 1.75em',
+ },
+ 'mjx-dialog h2': {
+ 'font-size': '20px',
+ margin: '.5em 0',
+ },
+ 'mjx-dialog kbd': {
+ display: 'inline-block',
+ padding: '3px 5px',
+ 'font-size': '11px',
+ 'line-height': '10px',
+ color: '#444d56',
+ 'vertical-align': 'middle',
+ 'background-color': '#fafbfc',
+ border: 'solid 1.5px #c6cbd1',
+ 'border-bottom-color': '#959da5',
+ 'border-radius': '3px',
+ 'box-shadow': 'inset -.5px -1px 0 #959da5',
+ },
+ 'mjx-dialog ul': {
+ 'list-style-type': 'none',
+ },
+ 'mjx-dialog li': {
+ 'margin-bottom': '.5em',
+ },
+ },
});
- const [title, select] = helpData.get(context.os);
- (help.childNodes[1] as HTMLElement).innerHTML = helpMessage(title, select);
- document.body.append(helpBackground);
- help.focus();
}
/********************************************************************/
diff --git a/ts/a11y/speech/SpeechMenu.ts b/ts/a11y/speech/SpeechMenu.ts
index 97663197d..e76a927cc 100644
--- a/ts/a11y/speech/SpeechMenu.ts
+++ b/ts/a11y/speech/SpeechMenu.ts
@@ -22,7 +22,12 @@
*/
import { ExplorerMathItem } from '../explorer.js';
-import { MJContextMenu } from '../../ui/menu/MJContextMenu.js';
+import { MJContextMenu, SubmenuCallback } from '../../ui/menu/MJContextMenu.js';
+import {
+ SelectionDialog,
+ SelectionOrder,
+ SelectionGrid,
+} from '../../ui/dialog/SelectionDialog.js';
import { SubMenu, Submenu } from '../../ui/menu/mj-context-menu.js';
import * as Sre from '../sre.js';
@@ -113,7 +118,7 @@ let counter = 0;
function csSelectionBox(menu: MJContextMenu, locale: string): object {
const props = localePreferences.get(locale);
csPrefsVariables(menu, Object.keys(props));
- const items = [];
+ const items: any[] = [];
for (const prop of Object.getOwnPropertyNames(props)) {
items.push({
title: prop,
@@ -121,22 +126,19 @@ function csSelectionBox(menu: MJContextMenu, locale: string): object {
variable: 'csprf_' + prop,
});
}
- const sb = menu.factory.get('selectionBox')(
- menu.factory,
- {
- title: 'Clearspeak Preferences',
- signature: '',
- order: 'alphabetic',
- grid: 'square',
- selections: items,
- },
+ const sb = new SelectionDialog(
+ 'Clearspeak Preferences',
+ '',
+ items,
+ SelectionOrder.ALPHABETICAL,
+ SelectionGrid.SQUARE,
menu
);
return {
type: 'command',
id: 'ClearspeakPreferences',
content: 'Select Preferences',
- action: () => sb.post(0, 0),
+ action: () => sb.post(),
};
}
@@ -223,13 +225,13 @@ function smartPreferences(
*
* @param {MJContextMenu} menu The context menu.
* @param {Submenu} sub The submenu to attach elements to.
- * @param {(sub: SubMenu) => void} callback Callback to apply on the constructed
+ * @param {SubmenuCallback} callback Callback to apply on the constructed
* submenu.
*/
export async function clearspeakMenu(
menu: MJContextMenu,
sub: Submenu,
- callback: (sub: SubMenu) => void
+ callback: SubmenuCallback
) {
const exit = (items: object[]) => {
callback(
@@ -288,13 +290,13 @@ let LOCALE_MENU: SubMenu = null;
*
* @param {MJContextMenu} menu The context menu.
* @param {Submenu} sub The submenu to attach elements to.
- * @param {(sub: SubMenu) => void} callback Callback to apply on the constructed
+ * @param {SubmenuCallback} callback Callback to apply on the constructed
* submenu.
*/
export function localeMenu(
menu: MJContextMenu,
sub: Submenu,
- callback: (sub: SubMenu) => void
+ callback: SubmenuCallback
) {
if (LOCALE_MENU) {
callback(LOCALE_MENU);
diff --git a/ts/ui/dialog/CopyDialog.ts b/ts/ui/dialog/CopyDialog.ts
new file mode 100644
index 000000000..d31caebc3
--- /dev/null
+++ b/ts/ui/dialog/CopyDialog.ts
@@ -0,0 +1,77 @@
+/*************************************************************
+ *
+ * Copyright (c) 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 Implements the CopyDialog class (InfoDialog with copy button).
+ *
+ * @author dpvc@mathjax.org (Davide Cervone)
+ */
+
+import { InfoDialog, InfoDialogArgs } from './InfoDialog.js';
+
+/**
+ * The args for a CopyDialog
+ */
+export type CopyDialogArgs = InfoDialogArgs & { code?: boolean };
+
+/**
+ * The CopyDialog subclass of InfoDialog
+ */
+export class CopyDialog extends InfoDialog {
+ /**
+ * @override
+ */
+ public static post(args: CopyDialogArgs) {
+ return super.post(args);
+ }
+
+ /**
+ * @override
+ */
+ protected html(args: CopyDialogArgs) {
+ //
+ // Add a copy-to-clipboard button
+ //
+ args.extraNodes ??= [];
+ const copy = args.adaptor.node('input', {
+ type: 'button',
+ value: 'Copy to Clipboard',
+ 'data-drag': 'none',
+ });
+ copy.addEventListener('click', this.copyToClipboard.bind(this));
+ args.extraNodes.push(copy);
+ //
+ // If this is a code dialog, format the source and set in a pre element
+ //
+ if (args.code) {
+ args.message = '' + this.formatSource(args.message) + '
';
+ }
+ return super.html(args);
+ }
+
+ /**
+ * @param {string} text The text to be displayed in the Info box
+ * @returns {string} The text with HTML specials being escaped
+ */
+ protected formatSource(text: string): string {
+ return text
+ .trim()
+ .replace(/&/g, '&')
+ .replace(//g, '>');
+ }
+}
diff --git a/ts/ui/dialog/DraggableDialog.ts b/ts/ui/dialog/DraggableDialog.ts
new file mode 100644
index 000000000..b90b0926d
--- /dev/null
+++ b/ts/ui/dialog/DraggableDialog.ts
@@ -0,0 +1,838 @@
+/*************************************************************
+ *
+ * Copyright (c) 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 Implements a draggable dialog class.
+ *
+ * @author dpvc@mathjax.org (Davide Cervone)
+ */
+
+import { DOMAdaptor } from '../../core/DOMAdaptor.js';
+import { StyleJson, StyleJsonSheet } from '../../util/StyleJson.js';
+
+export type ADAPTOR = DOMAdaptor;
+
+export type Action = (
+ dialog: DraggableDialog,
+ event: MouseEvent
+) => void | number[];
+export type ActionList = { [action: string]: Action };
+export type ActionMap = { [type: string]: ActionList };
+
+/**
+ * The arguments that control the dialog contents
+ */
+export type DialogArgs = {
+ title?: string; // // the dialog title HTML
+ message: string; // // the dialog message HTML
+ adaptor: ADAPTOR; // // the adaptor to use to create the dialog
+ node?: HTMLElement; // // the node to focus when the dialog closes, if any
+ styles?: StyleJson; // // extra styles to use for the dialog
+ extraNodes?: HTMLElement[]; // // extra HTML nodes to put at the bottom of the dialog
+ className?: string; // // optional class to apply to the dialog
+};
+
+/**
+ * True if we can rely on an HTML dialog element.
+ */
+export const isDialog: boolean = !!window.HTMLDialogElement;
+
+/*========================================================================*/
+
+/**
+ * The draggable dialog class
+ */
+export abstract class DraggableDialog {
+ /**
+ * The minimum width of the dialog
+ */
+ protected minW = 200;
+ /**
+ * The maximum width of the dialog
+ */
+ protected minH = 80;
+
+ /**
+ * The current x translation of the dialog
+ */
+ protected tx: number = 0;
+ /**
+ * The current y translation of the dialog
+ */
+ protected ty: number = 0;
+
+ /**
+ * The current mouse x position
+ */
+ protected x: number;
+ /**
+ * The current mouse y position
+ */
+ protected y: number;
+ /**
+ * The current dialog width
+ */
+ protected w: number;
+ /**
+ * The current dialog height
+ */
+ protected h: number;
+ /**
+ * True when the dialog is being dragged or sized
+ */
+ protected dragging: boolean = false;
+ /**
+ * The drag acction being taken (move, left, right, top, bottom, etc.)
+ */
+ protected action: string;
+
+ /**
+ * Elements where clicking doesn't cause dragging
+ */
+ protected noDrag: HTMLElement[];
+ /**
+ * The title element
+ */
+ protected title: HTMLElement;
+ /**
+ * The content div element
+ */
+ protected content: HTMLElement;
+
+ /**
+ * The node to focus when dialog closes
+ */
+ protected node: HTMLElement;
+ /**
+ * The background element when
',
- ].join('\n');
- },
- 'www.mathjax.org'
- );
+ ].join('\n'),
+ adaptor: this.document.adaptor,
+ extraNodes: [
+ this.document.adaptor.node(
+ 'a',
+ { href: 'https://www.mathjax.org', 'data-drag': 'none' },
+ [this.document.adaptor.text('https://www.mathjax.org')]
+ ),
+ ],
+ });
+ }
/**
* The "Show As MathML" info box
*/
- protected mathmlCode = new SelectableInfo(
- 'MathJax MathML Expression',
- () => {
- if (!this.menu.mathItem) return '';
- const text = this.toMML(this.menu.mathItem);
- return '' + this.formatSource(text) + '
';
- },
- ''
- );
+ protected mathMLCode() {
+ CopyDialog.post({
+ title: 'MathJax MathML Expression',
+ message: this.menu.mathItem ? this.toMML(this.menu.mathItem) : '',
+ adaptor: this.document.adaptor,
+ code: true,
+ });
+ }
/**
* The "Show As (original form)" info box
*/
- protected originalText = new SelectableInfo(
- 'MathJax Original Source',
- () => {
- if (!this.menu.mathItem) return '';
- const text = this.menu.mathItem.math;
- return (
- '' +
- this.formatSource(text) +
- '
'
- );
- },
- ''
- );
+ protected originalText() {
+ CopyDialog.post({
+ title: 'MathJax Original Source',
+ message: this.menu.mathItem?.math ?? '',
+ adaptor: this.document.adaptor,
+ code: true,
+ });
+ }
/**
* The "Show As Annotation" info box
*/
- protected annotationBox = new SelectableInfo(
- 'MathJax Annotation Text',
- () => {
- const text = AnnotationMenu.annotation;
- return (
- '' +
- this.formatSource(text) +
- '
'
- );
- },
- ''
- );
+ protected annotationBox() {
+ CopyDialog.post({
+ title: 'MathJax Annotation Text',
+ message: AnnotationMenu.annotation,
+ adaptor: this.document.adaptor,
+ code: true,
+ });
+ }
/**
* The "Show As SVG Image" info box
*/
- protected svgImage = new SelectableInfo(
- 'MathJax SVG Image',
- () => {
- //
- // SVG image inserted after it is created
- //
- return (
- '' +
- 'Generative SVG Image...
'
- );
- },
- ''
- );
+ public async svgImage() {
+ CopyDialog.post({
+ title: 'MathJax SVG Image',
+ message: await this.toSVG(this.menu.mathItem),
+ adaptor: this.document.adaptor,
+ code: true,
+ });
+ }
/**
* The "Show As Speech Text" info box
*/
- protected speechText = new SelectableInfo(
- 'MathJax Speech Text',
- () => {
- if (!this.menu.mathItem) return '';
- return (
- '' +
- this.formatSource(this.menu.mathItem.outputData.speech) +
- '
'
- );
- },
- ''
- );
+ protected speechText() {
+ CopyDialog.post({
+ title: 'MathJax Speech Text',
+ message: this.menu.mathItem?.outputData?.speech ?? '',
+ adaptor: this.document.adaptor,
+ code: true,
+ });
+ }
/**
* The "Show As Speech Text" info box
*/
- protected brailleText = new SelectableInfo(
- 'MathJax Braille Code',
- () => {
- if (!this.menu.mathItem) return '';
- return (
- '' +
- this.formatSource(this.menu.mathItem.outputData.braille) +
- '
'
- );
- },
- ''
- );
+ protected brailleText() {
+ CopyDialog.post({
+ title: 'MathJax Braille Text',
+ message: this.menu.mathItem?.outputData?.braille ?? '',
+ adaptor: this.document.adaptor,
+ code: true,
+ });
+ }
/**
* The "Show As Error Message" info box
*/
- protected errorMessage = new SelectableInfo(
- 'MathJax Error Message',
- () => {
- if (!this.menu.mathItem) return '';
- return (
- '' +
- this.formatSource(this.menu.errorMsg) +
- '
'
- );
- },
- ''
- );
+ protected errorMessage() {
+ CopyDialog.post({
+ title: 'MathJax Error Message',
+ message: this.menu.mathItem ? this.menu.errorMsg : '',
+ adaptor: this.document.adaptor,
+ code: true,
+ });
+ }
/**
* The info box for zoomed expressions
*/
- protected zoomBox = new Info(
- 'MathJax Zoomed Expression',
- () => {
- if (!this.menu.mathItem) return '';
- const element = (this.menu.mathItem.typesetRoot as any).cloneNode(
- true
- ) as HTMLElement;
- element.style.margin = '0';
- const scale = 1.25 * parseFloat(this.settings.zscale); // 1.25 is to reverse the default 80% font-size
- return (
- '' + element.outerHTML + '
'
- );
- },
- ''
- );
-
- protected postInfo(dialog: Info) {
+ protected zoomBox() {
+ let text = '';
if (this.menu.mathItem) {
- this.menu.nofocus = !!this.menu.mathItem.outputData.nofocus;
+ const node = this.menu.mathItem.typesetRoot as HTMLElement;
+ const size = this.document.adaptor.fontSize(node);
+ const zoom = node.cloneNode(true) as HTMLElement;
+ zoom.style.margin = '0';
+ const scale = (size * parseFloat(this.settings.zscale)) / 100;
+ text = `${zoom.outerHTML}
`;
}
- dialog.post();
+ InfoDialog.post({
+ title: 'MathJax Zoomed Expression',
+ message: text,
+ adaptor: this.document.adaptor,
+ styles: {
+ 'mjx-dialog > div': {
+ padding: '1.8em',
+ },
+ },
+ });
}
/*======================================================================*/
@@ -607,38 +656,23 @@ export class Menu {
],
items: [
this.submenu('Show', 'Show Math As', [
- this.command('MathMLcode', 'MathML Code', () =>
- this.postInfo(this.mathmlCode)
- ),
- this.command('Original', 'Original Form', () =>
- this.postInfo(this.originalText)
- ),
+ this.command('MathMLcode', 'MathML Code', () => this.mathMLCode()),
+ this.command('Original', 'Original Form', () => this.originalText()),
this.rule(),
- this.command(
- 'Speech',
- 'Speech Text',
- () => this.postInfo(this.speechText),
- {
- disabled: true,
- }
- ),
- this.command(
- 'Braille',
- 'Braille Code',
- () => this.postInfo(this.brailleText),
- { disabled: true }
- ),
- this.command('SVG', 'SVG Image', () => this.postSvgImage(), {
+ this.command('Speech', 'Speech Text', () => this.speechText(), {
+ disabled: true,
+ }),
+ this.command('Braille', 'Braille Code', () => this.brailleText(), {
+ disabled: true,
+ }),
+ this.command('SVG', 'SVG Image', () => this.svgImage(), {
disabled: true,
}),
this.submenu('ShowAnnotation', 'Annotation'),
this.rule(),
- this.command(
- 'Error',
- 'Error Message',
- () => this.postInfo(this.errorMessage),
- { disabled: true }
- ),
+ this.command('Error', 'Error Message', () => this.errorMessage(), {
+ disabled: true,
+ }),
]),
this.submenu('Copy', 'Copy to Clipboard', [
this.command('MathMLcode', 'MathML Code', () => this.copyMathML()),
@@ -694,9 +728,7 @@ export class Menu {
this.submenu('Language', 'Language'),
this.rule(),
this.submenu('ZoomTrigger', 'Zoom Trigger', [
- this.command('ZoomNow', 'Zoom Once Now', () =>
- this.zoom(null, '', this.menu.mathItem)
- ),
+ this.command('ZoomNow', 'Zoom Once Now', () => this.zoom(null, '')),
this.rule(),
this.radioGroup('zoom', [
['Click'],
@@ -864,8 +896,8 @@ export class Menu {
),
]),
this.rule(),
- this.command('About', 'About MathJax', () => this.postInfo(this.about)),
- this.command('Help', 'MathJax Help', () => this.postInfo(this.help)),
+ this.command('About', 'About MathJax', () => this.about()),
+ this.command('Help', 'MathJax Help', () => this.help()),
],
}) as MJContextMenu;
const menu = this.menu;
@@ -873,12 +905,11 @@ export class Menu {
menu.findID('Settings', 'Overflow', 'Elide').disable();
menu.findID('Braille', 'ueb').hide();
menu.setJax(this.jax);
- this.attachDialogMenus(menu);
this.checkLoadableItems();
const cache: [string, string][] = [];
MJContextMenu.DynamicSubmenus.set('ShowAnnotation', [
AnnotationMenu.showAnnotations(
- this.annotationBox,
+ () => this.annotationBox(),
this.options.annotationTypes,
cache
),
@@ -892,22 +923,6 @@ export class Menu {
CssStyles.addMenuStyles(this.document.document as any);
}
- /**
- * @param {MJContextMenu} menu The menu to attach
- */
- protected attachDialogMenus(menu: MJContextMenu) {
- this.about.attachMenu(menu);
- this.help.attachMenu(menu);
- this.originalText.attachMenu(menu);
- this.mathmlCode.attachMenu(menu);
- this.originalText.attachMenu(menu);
- this.svgImage.attachMenu(menu);
- this.speechText.attachMenu(menu);
- this.brailleText.attachMenu(menu);
- this.errorMessage.attachMenu(menu);
- this.zoomBox.attachMenu(menu);
- }
-
/**
* Check whether the startup and loader modules are available, and
* if not, disable the a11y modules (since we can't load them
@@ -1475,18 +1490,6 @@ export class Menu {
}
}
- /**
- * @param {string} text The text to be displayed in an Info box
- * @returns {string} The text with HTML specials being escaped
- */
- protected formatSource(text: string): string {
- return text
- .trim()
- .replace(/&/g, '&')
- .replace(//g, '>');
- }
-
/**
* @param {HTMLMATHITEM} math The MathItem to serialize as MathML
* @returns {string} The serialized version of the internal MathML
@@ -1504,12 +1507,11 @@ export class Menu {
* @param {HTMLMATHITEM} math The MathItem to serialize as SVG
* @returns {Promise} A promise returning the serialized SVG
*/
- protected toSVG(math: HTMLMATHITEM): Promise {
+ protected async toSVG(math: HTMLMATHITEM): Promise {
const jax = this.jax.SVG;
- if (!jax)
- return Promise.resolve(
- "SVG can't be produced.
Try switching to SVG output first."
- );
+ if (!jax) {
+ return "SVG can't be produced.
Try switching to SVG output first.";
+ }
const adaptor = jax.adaptor;
const cache = jax.options.fontCache;
const breaks = !!math.root.getProperty('process-breaks');
@@ -1520,9 +1522,7 @@ export class Menu {
) {
for (const child of adaptor.childNodes(math.typesetRoot)) {
if (adaptor.kind(child) === 'svg') {
- return Promise.resolve(
- this.formatSvg(adaptor.serializeXML(child as HTMLElement))
- );
+ return this.formatSvg(adaptor.serializeXML(child as HTMLElement));
}
}
}
@@ -1552,14 +1552,12 @@ export class Menu {
jax.unmarkInlineBreaks(math.root);
math.root.setProperty('inlineMarked', false);
}
- const promise = mathjax.handleRetriesFor(() => {
+ await mathjax.handleRetriesFor(() => {
jax.toDOM(math, div, jax.document);
});
- return promise.then(() => {
- math.root = root;
- jax.options.fontCache = cache;
- return this.formatSvg(jax.adaptor.innerHTML(div));
- });
+ math.root = root;
+ jax.options.fontCache = cache;
+ return this.formatSvg(jax.adaptor.serializeXML(div));
}
/**
@@ -1567,18 +1565,47 @@ export class Menu {
* @returns {string} The adjusted SVG string
*/
protected formatSvg(svg: string): string {
+ //
+ // Insert the minimal CSS styles
+ //
const css = (this.constructor as typeof Menu).SvgCss;
svg = svg.match(/^/)
? svg.replace(//, ``)
: svg.replace(/^()/, `$1`);
+ //
+ // Use black as default color
+ //
svg = svg
.replace(/ (?:role|focusable)=".*?"/g, '')
.replace(/"currentColor"/g, '"black"');
+ //
+ // Add newlines and indentation
+ //
+ const SVG = svg.split(/(<\/?[a-zA-Z].*?>)/);
+ for (let i = 2, spaces = ''; i < SVG.length; i += 2) {
+ const prev = SVG[i - 1];
+ const next = SVG[i + 1];
+ if (prev.charAt(1) !== '/' && prev.charAt(prev.length - 2) !== '/') {
+ spaces += ' ';
+ }
+ if (next) {
+ if (next.charAt(1) === '/') spaces = spaces.slice(2);
+ SVG[i + 1] = next.replace(
+ ' xmlns:xlink="http://www.w3.org/1999/xlink"',
+ ''
+ );
+ }
+ if (SVG[i]) {
+ SVG[i] = '\n ' + spaces + SVG[i].replace(/\n/g, '\n ' + spaces);
+ }
+ SVG[i] += '\n' + spaces;
+ }
+ svg = SVG.join('');
+ //
+ // Remove unwanted attributes
+ //
if (!this.settings.showSRE) {
- svg = svg.replace(
- / (?:data-semantic-.*?|data-speech-node|role|aria-(?:level|posinset|setsize|owns))=".*?"/g,
- ''
- );
+ svg = svg.replace(/ (?:data-semantic-.*?|data-speech-node)=".*?"/g, '');
}
if (!this.settings.showTex) {
svg = svg.replace(/ data-latex(?:-item)?=".*?"/g, '');
@@ -1591,39 +1618,21 @@ export class Menu {
)
.replace(/ data-mml-node="TeXAtom"/g, '');
}
+ //
+ // Return the result
+ //
return `${XMLDECLARATION}\n${svg}`;
}
- /**
- * Get the SVG image and post it
- */
- public postSvgImage() {
- this.postInfo(this.svgImage);
- this.toSVG(this.menu.mathItem).then((svg) => {
- const html = this.svgImage.html.querySelector('#svg-image');
- html.innerHTML = this.formatSource(svg).replace(/\n/g, '
');
- });
- }
-
/*======================================================================*/
/**
* @param {MouseEvent|null} event The event triggering the zoom (or null for from a menu pick)
* @param {string} type The type of event occurring (click, dblclick)
- * @param {HTMLMATHITEM} math The MathItem triggering the event
*/
- protected zoom(event: MouseEvent, type: string, math: HTMLMATHITEM) {
+ protected zoom(event: MouseEvent, type: string) {
if (!event || this.isZoomEvent(event, type)) {
- this.menu.mathItem = math;
- if (event) {
- //
- // The zoomBox.post() below assumes the menu is open,
- // so if this zoom() call is from an event (not the menu),
- // make sure the menu is open before posting the zoom box
- //
- this.menu.post(event);
- }
- this.postInfo(this.zoomBox);
+ this.zoomBox();
}
}
@@ -1717,6 +1726,9 @@ export class Menu {
math.typesetRoot.tabIndex = this.settings.inTabOrder ? 0 : -1;
}
+ /**
+ * @param {HTMLMATHITEM} math The math item to which listeners are to be attached
+ */
public addEvents(math: HTMLMATHITEM) {
const node = math.typesetRoot;
node.addEventListener(
@@ -1740,14 +1752,10 @@ export class Menu {
true
);
node.addEventListener('keydown', () => (this.menu.mathItem = math), true);
- node.addEventListener(
- 'click',
- (event) => this.zoom(event, 'Click', math),
- true
- );
+ node.addEventListener('click', (event) => this.zoom(event, 'Click'), true);
node.addEventListener(
'dblclick',
- (event) => this.zoom(event, 'DoubleClick', math),
+ (event) => this.zoom(event, 'DoubleClick'),
true
);
}
diff --git a/ts/ui/menu/SelectableInfo.ts b/ts/ui/menu/SelectableInfo.ts
deleted file mode 100644
index 34a4127e1..000000000
--- a/ts/ui/menu/SelectableInfo.ts
+++ /dev/null
@@ -1,85 +0,0 @@
-/*************************************************************
- *
- * 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 An info box that allows text selection and has copy-to-clipboard functions
- *
- * @author dpvc@mathjax.org (Davide Cervone)
- */
-
-import { Info, HtmlClasses } from './mj-context-menu.js';
-
-/*==========================================================================*/
-
-/**
- * The SelectableInfo class definition
- */
-export class SelectableInfo extends Info {
- /**
- * Handle "select all" so that only the info-box's text is selected
- * (not the whole page)
- *
- * @override
- */
- public keydown(event: KeyboardEvent) {
- if (event.key === 'a' && (event.ctrlKey || event.metaKey)) {
- this.selectAll();
- this.stop(event);
- return;
- }
- super.keydown(event);
- }
-
- /**
- * Select all the main text of the info box
- */
- public selectAll() {
- const selection = document.getSelection();
- selection.selectAllChildren(
- this.html.querySelector('.CtxtMenu_InfoContent').firstChild
- );
- }
-
- /**
- * Implement the copy-to-clipboard action
- */
- public copyToClipboard() {
- this.selectAll();
- try {
- document.execCommand('copy');
- } catch (err) {
- alert(`Can't copy to clipboard: ${err.message}`);
- }
- document.getSelection().removeAllRanges();
- }
-
- /**
- * Attach the copy-to-clipboard action to its button
- */
- public generateHtml() {
- super.generateHtml();
- const footer = this.html.querySelector(
- 'span.' + HtmlClasses['INFOSIGNATURE']
- );
- const button = footer.appendChild(document.createElement('input'));
- button.type = 'button';
- button.value = 'Copy to Clipboard';
- button.addEventListener('click', (_event: MouseEvent) =>
- this.copyToClipboard()
- );
- }
-}
diff --git a/ts/ui/menu/mj-context-menu.ts b/ts/ui/menu/mj-context-menu.ts
index 7ddaf2b89..0a69005d5 100644
--- a/ts/ui/menu/mj-context-menu.ts
+++ b/ts/ui/menu/mj-context-menu.ts
@@ -25,7 +25,6 @@
export { ContextMenu } from '#menu/context_menu.js';
export { SubMenu } from '#menu/sub_menu.js';
export { Submenu } from '#menu/item_submenu.js';
-export { Info } from '#menu/info.js';
export { Radio } from '#menu/item_radio.js';
export { Rule } from '#menu/item_rule.js';
export { ParserFactory } from '#menu/parser_factory.js';
From a88d24943e4a5f5258984079e322ff2859cd26ab Mon Sep 17 00:00:00 2001
From: "Davide P. Cervone"
Date: Thu, 25 Sep 2025 06:25:19 -0400
Subject: [PATCH 02/17] Fix issue with tests
---
ts/ui/dialog/DraggableDialog.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/ts/ui/dialog/DraggableDialog.ts b/ts/ui/dialog/DraggableDialog.ts
index b90b0926d..4550e1297 100644
--- a/ts/ui/dialog/DraggableDialog.ts
+++ b/ts/ui/dialog/DraggableDialog.ts
@@ -23,6 +23,7 @@
import { DOMAdaptor } from '../../core/DOMAdaptor.js';
import { StyleJson, StyleJsonSheet } from '../../util/StyleJson.js';
+import { context } from '../../util/context.js';
export type ADAPTOR = DOMAdaptor;
@@ -49,7 +50,7 @@ export type DialogArgs = {
/**
* True if we can rely on an HTML dialog element.
*/
-export const isDialog: boolean = !!window.HTMLDialogElement;
+export const isDialog: boolean = !!context.window?.HTMLDialogElement;
/*========================================================================*/
From 9ea36070824de52864f277285e9fbde0f04295bb Mon Sep 17 00:00:00 2001
From: "Davide P. Cervone"
Date: Thu, 25 Sep 2025 16:10:50 -0400
Subject: [PATCH 03/17] Add keyboard control for sizing and moving, and a help
button to explain how it works.
---
ts/a11y/explorer/KeyExplorer.ts | 13 --
ts/ui/dialog/DraggableDialog.ts | 268 ++++++++++++++++++++++++++++----
2 files changed, 239 insertions(+), 42 deletions(-)
diff --git a/ts/a11y/explorer/KeyExplorer.ts b/ts/a11y/explorer/KeyExplorer.ts
index af0be2b07..4aa0e2652 100644
--- a/ts/a11y/explorer/KeyExplorer.ts
+++ b/ts/a11y/explorer/KeyExplorer.ts
@@ -969,19 +969,6 @@ export class SpeechExplorer
'font-size': '20px',
margin: '.5em 0',
},
- 'mjx-dialog kbd': {
- display: 'inline-block',
- padding: '3px 5px',
- 'font-size': '11px',
- 'line-height': '10px',
- color: '#444d56',
- 'vertical-align': 'middle',
- 'background-color': '#fafbfc',
- border: 'solid 1.5px #c6cbd1',
- 'border-bottom-color': '#959da5',
- 'border-radius': '3px',
- 'box-shadow': 'inset -.5px -1px 0 #959da5',
- },
'mjx-dialog ul': {
'list-style-type': 'none',
},
diff --git a/ts/ui/dialog/DraggableDialog.ts b/ts/ui/dialog/DraggableDialog.ts
index 4550e1297..320679f6b 100644
--- a/ts/ui/dialog/DraggableDialog.ts
+++ b/ts/ui/dialog/DraggableDialog.ts
@@ -47,6 +47,14 @@ export type DialogArgs = {
className?: string; // // optional class to apply to the dialog
};
+/**
+ * Type of function that implements a key press action
+ */
+export type keyMapping = (
+ dialog: DraggableDialog,
+ event: KeyboardEvent
+) => void;
+
/**
* True if we can rely on an HTML dialog element.
*/
@@ -57,7 +65,7 @@ export const isDialog: boolean = !!context.window?.HTMLDialogElement;
/**
* The draggable dialog class
*/
-export abstract class DraggableDialog {
+export class DraggableDialog {
/**
* The minimum width of the dialog
*/
@@ -135,6 +143,18 @@ export abstract class DraggableDialog {
['mouseup', this.MouseUp.bind(this)],
];
+ /*
+ * Key bindings for when dialog is open
+ */
+ protected static keyActions: Map = new Map([
+ ['Escape', (dialog, event) => dialog.escKey(event)],
+ ['a', (dialog, event) => dialog.aKey(event)],
+ ['ArrowRight', (dialog, event) => dialog.arrowKey(event, 'right')],
+ ['ArrowLeft', (dialog, event) => dialog.arrowKey(event, 'left')],
+ ['ArrowUp', (dialog, event) => dialog.arrowKey(event, 'up')],
+ ['ArrowDown', (dialog, event) => dialog.arrowKey(event, 'down')],
+ ]);
+
/**
* The style element ID for dialog styles
*/
@@ -144,6 +164,11 @@ export abstract class DraggableDialog {
*/
public static className: string = '';
+ /**
+ * An id incremented for each instance of a dialog
+ */
+ public static id: number = 0;
+
/**
* The default styles for all dialogs
*/
@@ -243,27 +268,27 @@ export abstract class DraggableDialog {
},
//
- // The close button
+ // The dialog buttons
//
- 'mjx-dialog-close': {
+ '.mjx-dialog-button': {
position: 'absolute',
- right: '6px',
top: '6px',
+ height: '17px',
cursor: 'default',
display: 'block',
border: '2px solid #AAA',
'border-radius': '18px',
'font-family': '"Courier New", Courier',
- 'font-size': '24px;',
+ 'text-align': 'center',
color: '#F0F0F0',
'-webkit-user-select': 'none',
'user-select': 'none',
},
- 'mjx-dialog-close:hover': {
+ '.mjx-dialog-button:hover': {
color: 'white !important',
border: '2px solid #CCC !important',
},
- 'mjx-dialog-close-x': {
+ '.mjx-dialog-button > mjx-dialog-icon': {
display: 'block',
'background-color': '#AAA',
border: '1.5px solid',
@@ -271,10 +296,47 @@ export abstract class DraggableDialog {
'line-height': 0,
padding: '8px 0 6px',
},
- 'mjx-dialog-close-x:hover': {
+ '.mjs-dialog-button > mjx-dialog-icon:hover': {
'background-color': '#CCC !important',
},
+ //
+ // The close button
+ //
+ 'mjx-dialog-close': {
+ right: '6px',
+ 'font-size': '24px;',
+ },
+
+ //
+ // The help button
+ //
+ 'mjx-dialog-help': {
+ left: '6px',
+ 'font-size': '16px;',
+ width: '17px',
+ },
+ '.mjx-dialog-help mjx-dialog-help': {
+ display: 'none',
+ },
+
+ //
+ // Key icons in the dialogs
+ //
+ 'mjx-dialog kbd': {
+ display: 'inline-block',
+ padding: '3px 5px',
+ 'font-size': '11px',
+ 'line-height': '10px',
+ color: '#444d56',
+ 'vertical-align': 'middle',
+ 'background-color': '#fafbfc',
+ border: 'solid 1.5px #c6cbd1',
+ 'border-bottom-color': '#959da5',
+ 'border-radius': '3px',
+ 'box-shadow': 'inset -.5px -1px 0 #959da5',
+ },
+
//
// The drag edges and corners
//
@@ -344,6 +406,29 @@ export abstract class DraggableDialog {
},
};
+ protected static helpMessage: string = `
+ The dialog boxes in MathJax are movable and sizeable.
+
+ For mouse users, dragging any of the edges will enlarge or shrink
+ the dialog box by moving that side. Dragging any of the corners
+ changes the two sides that meet at that corner. Dragging elsewhere on
+ the dialog frame will move the dialog without changing its size.
+
+ For keyboard users, to change the dialog size, hold the
+ alt or option key and press any of the arrow
+ keys to enlarge or shrink the dialog box. Left and right move the
+ right-hand edge of the dialog, while up and down move the bottom edge
+ of the dialog. Hold the Win or Command key and
+ press any of the arrow keys to move the dialog box in the given direction.
+ Holding a shift key as well will make the larger changes
+ in the size or position.
+
+ Use Tab to move among the text and buttons and links
+ within the dialog. The Enter or Space key
+ activates the focused item. The Escape key closes the
+ dialog, as does clicking outside the dialog box.
+ `;
+
/**
* @param {DialogArgs} args The data describing the dialog
*/
@@ -363,7 +448,10 @@ export abstract class DraggableDialog {
this.content = this.dialog.firstChild.firstChild.nextSibling as HTMLElement;
const close = this.dialog.lastChild;
close.addEventListener('click', this.closeDialog.bind(this));
- close.addEventListener('keydown', this.closeKey.bind(this));
+ close.addEventListener('keydown', this.actionKey.bind(this, this.closeDialog.bind(this)));
+ const help = this.dialog.lastChild.previousSibling;
+ help.addEventListener('click', this.helpDialog.bind(this, adaptor));
+ help.addEventListener('keydown', this.actionKey.bind(this, this.helpDialog.bind(this, adaptor)));
this.noDrag = Array.from(
this.dialog.querySelectorAll('[data-drag="none"]')
@@ -413,15 +501,16 @@ export abstract class DraggableDialog {
extraNodes.unshift(stylesheet);
}
//
- // Create the doalog HTML tree
+ // Create the dialog HTML tree
//
+ const label = 'mjx-dialog-label-' + DraggableDialog.id++;
const dialog = adaptor.node(
'dialog',
{ closedby: 'any', class: ('mjx-dialog ' + className).trim() },
[
- adaptor.node('mjx-dialog', { 'aria-labeledby': 'mjx-dialog-label' }, [
+ adaptor.node('mjx-dialog', { 'aria-labeledby': label }, [
adaptor.node('mjx-title', {}, [
- adaptor.node('h1', { id: 'mjx-dialog-label', tabIndex: 0 }),
+ adaptor.node('h1', { id: label, tabIndex: 0 }),
]),
adaptor.node('div', { 'data-drag': 'none', tabIndex: 0 }),
]),
@@ -459,16 +548,32 @@ export abstract class DraggableDialog {
'data-drag': 'botright',
'aria-hidden': true,
}),
+ adaptor.node(
+ 'mjx-dialog-help',
+ {
+ class: 'mjx-dialog-button',
+ 'data-drag': 'none',
+ tabIndex: 0,
+ role: 'button',
+ 'aria-label': 'Dialog Help',
+ },
+ [
+ adaptor.node('mjx-dialog-icon', { 'aria-hidden': true }, [
+ adaptor.text('?'),
+ ]),
+ ]
+ ),
adaptor.node(
'mjx-dialog-close',
{
+ class: 'mjx-dialog-button',
'data-drag': 'none',
tabIndex: 0,
role: 'button',
'aria-label': 'Close Dialog Box',
},
[
- adaptor.node('mjx-dialog-close-x', { 'aria-hidden': true }, [
+ adaptor.node('mjx-dialog-icon', { 'aria-hidden': true }, [
adaptor.text('\u00d7'),
]),
]
@@ -530,6 +635,10 @@ export abstract class DraggableDialog {
* od the dialog.
*/
protected actions: ActionMap = {
+ //
+ // Mouse actions
+ //
+
down: {
move: (d) => {
d.dialog.classList.add('mjx-moving');
@@ -573,6 +682,38 @@ export abstract class DraggableDialog {
dg.dialog.classList.remove('mjx-moving');
},
},
+
+ //
+ // Keyboard actions
+ //
+
+ keymove: {
+ left: () => [-5, 0, 0, 0],
+ right: () => [5, 0, 0, 0],
+ up: () => [0, -5, 0, 0],
+ down: () => [0, 5, 0, 0],
+ },
+
+ bigmove: {
+ left: () => [-20, 0, 0, 0],
+ right: () => [20, 0, 0, 0],
+ up: () => [0, -20, 0, 0],
+ down: () => [0, 20, 0, 0],
+ },
+
+ keysize: {
+ left: () => [-3, 0, -6, 0],
+ right: () => [3, 0, 6, 0],
+ up: () => [0, -3, 0, -6],
+ down: () => [0, 3, 0, 6],
+ },
+
+ bigsize: {
+ left: () => [-10, 0, -20, 0],
+ right: () => [10, 0, 20, 0],
+ up: () => [0, -10, 0, -20],
+ down: () => [0, 10, 0, 20],
+ },
};
/**
@@ -582,7 +723,9 @@ export abstract class DraggableDialog {
* @param {MouseEvent} event The event causing the action
*/
protected dragAction(type: string, event: MouseEvent = null) {
- this.stop(event);
+ if (event) {
+ this.stop(event);
+ }
//
// Get the move/resize data for the action
//
@@ -598,7 +741,7 @@ export abstract class DraggableDialog {
if (dw) {
const W = this.w + dw;
if (W >= this.minW) {
- this.x = event.x;
+ this.x = event?.x;
this.w = W;
this.dialog.style.maxWidth = this.dialog.style.width = W + 'px';
} else {
@@ -611,7 +754,7 @@ export abstract class DraggableDialog {
if (dh) {
const H = this.h + dh;
if (H >= this.minH + this.title.offsetHeight) {
- this.y = event.y;
+ this.y = event?.y;
this.h = H;
this.dialog.style.maxHeight = this.dialog.style.height = H + 'px';
} else {
@@ -623,11 +766,11 @@ export abstract class DraggableDialog {
//
if (dx || dy) {
if (dx) {
- this.x = event.x;
+ this.x = event?.x;
this.tx += dx || 0;
}
if (dy) {
- this.y = event.y;
+ this.y = event?.y;
this.ty += dy || 0;
}
this.dialog.style.transform = `translate(${this.tx}px, ${this.ty}px)`;
@@ -706,25 +849,63 @@ export abstract class DraggableDialog {
* @param {KeyboardEvent} event The key event to handle
*/
protected KeyDown(event: KeyboardEvent) {
- if (event.code === 'Escape') {
- this.closeDialog(event);
- return;
+ const CLASS = (this.constructor as typeof DraggableDialog);
+ const action = CLASS.keyActions.get(event.key);
+ if (action) {
+ action(this, event);
}
- if (event.key === 'a' && (event.ctrlKey || event.metaKey)) {
+ }
+
+ /**
+ * Handle the Escape key
+ *
+ * @param {KeyboardEvent} event The key event to handle
+ */
+ protected escKey(event: KeyboardEvent) {
+ this.closeDialog(event);
+ }
+
+ /**
+ * Handle the "a" key for selecting all
+ *
+ * @param {KeyboardEvent} event The key event to handle
+ */
+ protected aKey(event: KeyboardEvent) {
+ if (event.ctrlKey || event.metaKey) {
this.selectAll();
this.stop(event);
- return;
}
}
/**
- * Handle the enter or space key on the close icon
+ * Handle the arrow keys
+ *
+ * @param {KeyboardEvent} event The key event to handle
+ * @param {string} direction The direction of the arrow
+ */
+ protected arrowKey(event: KeyboardEvent, direction: string) {
+ if (event.ctrlKey || this.dragging) return;
+ this.action = direction;
+ this.getWH();
+ if (event.altKey) {
+ this.dragAction(event.shiftKey ? 'bigsize' : 'keysize');
+ this.stop(event);
+ } else if (event.metaKey) {
+ this.dragAction(event.shiftKey ? 'bigmove' : 'keymove');
+ this.stop(event);
+ }
+ this.action = '';
+ }
+
+ /**
+ * Handle the enter or space key on a button icon
*
- * @param {KeyboardEvent} event The event to check
+ * @param {(event: KeyboardEvent) => void} action The action to take on enter or space
+ * @param {KeyboardEvent} event The event to check
*/
- protected closeKey(event: KeyboardEvent) {
+ protected actionKey(action: (event: KeyboardEvent) => void, event: KeyboardEvent) {
if (event.code === 'Enter' || event.code === 'Space') {
- this.closeDialog(event);
+ action(event);
}
}
@@ -760,8 +941,7 @@ export abstract class DraggableDialog {
//
this.x = event.x;
this.y = event.y;
- this.w = this.dialog.clientWidth - 8; // adjust for the 4px padding on all sides
- this.h = this.dialog.clientHeight - 8;
+ this.getWH();
this.dragging = true;
//
// Add the mousemove and mouseup handlers
@@ -772,6 +952,14 @@ export abstract class DraggableDialog {
}
}
+ /**
+ * Cache the current width and height values.
+ */
+ protected getWH() {
+ this.w = this.dialog.clientWidth - 8; // adjust for the 4px padding on all sides
+ this.h = this.dialog.clientHeight - 8;
+ }
+
/**
* End a dragging operation
*/
@@ -806,6 +994,28 @@ export abstract class DraggableDialog {
this.stop(event);
}
+ /**
+ * Display the dialog help message
+ *
+ * @param {ADAPTOR} adaptor The DOM adaptor to use
+ * @param {Event} event The event that triggered the help
+ */
+ protected helpDialog(adaptor: ADAPTOR, event: Event) {
+ const help = new DraggableDialog({
+ title: 'MathJax Dialog Help',
+ message: (this.constructor as typeof DraggableDialog).helpMessage,
+ adaptor: adaptor,
+ className: 'mjx-dialog-help',
+ styles: {
+ '.mjx-dialog-help': {
+ 'max-width': 'calc(min(50em, 80%))',
+ }
+ }
+ });
+ help.attach();
+ this.stop(event);
+ }
+
/**
* Check if an event is inside the dialog.
*
From d3ef2f0dc65e14d746d1a240fc35084d8d7573d2 Mon Sep 17 00:00:00 2001
From: "Davide P. Cervone"
Date: Thu, 25 Sep 2025 16:11:47 -0400
Subject: [PATCH 04/17] Updates for prettier
---
ts/ui/dialog/DraggableDialog.ts | 27 ++++++++++++++++++---------
1 file changed, 18 insertions(+), 9 deletions(-)
diff --git a/ts/ui/dialog/DraggableDialog.ts b/ts/ui/dialog/DraggableDialog.ts
index 320679f6b..547424b19 100644
--- a/ts/ui/dialog/DraggableDialog.ts
+++ b/ts/ui/dialog/DraggableDialog.ts
@@ -448,10 +448,16 @@ export class DraggableDialog {
this.content = this.dialog.firstChild.firstChild.nextSibling as HTMLElement;
const close = this.dialog.lastChild;
close.addEventListener('click', this.closeDialog.bind(this));
- close.addEventListener('keydown', this.actionKey.bind(this, this.closeDialog.bind(this)));
+ close.addEventListener(
+ 'keydown',
+ this.actionKey.bind(this, this.closeDialog.bind(this))
+ );
const help = this.dialog.lastChild.previousSibling;
help.addEventListener('click', this.helpDialog.bind(this, adaptor));
- help.addEventListener('keydown', this.actionKey.bind(this, this.helpDialog.bind(this, adaptor)));
+ help.addEventListener(
+ 'keydown',
+ this.actionKey.bind(this, this.helpDialog.bind(this, adaptor))
+ );
this.noDrag = Array.from(
this.dialog.querySelectorAll('[data-drag="none"]')
@@ -849,7 +855,7 @@ export class DraggableDialog {
* @param {KeyboardEvent} event The key event to handle
*/
protected KeyDown(event: KeyboardEvent) {
- const CLASS = (this.constructor as typeof DraggableDialog);
+ const CLASS = this.constructor as typeof DraggableDialog;
const action = CLASS.keyActions.get(event.key);
if (action) {
action(this, event);
@@ -903,7 +909,10 @@ export class DraggableDialog {
* @param {(event: KeyboardEvent) => void} action The action to take on enter or space
* @param {KeyboardEvent} event The event to check
*/
- protected actionKey(action: (event: KeyboardEvent) => void, event: KeyboardEvent) {
+ protected actionKey(
+ action: (event: KeyboardEvent) => void,
+ event: KeyboardEvent
+ ) {
if (event.code === 'Enter' || event.code === 'Space') {
action(event);
}
@@ -1009,8 +1018,8 @@ export class DraggableDialog {
styles: {
'.mjx-dialog-help': {
'max-width': 'calc(min(50em, 80%))',
- }
- }
+ },
+ },
});
help.attach();
this.stop(event);
@@ -1026,9 +1035,9 @@ export class DraggableDialog {
if (!this.dialog.contains(event.target as HTMLElement)) {
return false;
}
- const {x, y} = event;
- const {left, right, top, bottom} = this.dialog.getBoundingClientRect();
- return (x >= left && x <= right && y >= top && y <= bottom);
+ const { x, y } = event;
+ const { left, right, top, bottom } = this.dialog.getBoundingClientRect();
+ return x >= left && x <= right && y >= top && y <= bottom;
}
/**
From bc11aba9dc45e52534b4833df300c4722df2fdfe Mon Sep 17 00:00:00 2001
From: "Davide P. Cervone"
Date: Fri, 29 Aug 2025 11:36:00 -0400
Subject: [PATCH 05/17] Add option to disable explorer help
---
ts/a11y/explorer.ts | 1 +
ts/a11y/explorer/KeyExplorer.ts | 17 +++++++++++++----
ts/ui/menu/MenuHandler.ts | 1 +
3 files changed, 15 insertions(+), 4 deletions(-)
diff --git a/ts/a11y/explorer.ts b/ts/a11y/explorer.ts
index 34af8d182..8df549bc6 100644
--- a/ts/a11y/explorer.ts
+++ b/ts/a11y/explorer.ts
@@ -321,6 +321,7 @@ export function ExplorerMathDocumentMixin<
public static OPTIONS: OptionList = {
...BaseDocument.OPTIONS,
enableExplorer: hasWindow, // only activate in interactive contexts
+ enableExplorerHelp: true, // help dialog is enabled
renderActions: expandable({
...BaseDocument.OPTIONS.renderActions,
explorable: [STATE.EXPLORER]
diff --git a/ts/a11y/explorer/KeyExplorer.ts b/ts/a11y/explorer/KeyExplorer.ts
index 4aa0e2652..5fc4fa436 100644
--- a/ts/a11y/explorer/KeyExplorer.ts
+++ b/ts/a11y/explorer/KeyExplorer.ts
@@ -619,8 +619,13 @@ export class SpeechExplorer
/**
* Open the help dialog, and refocus when it closes.
+ *
+ * @returns {boolean | void} True cancels the event
*/
- protected hKey() {
+ protected hKey(): boolean | void {
+ if (!this.document.options.enableExplorerHelp) {
+ return true;
+ }
this.refocus = this.current;
this.help();
}
@@ -1072,7 +1077,7 @@ export class SpeechExplorer
if (describe) {
let description =
this.description === this.none ? '' : ', ' + this.description;
- if (this.document.options.a11y.help) {
+ if (this.document.options.a11y.help && this.document.options.enableExplorerHelp) {
description += ', press h for help';
}
speech += description;
@@ -1548,7 +1553,9 @@ export class SpeechExplorer
// and add the info icon.
//
this.node.classList.add('mjx-explorer-active');
- this.node.append(this.document.infoIcon);
+ if (this.document.options.enableExplorerHelp) {
+ this.node.append(this.document.infoIcon);
+ }
//
// Get the node to make current, and determine if we need to add a
// speech node (or just use the top-level node), then set the
@@ -1584,7 +1591,9 @@ export class SpeechExplorer
this.node.setAttribute('aria-roledescription', description);
}
this.node.classList.remove('mjx-explorer-active');
- this.document.infoIcon.remove();
+ if (this.document.options.enableExplorerHelp) {
+ this.document.infoIcon.remove();
+ }
this.pool.unhighlight();
this.magnifyRegion.Hide();
this.region.Hide();
diff --git a/ts/ui/menu/MenuHandler.ts b/ts/ui/menu/MenuHandler.ts
index bf5498d93..1dfdb4e70 100644
--- a/ts/ui/menu/MenuHandler.ts
+++ b/ts/ui/menu/MenuHandler.ts
@@ -201,6 +201,7 @@ export function MenuMathDocumentMixin(
enableSpeech: true,
enableBraille: true,
enableExplorer: true,
+ enableExplorerHelp: true,
enrichSpeech: 'none',
enrichError: (_doc: MenuMathDocument, _math: MenuMathItem, err: Error) =>
console.warn('Enrichment Error:', err),
From 18c9ec4debf039a5e4b8b3e974dd5c16b4a1bc84 Mon Sep 17 00:00:00 2001
From: "Davide P. Cervone"
Date: Fri, 17 Oct 2025 09:01:34 -0400
Subject: [PATCH 06/17] Uppdate for prettier
---
ts/a11y/explorer/KeyExplorer.ts | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/ts/a11y/explorer/KeyExplorer.ts b/ts/a11y/explorer/KeyExplorer.ts
index 5fc4fa436..c57426460 100644
--- a/ts/a11y/explorer/KeyExplorer.ts
+++ b/ts/a11y/explorer/KeyExplorer.ts
@@ -1077,7 +1077,10 @@ export class SpeechExplorer
if (describe) {
let description =
this.description === this.none ? '' : ', ' + this.description;
- if (this.document.options.a11y.help && this.document.options.enableExplorerHelp) {
+ if (
+ this.document.options.a11y.help &&
+ this.document.options.enableExplorerHelp
+ ) {
description += ', press h for help';
}
speech += description;
From 4cf3a3cf3bd228c5dbd7e8793a3fc9e3ea4f765e Mon Sep 17 00:00:00 2001
From: "Davide P. Cervone"
Date: Mon, 20 Oct 2025 08:43:54 -0400
Subject: [PATCH 07/17] Adjust dialog icons for better cross-platform support.
Fix dialog height issue in Safari. Fix wrapping of help dialog when About
MathJax dialog is open.
---
ts/ui/dialog/DraggableDialog.ts | 8 ++++----
ts/ui/menu/Menu.ts | 3 +++
2 files changed, 7 insertions(+), 4 deletions(-)
diff --git a/ts/ui/dialog/DraggableDialog.ts b/ts/ui/dialog/DraggableDialog.ts
index 547424b19..a7aa784c7 100644
--- a/ts/ui/dialog/DraggableDialog.ts
+++ b/ts/ui/dialog/DraggableDialog.ts
@@ -231,7 +231,6 @@ export class DraggableDialog {
all: 'initial',
cursor: 'inherit',
width: '100%',
- height: '100%',
display: 'flex',
'flex-direction': 'column',
'flex-grow': 1,
@@ -274,6 +273,7 @@ export class DraggableDialog {
position: 'absolute',
top: '6px',
height: '17px',
+ width: '17px',
cursor: 'default',
display: 'block',
border: '2px solid #AAA',
@@ -305,7 +305,7 @@ export class DraggableDialog {
//
'mjx-dialog-close': {
right: '6px',
- 'font-size': '24px;',
+ 'font-size': '20px;',
},
//
@@ -313,8 +313,8 @@ export class DraggableDialog {
//
'mjx-dialog-help': {
left: '6px',
- 'font-size': '16px;',
- width: '17px',
+ 'font-size': '14px;',
+ 'font-weight': 'bold',
},
'.mjx-dialog-help mjx-dialog-help': {
display: 'none',
diff --git a/ts/ui/menu/Menu.ts b/ts/ui/menu/Menu.ts
index 0ede2f1eb..2f91ca01f 100644
--- a/ts/ui/menu/Menu.ts
+++ b/ts/ui/menu/Menu.ts
@@ -334,6 +334,9 @@ export class Menu {
'mjx-dialog > div': {
'white-space': 'nowrap',
},
+ 'dialog.mjx-dialog-help > mjx-dialog > div': {
+ 'white-space': 'normal',
+ },
'mjx-v': {
'font-size': '80%',
},
From 2ef10bd61f1f289ef76d95f0233cab55bceda319 Mon Sep 17 00:00:00 2001
From: "Davide P. Cervone"
Date: Sun, 19 Oct 2025 15:41:43 -0400
Subject: [PATCH 08/17] Add color handling for dark mode
---
ts/a11y/complexity/collapse.ts | 2 +-
ts/a11y/explorer.ts | 15 +++++
ts/a11y/explorer/Highlighter.ts | 41 +++---------
ts/a11y/explorer/Region.ts | 100 +++++++++++++++++++++++-----
ts/output/chtml.ts | 9 +++
ts/output/chtml/Wrappers/maction.ts | 19 ++++++
ts/output/svg.ts | 8 +++
ts/output/svg/Wrappers/maction.ts | 18 +++++
ts/ui/dialog/DraggableDialog.ts | 47 ++++++++++++-
ts/ui/menu/Menu.ts | 20 ++++--
ts/util/StyleJson.ts | 24 +++----
11 files changed, 234 insertions(+), 69 deletions(-)
diff --git a/ts/a11y/complexity/collapse.ts b/ts/a11y/complexity/collapse.ts
index f20db9065..24ce13109 100644
--- a/ts/a11y/complexity/collapse.ts
+++ b/ts/a11y/complexity/collapse.ts
@@ -599,7 +599,7 @@ export class Collapse {
),
},
[
- factory.create('mtext', { mathcolor: 'blue', ...variant }, [
+ factory.create('mtext', variant, [
(factory.create('text') as TextNode).setText(marker),
]),
]
diff --git a/ts/a11y/explorer.ts b/ts/a11y/explorer.ts
index 8df549bc6..575533aed 100644
--- a/ts/a11y/explorer.ts
+++ b/ts/a11y/explorer.ts
@@ -418,6 +418,21 @@ export function ExplorerMathDocumentMixin<
display: 'inline-flex',
'align-items': 'center',
},
+ '@media (prefers-color-scheme: dark) /* explorer */': {
+ 'mjx-help > svg': {
+ stroke: '#E0E0E0',
+ },
+ 'mjx-help > svg > circle': {
+ fill: '#404040',
+ },
+ 'mjx-help > svg > circle:nth-child(2)': {
+ fill: 'rgba(132, 132, 255, .3)',
+ },
+ 'mjx-help:hover > svg > circle:nth-child(2)': {
+ stroke: '#AAAAAA',
+ fill: '#404040',
+ },
+ }
};
/**
diff --git a/ts/a11y/explorer/Highlighter.ts b/ts/a11y/explorer/Highlighter.ts
index 7f520914c..27d5c864f 100644
--- a/ts/a11y/explorer/Highlighter.ts
+++ b/ts/a11y/explorer/Highlighter.ts
@@ -21,26 +21,9 @@
interface NamedColor {
color: string;
alpha?: number;
+ type?: string;
}
-interface ChannelColor {
- red: number;
- green: number;
- blue: number;
- alpha?: number;
-}
-
-const namedColors: { [key: string]: ChannelColor } = {
- red: { red: 255, green: 0, blue: 0 },
- green: { red: 0, green: 255, blue: 0 },
- blue: { red: 0, green: 0, blue: 255 },
- yellow: { red: 255, green: 255, blue: 0 },
- cyan: { red: 0, green: 255, blue: 255 },
- magenta: { red: 255, green: 0, blue: 255 },
- white: { red: 255, green: 255, blue: 255 },
- black: { red: 0, green: 0, blue: 0 },
-};
-
/**
* Turns a named color into a channel color.
*
@@ -49,30 +32,22 @@ const namedColors: { [key: string]: ChannelColor } = {
* @returns {string} The channel color.
*/
function getColorString(color: NamedColor, deflt: NamedColor): string {
- const channel = namedColors[color.color] || namedColors[deflt.color];
- channel.alpha = color.alpha ?? deflt.alpha;
- return rgba(channel);
-}
-
-/**
- * RGBa string version of the channel color.
- *
- * @param {ChannelColor} color The channel color.
- * @returns {string} The color in RGBa format.
- */
-function rgba(color: ChannelColor): string {
- return `rgba(${color.red},${color.green},${color.blue},${color.alpha ?? 1})`;
+ const type = deflt.type;
+ const name = color.color ?? deflt.color;
+ const opacity = color.alpha ?? deflt.alpha;
+ const alpha = opacity === 1 ? 1 : `var(--mjx-${type}-alpha)`;
+ return `rgba(var(--mjx-${type}-${name}), ${alpha})`;
}
/**
* The default background color if a none existing color is provided.
*/
-const DEFAULT_BACKGROUND: NamedColor = { color: 'blue', alpha: 1 };
+const DEFAULT_BACKGROUND: NamedColor = { color: 'blue', alpha: 1, type: 'bg' };
/**
* The default color if a none existing color is provided.
*/
-const DEFAULT_FOREGROUND: NamedColor = { color: 'black', alpha: 1 };
+const DEFAULT_FOREGROUND: NamedColor = { color: 'black', alpha: 1, type: 'fg' };
export interface Highlighter {
/**
diff --git a/ts/a11y/explorer/Region.ts b/ts/a11y/explorer/Region.ts
index 109a42d2b..374288c89 100644
--- a/ts/a11y/explorer/Region.ts
+++ b/ts/a11y/explorer/Region.ts
@@ -73,13 +73,6 @@ export abstract class AbstractRegion implements Region {
*/
protected static className: string;
- /**
- * True if the style has already been added to the document.
- *
- * @type {boolean}
- */
- protected static styleAdded: boolean = false;
-
/**
* The CSS style that needs to be added for this type of region.
*
@@ -117,20 +110,24 @@ export abstract class AbstractRegion implements Region {
this.AddStyles();
}
+ /**
+ * @returns {string} The stylesheet ID
+ */
+ public static get sheetId(): string {
+ return 'MJX-' + this.name + '-styles';
+ }
+
/**
* @override
*/
public AddStyles() {
- if (this.CLASS.styleAdded) {
+ const id = this.CLASS.sheetId;
+ if (!this.CLASS.style || this.document.adaptor.head().querySelector('#' + id)) {
return;
}
- // TODO: should that be added to document.documentStyleSheet()?
- const node = this.document.adaptor.node('style');
+ const node = this.document.adaptor.node('style', {id});
node.innerHTML = this.CLASS.style.cssText;
- this.document.adaptor
- .head(this.document.adaptor.document)
- .appendChild(node);
- this.CLASS.styleAdded = true;
+ this.document.adaptor.head().appendChild(node);
}
/**
@@ -177,7 +174,7 @@ export abstract class AbstractRegion implements Region {
*/
public Hide() {
if (!this.div) return;
- this.div.parentNode.removeChild(this.div);
+ this.div.remove();
this.div = null;
this.inner = null;
}
@@ -335,6 +332,13 @@ export class ToolTip extends StringRegion {
'border-radius': 'inherit',
padding: '0 2px',
},
+ '@media (prefers-color-scheme: dark)': {
+ ['.' + ToolTip.className]: {
+ 'background-color': '#222025',
+ 'box-shadow': '0px 5px 20px #000',
+ border: '1px solid #7C7C7C',
+ },
+ },
});
}
@@ -348,6 +352,43 @@ export class LiveRegion extends StringRegion {
* @override
*/
protected static style: StyleJsonSheet = new StyleJsonSheet({
+ ':root': {
+ '--mjx-fg-red': '255, 0, 0',
+ '--mjx-fg-green': '0, 255, 0',
+ '--mjx-fg-blue': '0, 0, 255',
+ '--mjx-fg-yellow': '255, 255, 0',
+ '--mjx-fg-cyan': '0, 255, 255',
+ '--mjx-fg-magenta': '255, 0, 255',
+ '--mjx-fg-white': '255, 255, 255',
+ '--mjx-fg-black': '0, 0, 0',
+ '--mjx-bg-red': '255, 0, 0',
+ '--mjx-bg-green': '0, 255, 0',
+ '--mjx-bg-blue': '0, 0, 255',
+ '--mjx-bg-yellow': '255, 255, 0',
+ '--mjx-bg-cyan': '0, 255, 255',
+ '--mjx-bg-magenta': '255, 0, 255',
+ '--mjx-bg-white': '255, 255, 255',
+ '--mjx-bg-black': '0, 0, 0',
+ '--mjx-live-bg-color': 'white',
+ '--mjx-live-shadow-color': '#888',
+ '--mjx-live-border-color': '#CCCCCC',
+ '--mjx-bg-alpha': 0.2,
+ '--mjx-fg-alpha': 1,
+ },
+ '@media (prefers-color-scheme: dark)': {
+ ':root': {
+ '--mjx-bg-blue': '132, 132, 255',
+ '--mjx-bg-white': '0, 0, 0',
+ '--mjx-bg-black': '255, 255, 255',
+ '--mjx-fg-white': '0, 0, 0',
+ '--mjx-fg-black': '255, 255, 255',
+ '--mjx-live-bg-color': '#222025',
+ '--mjx-live-shadow-color': 'black',
+ '--mjx-live-border-color': '#7C7C7C',
+ '--mjx-bg-alpha': 0.3,
+ '--mjx-fg-alpha': 1,
+ },
+ },
['.' + LiveRegion.className]: {
position: 'absolute',
top: 0,
@@ -360,20 +401,36 @@ export class LiveRegion extends StringRegion {
left: 0,
right: 0,
margin: '0 auto',
- 'background-color': 'white',
- 'box-shadow': '0px 5px 20px #888',
- border: '2px solid #CCCCCC',
+ 'background-color': 'var(--mjx-live-bg-color)',
+ 'box-shadow': '0px 5px 20px var(--mjx-live-shadow-color)',
+ border: '2px solid var(--mjx-live-border-color)',
},
['.' + LiveRegion.className + '_Show']: {
display: 'block',
},
});
+
+ /**
+ * @param {string} type The type of alpha to set (fg or bg)
+ * @param {number} alpha The alpha value to use for the background
+ * @param {Document} document The document whose CSS styles are to be adjusted
+ */
+ public static setAlpha(type: string, alpha: number, document: Document) {
+ const style = document.head.querySelector('#' + this.sheetId) as HTMLStyleElement;
+ if (style) {
+ const name = `--mjx-${type}-alpha`;
+ (style.sheet.cssRules[0] as any).style.setProperty(name, alpha);
+ (style.sheet.cssRules[1] as any).cssRules[0].style.setProperty(name, alpha ** 0.7071);
+ }
+ }
}
/**
* Region class that enables auto voicing of content via SSML markup.
*/
export class SpeechRegion extends LiveRegion {
+ protected static style: StyleJsonSheet = null;
+
/**
* Flag to activate auto voicing.
*/
@@ -583,6 +640,13 @@ export class HoverRegion extends AbstractRegion {
['.' + HoverRegion.className + ' > div']: {
overflow: 'hidden',
},
+ '@media (prefers-color-scheme: dark)': {
+ ['.' + HoverRegion.className]: {
+ 'background-color': '#222025',
+ 'box-shadow': '0px 5px 20px #000',
+ border: '1px solid #7C7C7C',
+ },
+ },
});
/**
diff --git a/ts/output/chtml.ts b/ts/output/chtml.ts
index 68bf483c0..5c33718fa 100644
--- a/ts/output/chtml.ts
+++ b/ts/output/chtml.ts
@@ -151,6 +151,15 @@ export class CHTML extends CommonOutputJax<
'mjx-container [inline-breaks]': { display: 'inline' },
+ 'mjx-container .mjx-selected': {
+ outline: '2px solid black',
+ },
+ '@media (prefers-color-scheme: dark)': {
+ 'mjx-container .mjx-selected': {
+ outline: '2px solid #C8C8C8',
+ },
+ },
+
//
// These don't have Wrapper subclasses, so add their styles here
//
diff --git a/ts/output/chtml/Wrappers/maction.ts b/ts/output/chtml/Wrappers/maction.ts
index eddafb694..9f66e54a8 100644
--- a/ts/output/chtml/Wrappers/maction.ts
+++ b/ts/output/chtml/Wrappers/maction.ts
@@ -188,6 +188,25 @@ export const ChtmlMaction = (function (): ChtmlMactionClass {
'background-color': '#F8F8F8',
color: 'black',
},
+ 'mjx-maction[data-collapsible][toggle="1"]': {
+ color: '#55F',
+ },
+
+ '@media (prefers-color-scheme: dark) /* chtml maction */': {
+ 'mjx-tool > mjx-tip': {
+ border: '1px solid #888',
+ 'background-color': '#303030',
+ color: '#E0E0E0',
+ 'box-shadow': '2px 2px 5px #000',
+ },
+ 'mjx-status': {
+ 'background-color': '#303030',
+ color: '#E0E0E0',
+ },
+ 'mjx-maction[data-collapsible][toggle="1"]': {
+ color: '#88F',
+ },
+ },
};
/**
diff --git a/ts/output/svg.ts b/ts/output/svg.ts
index 38844e48d..179043986 100644
--- a/ts/output/svg.ts
+++ b/ts/output/svg.ts
@@ -114,6 +114,14 @@ export class SVG extends CommonOutputJax<
stroke: 'black',
'stroke-width': '80px',
},
+ '@media (prefers-color-scheme: dark)': {
+ [[
+ 'rect[data-sre-highlighter-added]:has(+ .mjx-selected)',
+ 'rect[data-sre-highlighter-bbox].mjx-selected',
+ ].join(', ')]: {
+ stroke: '#C8C8C8',
+ },
+ },
};
/**
diff --git a/ts/output/svg/Wrappers/maction.ts b/ts/output/svg/Wrappers/maction.ts
index 0a551cce2..298c40f11 100644
--- a/ts/output/svg/Wrappers/maction.ts
+++ b/ts/output/svg/Wrappers/maction.ts
@@ -187,6 +187,24 @@ export const SvgMaction = (function (): SvgMactionClass {
'background-color': '#F8F8F8',
color: 'black',
},
+ 'g[data-mml-node="maction"][data-collapsible][data-toggle="1"]': {
+ fill: '#55F',
+ },
+
+ '@media (prefers-color-scheme: dark) /* svg maction */': {
+ 'mjx-tool > mjx-tip': {
+ 'background-color': '#303030',
+ color: '#E0E0E0',
+ 'box-shadow': '2px 2px 5px #000',
+ },
+ 'mjx-status': {
+ 'background-color': '#303030',
+ color: '#E0E0E0',
+ },
+ 'g[data-mml-node="maction"][data-collapsible][data-toggle="1"]': {
+ fill: '#88F',
+ },
+ },
};
/**
diff --git a/ts/ui/dialog/DraggableDialog.ts b/ts/ui/dialog/DraggableDialog.ts
index a7aa784c7..5c8a3cbd7 100644
--- a/ts/ui/dialog/DraggableDialog.ts
+++ b/ts/ui/dialog/DraggableDialog.ts
@@ -173,6 +173,7 @@ export class DraggableDialog {
* The default styles for all dialogs
*/
public static styles: StyleJson = {
+
//
// For when dialog element is not available
//
@@ -296,7 +297,7 @@ export class DraggableDialog {
'line-height': 0,
padding: '8px 0 6px',
},
- '.mjs-dialog-button > mjx-dialog-icon:hover': {
+ '.mjx-dialog-button > mjx-dialog-icon:hover': {
'background-color': '#CCC !important',
},
@@ -404,6 +405,50 @@ export class DraggableDialog {
bottom: '-3px',
cursor: 'nwse-resize',
},
+
+ '@media (prefers-color-scheme: dark)': {
+ '.mjx-dialog': {
+ 'background-color': '#303030',
+ 'box-shadow': '0px 10px 20px #000',
+ border: '3px outset #7C7C7C',
+ },
+ 'mjx-dialog': {
+ color: '#E0E0E0',
+ },
+ 'mjx-dialog > div': {
+ border: '2px inset #7C7C7C',
+ 'background-color': '#222025',
+ },
+ 'a[href]': {
+ color: '#86A7F5',
+ },
+ 'a[href]:visited': {
+ color: '#DD98E2',
+ },
+ 'mjx-dialog kbd': {
+ color: '#F8F8F8',
+ 'background-color': '#545454',
+ border: 'solid 1.5px #7A7C7E',
+ 'border-bottom-color': '#707070',
+ 'box-shadow': 'inset -.5px -1px 0 #818589',
+ },
+ '.mjx-dialog-button': {
+ border: '2px solid #686868',
+ color: '#A4A4A4',
+ },
+ '.mjx-dialog-button:hover': {
+ color: '#CBCBCB !important',
+ border: '2px solid #888888 !important',
+ },
+ '.mjx-dialog-button > mjx-dialog-icon': {
+ 'background-color': '#646464',
+ },
+ '.mjx-dialog-button > mjx-dialog-icon:hover': {
+ 'background-color': '#888888 !important',
+ },
+ },
+
+
};
protected static helpMessage: string = `
diff --git a/ts/ui/menu/Menu.ts b/ts/ui/menu/Menu.ts
index 2f91ca01f..68eb5c8f2 100644
--- a/ts/ui/menu/Menu.ts
+++ b/ts/ui/menu/Menu.ts
@@ -621,9 +621,9 @@ export class Menu {
),
this.a11yVar('highlight', (value) => this.setHighlight(value)),
this.a11yVar('backgroundColor'),
- this.a11yVar('backgroundOpacity'),
+ this.a11yVar('backgroundOpacity', (value) => this.setAlpha('bg', value)),
this.a11yVar('foregroundColor'),
- this.a11yVar('foregroundOpacity'),
+ this.a11yVar('foregroundOpacity', (value) => this.setAlpha('fg', value)),
this.a11yVar('subtitles'),
this.a11yVar('viewBraille'),
this.a11yVar('voicing'),
@@ -922,8 +922,7 @@ export class Menu {
AnnotationMenu.copyAnnotations(cache),
'',
]);
- CssStyles.addInfoStyles(this.document.document as any);
- CssStyles.addMenuStyles(this.document.document as any);
+ CssStyles.addMenuStyles(this.document.document as Document);
}
/**
@@ -1075,6 +1074,8 @@ export class Menu {
if (renderer !== this.defaultSettings.renderer) {
this.document.whenReady(() => this.setRenderer(renderer, false));
}
+ this.setAlpha('fg', this.settings.foregroundOpacity ?? '100');
+ this.setAlpha('bg', this.settings.backgroundOpacity ?? '20');
});
}
@@ -1327,6 +1328,17 @@ export class Menu {
}
}
+ /**
+ * @param {string} type The type of alpha to set (fg or bg)
+ * @param {string} value The value to set it to
+ */
+ protected setAlpha(type: string, value: string) {
+ if (MathJax._?.a11y?.explorer) {
+ const alpha = parseInt(value) / 100;
+ MathJax._.a11y.explorer.Region.LiveRegion.setAlpha(type, alpha, this.document.document);
+ }
+ }
+
/**
* Request the scaling value from the user and save it in the settings
*/
diff --git a/ts/util/StyleJson.ts b/ts/util/StyleJson.ts
index a27de84fa..5755ab3eb 100644
--- a/ts/util/StyleJson.ts
+++ b/ts/util/StyleJson.ts
@@ -32,7 +32,7 @@ export type StyleJsonData = {
* A list of selectors and their data (basically a stylesheet)
*/
export type StyleJson = {
- [selector: string]: StyleJsonData;
+ [selector: string]: StyleJsonData | StyleJson;
};
/******************************************************************************/
@@ -97,32 +97,32 @@ export class StyleJsonSheet {
}
/**
+ * @param {StyleJson} styles The style list to convert
* @returns {string[]} An array of rule strings for the style list
*/
- public getStyleRules(): string[] {
- const selectors = Object.keys(this.styles);
+ public getStyleRules(styles: StyleJson = this.styles): string[] {
+ const selectors = Object.keys(styles);
const defs: string[] = new Array(selectors.length);
let i = 0;
for (const selector of selectors) {
- defs[i++] =
- selector +
- ' {\n' +
- this.getStyleDefString(this.styles[selector]) +
- '\n}';
+ const data = styles[selector];
+ defs[i++] = `${selector} {\n${this.getStyleDefString(data)}\n}`;
}
return defs;
}
/**
- * @param {StyleJsonData} styles The style data to be stringified
- * @returns {string} The CSS string for the given data
+ * @param {StyleJsonData | StyleJson} styles The style data to be stringified
+ * @returns {string} The CSS string for the given data
*/
- public getStyleDefString(styles: StyleJsonData): string {
+ public getStyleDefString(styles: StyleJsonData | StyleJson): string {
const properties = Object.keys(styles);
const values: string[] = new Array(properties.length);
let i = 0;
for (const property of properties) {
- values[i++] = ' ' + property + ': ' + styles[property] + ';';
+ values[i++] = styles[property] instanceof Object
+ ? ' ' + this.getStyleRules({[property]: styles[property]} as StyleJson).join('\n ')
+ : ' ' + property + ': ' + styles[property] + ';';
}
return values.join('\n');
}
From 2df99776d5f48480f852d806c6684658df71993f Mon Sep 17 00:00:00 2001
From: "Davide P. Cervone"
Date: Sun, 19 Oct 2025 16:33:33 -0400
Subject: [PATCH 09/17] Fixes for prettier
---
ts/a11y/explorer.ts | 2 +-
ts/a11y/explorer/Region.ts | 16 ++++++++++++----
ts/ui/dialog/DraggableDialog.ts | 3 ---
ts/ui/menu/Menu.ts | 14 +++++++++++---
ts/util/StyleJson.ts | 10 +++++++---
5 files changed, 31 insertions(+), 14 deletions(-)
diff --git a/ts/a11y/explorer.ts b/ts/a11y/explorer.ts
index 575533aed..a24aedb37 100644
--- a/ts/a11y/explorer.ts
+++ b/ts/a11y/explorer.ts
@@ -432,7 +432,7 @@ export function ExplorerMathDocumentMixin<
stroke: '#AAAAAA',
fill: '#404040',
},
- }
+ },
};
/**
diff --git a/ts/a11y/explorer/Region.ts b/ts/a11y/explorer/Region.ts
index 374288c89..9feaabb23 100644
--- a/ts/a11y/explorer/Region.ts
+++ b/ts/a11y/explorer/Region.ts
@@ -122,10 +122,13 @@ export abstract class AbstractRegion implements Region {
*/
public AddStyles() {
const id = this.CLASS.sheetId;
- if (!this.CLASS.style || this.document.adaptor.head().querySelector('#' + id)) {
+ if (
+ !this.CLASS.style ||
+ this.document.adaptor.head().querySelector('#' + id)
+ ) {
return;
}
- const node = this.document.adaptor.node('style', {id});
+ const node = this.document.adaptor.node('style', { id });
node.innerHTML = this.CLASS.style.cssText;
this.document.adaptor.head().appendChild(node);
}
@@ -416,11 +419,16 @@ export class LiveRegion extends StringRegion {
* @param {Document} document The document whose CSS styles are to be adjusted
*/
public static setAlpha(type: string, alpha: number, document: Document) {
- const style = document.head.querySelector('#' + this.sheetId) as HTMLStyleElement;
+ const style = document.head.querySelector(
+ '#' + this.sheetId
+ ) as HTMLStyleElement;
if (style) {
const name = `--mjx-${type}-alpha`;
(style.sheet.cssRules[0] as any).style.setProperty(name, alpha);
- (style.sheet.cssRules[1] as any).cssRules[0].style.setProperty(name, alpha ** 0.7071);
+ (style.sheet.cssRules[1] as any).cssRules[0].style.setProperty(
+ name,
+ alpha ** 0.7071
+ );
}
}
}
diff --git a/ts/ui/dialog/DraggableDialog.ts b/ts/ui/dialog/DraggableDialog.ts
index 5c8a3cbd7..5e981a225 100644
--- a/ts/ui/dialog/DraggableDialog.ts
+++ b/ts/ui/dialog/DraggableDialog.ts
@@ -173,7 +173,6 @@ export class DraggableDialog {
* The default styles for all dialogs
*/
public static styles: StyleJson = {
-
//
// For when dialog element is not available
//
@@ -447,8 +446,6 @@ export class DraggableDialog {
'background-color': '#888888 !important',
},
},
-
-
};
protected static helpMessage: string = `
diff --git a/ts/ui/menu/Menu.ts b/ts/ui/menu/Menu.ts
index 68eb5c8f2..90abfdbdf 100644
--- a/ts/ui/menu/Menu.ts
+++ b/ts/ui/menu/Menu.ts
@@ -621,9 +621,13 @@ export class Menu {
),
this.a11yVar('highlight', (value) => this.setHighlight(value)),
this.a11yVar('backgroundColor'),
- this.a11yVar('backgroundOpacity', (value) => this.setAlpha('bg', value)),
+ this.a11yVar('backgroundOpacity', (value) =>
+ this.setAlpha('bg', value)
+ ),
this.a11yVar('foregroundColor'),
- this.a11yVar('foregroundOpacity', (value) => this.setAlpha('fg', value)),
+ this.a11yVar('foregroundOpacity', (value) =>
+ this.setAlpha('fg', value)
+ ),
this.a11yVar('subtitles'),
this.a11yVar('viewBraille'),
this.a11yVar('voicing'),
@@ -1335,7 +1339,11 @@ export class Menu {
protected setAlpha(type: string, value: string) {
if (MathJax._?.a11y?.explorer) {
const alpha = parseInt(value) / 100;
- MathJax._.a11y.explorer.Region.LiveRegion.setAlpha(type, alpha, this.document.document);
+ MathJax._.a11y.explorer.Region.LiveRegion.setAlpha(
+ type,
+ alpha,
+ this.document.document
+ );
}
}
diff --git a/ts/util/StyleJson.ts b/ts/util/StyleJson.ts
index 5755ab3eb..61611cd48 100644
--- a/ts/util/StyleJson.ts
+++ b/ts/util/StyleJson.ts
@@ -120,9 +120,13 @@ export class StyleJsonSheet {
const values: string[] = new Array(properties.length);
let i = 0;
for (const property of properties) {
- values[i++] = styles[property] instanceof Object
- ? ' ' + this.getStyleRules({[property]: styles[property]} as StyleJson).join('\n ')
- : ' ' + property + ': ' + styles[property] + ';';
+ values[i++] =
+ styles[property] instanceof Object
+ ? ' ' +
+ this.getStyleRules({
+ [property]: styles[property],
+ } as StyleJson).join('\n ')
+ : ' ' + property + ': ' + styles[property] + ';';
}
return values.join('\n');
}
From ff85e8dc61178b826a478d495c6ef80ba001e5f1 Mon Sep 17 00:00:00 2001
From: "Davide P. Cervone"
Date: Tue, 21 Oct 2025 08:55:52 -0400
Subject: [PATCH 10/17] Update ts/ui/dialog/DraggableDialog.ts
Co-authored-by: Volker Sorge
---
ts/ui/dialog/DraggableDialog.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/ts/ui/dialog/DraggableDialog.ts b/ts/ui/dialog/DraggableDialog.ts
index a7aa784c7..9800adcfd 100644
--- a/ts/ui/dialog/DraggableDialog.ts
+++ b/ts/ui/dialog/DraggableDialog.ts
@@ -420,7 +420,7 @@ export class DraggableDialog {
right-hand edge of the dialog, while up and down move the bottom edge
of the dialog. Hold the Win or Command key and
press any of the arrow keys to move the dialog box in the given direction.
- Holding a shift key as well will make the larger changes
+ Holding a shift key as well will make larger changes
in the size or position.
Use Tab to move among the text and buttons and links
From 9b0d56701af4a30ab18f6519779179e6e65e2bb3 Mon Sep 17 00:00:00 2001
From: "Davide P. Cervone"
Date: Tue, 21 Oct 2025 08:56:29 -0400
Subject: [PATCH 11/17] Update ts/ui/dialog/DraggableDialog.ts
Co-authored-by: Volker Sorge
---
ts/ui/dialog/DraggableDialog.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/ts/ui/dialog/DraggableDialog.ts b/ts/ui/dialog/DraggableDialog.ts
index 9800adcfd..663608093 100644
--- a/ts/ui/dialog/DraggableDialog.ts
+++ b/ts/ui/dialog/DraggableDialog.ts
@@ -919,7 +919,7 @@ export class DraggableDialog {
}
/**
- * Select the contents of the dialog for copying
+ * Select the content of the dialog for copying
*/
protected selectAll() {
const selection = document.getSelection();
From 86225a3d83b93ee06dcebb81ed51c9a46f524ce0 Mon Sep 17 00:00:00 2001
From: "Davide P. Cervone"
Date: Wed, 22 Oct 2025 19:27:59 -0400
Subject: [PATCH 12/17] Fix lint issues
---
ts/ui/menu/AnnotationMenu.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/ts/ui/menu/AnnotationMenu.ts b/ts/ui/menu/AnnotationMenu.ts
index a1001d941..d7a85e0bd 100644
--- a/ts/ui/menu/AnnotationMenu.ts
+++ b/ts/ui/menu/AnnotationMenu.ts
@@ -42,7 +42,7 @@ type AnnotationTypes = { [type: string]: string[] };
/**
* Returns a method to create the dynamic submenu for showing annotations.
*
- * @param {SelectableInfo} box The info box in which to post annotation info.
+ * @param {() => void} box The info box in which to post annotation info.
* @param {AnnotationTypes} types The legitimate annotation types.
* @param {[string, string][]} cache We cache annotations of a math item, so we
* only have to compute them once for the two annotation menus.
@@ -67,7 +67,7 @@ export function showAnnotations(
* @returns {DynamicSubmenu} Method generating
* the copy annotations submenu.
*/
-export function copyAnnotations(cache: [string, string][]) {
+export function copyAnnotations(cache: [string, string][]): DynamicSubmenu {
return (menu: MJContextMenu, sub: Submenu, callback: SubmenuCallback) => {
const annotations = cache.slice();
cache.length = 0;
From 436965d2cbb0b8c9ec7845239cb972dda36d27e1 Mon Sep 17 00:00:00 2001
From: "Davide P. Cervone"
Date: Fri, 24 Oct 2025 11:06:40 -0400
Subject: [PATCH 13/17] Add move/size toggle keys for use with plain arrows,
adjust the help message, and close the dialog if the user moves away from the
page in the browser history while one is open.
---
ts/ui/dialog/DraggableDialog.ts | 90 ++++++++++++++++++++++++++-------
1 file changed, 73 insertions(+), 17 deletions(-)
diff --git a/ts/ui/dialog/DraggableDialog.ts b/ts/ui/dialog/DraggableDialog.ts
index 663608093..6575376e9 100644
--- a/ts/ui/dialog/DraggableDialog.ts
+++ b/ts/ui/dialog/DraggableDialog.ts
@@ -149,6 +149,8 @@ export class DraggableDialog {
protected static keyActions: Map = new Map([
['Escape', (dialog, event) => dialog.escKey(event)],
['a', (dialog, event) => dialog.aKey(event)],
+ ['m', (dialog, event) => dialog.mKey(event)],
+ ['s', (dialog, event) => dialog.sKey(event)],
['ArrowRight', (dialog, event) => dialog.arrowKey(event, 'right')],
['ArrowLeft', (dialog, event) => dialog.arrowKey(event, 'left')],
['ArrowUp', (dialog, event) => dialog.arrowKey(event, 'up')],
@@ -414,21 +416,39 @@ export class DraggableDialog {
changes the two sides that meet at that corner. Dragging elsewhere on
the dialog frame will move the dialog without changing its size.
- For keyboard users, to change the dialog size, hold the
- alt or option key and press any of the arrow
- keys to enlarge or shrink the dialog box. Left and right move the
- right-hand edge of the dialog, while up and down move the bottom edge
- of the dialog. Hold the Win or Command key and
- press any of the arrow keys to move the dialog box in the given direction.
- Holding a shift key as well will make larger changes
- in the size or position.
-
- Use Tab to move among the text and buttons and links
+
For keyboard users, there are two ways to adjust the position
+ and size of the dialog box. The first is to hold the
+ Alt or Option key and press any of the arrow
+ keys to move the dialog box in the given direction. Hold the
+ Win or Command key and press any of the
+ arrow keys to enlarge or shrink the dialog box. Left and right
+ move the right-hand edge of the dialog, while up and down move the
+ bottom edge of the dialog.
+
+
+ For some users, holding two keys down at once may be difficult,
+ so the second way is to press the m to start "move"
+ mode, then use the arrow keys to move the dialog box in the given
+ direction. Press m again to stop moving the dialog.
+ Similarly, press s to start and stop "sizing" mode,
+ where the arrows will change the size of the dialog box.
+
+ Holding a shift key along with the arrow key will
+ make larger changes in the size or position, for either method
+ described above.
+
+ Use Tab to move among the text, buttons, and links
within the dialog. The Enter or Space key
activates the focused item. The Escape key closes the
- dialog, as does clicking outside the dialog box.
+ dialog, as does clicking outside the dialog box, or clicking the
+ "\u00D7" icon in the upper right-hand corner of the dialog.
`;
+ /**
+ * When moving/sizing by keyboard, this gives which is being adjusted.
+ */
+ protected mode: string = '';
+
/**
* @param {DialogArgs} args The data describing the dialog
*/
@@ -621,6 +641,10 @@ export class DraggableDialog {
this.background.append(this.dialog);
document.body.append(this.background);
}
+ context.window.addEventListener(
+ 'visibilitychange',
+ this.Visibility.bind(this)
+ );
//
// Adjust the min width and height, if the initial dialog is small
//
@@ -849,6 +873,16 @@ export class DraggableDialog {
}
}
+ /**
+ * Close the dialog if the pages becomes hidden (e.g., moving
+ * foreward or backward in the window's history).
+ */
+ protected Visibility() {
+ if (context.document.hidden) {
+ this.closeDialog();
+ }
+ }
+
/**
* Handle a keydown event
*
@@ -883,6 +917,26 @@ export class DraggableDialog {
}
}
+ /**
+ * Start or stop moving the dialog via arrow keys
+ *
+ * @param {KeyboardEvent} event The key event to handle
+ */
+ protected mKey(event: KeyboardEvent) {
+ this.mode = this.mode === 'move' ? '' : 'move';
+ this.stop(event);
+ }
+
+ /**
+ * Start or stop sizing the dialog via arrow keys
+ *
+ * @param {KeyboardEvent} event The key event to handle
+ */
+ protected sKey(event: KeyboardEvent) {
+ this.mode = this.mode === 'size' ? '' : 'size';
+ this.stop(event);
+ }
+
/**
* Handle the arrow keys
*
@@ -893,12 +947,12 @@ export class DraggableDialog {
if (event.ctrlKey || this.dragging) return;
this.action = direction;
this.getWH();
- if (event.altKey) {
- this.dragAction(event.shiftKey ? 'bigsize' : 'keysize');
- this.stop(event);
- } else if (event.metaKey) {
+ if (event.altKey || this.mode === 'move') {
this.dragAction(event.shiftKey ? 'bigmove' : 'keymove');
this.stop(event);
+ } else if (event.metaKey || this.mode === 'size') {
+ this.dragAction(event.shiftKey ? 'bigsize' : 'keysize');
+ this.stop(event);
}
this.action = '';
}
@@ -992,7 +1046,7 @@ export class DraggableDialog {
*
* @param {Event} event The event that caused the closure
*/
- protected closeDialog(event: Event) {
+ protected closeDialog(event?: Event) {
if (isDialog) {
this.dialog.close();
this.dialog.remove();
@@ -1000,7 +1054,9 @@ export class DraggableDialog {
this.background.remove();
}
this.node?.focus();
- this.stop(event);
+ if (event) {
+ this.stop(event);
+ }
}
/**
From 113588724355c1a19382e24147b51d1afc26658e Mon Sep 17 00:00:00 2001
From: "Davide P. Cervone"
Date: Fri, 24 Oct 2025 11:11:47 -0400
Subject: [PATCH 14/17] Update explorer help to make it clear how to turn off
the explorer (for sighted users who don't want to use it).
---
ts/a11y/explorer/KeyExplorer.ts | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/ts/a11y/explorer/KeyExplorer.ts b/ts/a11y/explorer/KeyExplorer.ts
index c57426460..e333db800 100644
--- a/ts/a11y/explorer/KeyExplorer.ts
+++ b/ts/a11y/explorer/KeyExplorer.ts
@@ -214,7 +214,9 @@ export class SpeechExplorer
particular, the Explorer submenu allows you to specify how the
mathematics should be identified in the page (e.g., by saying "math"
when the expression is spoken), and whether or not to include a
- message about the letter "h" bringing up this dialog box.
+ message about the letter "h" bringing up this dialog box. Turning off
+ speech and Braille will disable the expression explorer, its
+ highlighting, and its help icon.
The contextual menu also provides options for viewing or copying a
MathML version of the expression or its original source format,
From b459898745b2eba7608b820dd9b5fec0ad4c0db1 Mon Sep 17 00:00:00 2001
From: "Davide P. Cervone"
Date: Sun, 26 Oct 2025 15:53:40 -0400
Subject: [PATCH 15/17] Update spacing for StyleJason for @media rules, and add
a test for it
---
testsuite/tests/util/StyleJson.test.ts | 16 +++++++++++++++
ts/util/StyleJson.ts | 28 +++++++++++++++++---------
2 files changed, 35 insertions(+), 9 deletions(-)
diff --git a/testsuite/tests/util/StyleJson.test.ts b/testsuite/tests/util/StyleJson.test.ts
index 84a013515..08d76dd2c 100644
--- a/testsuite/tests/util/StyleJson.test.ts
+++ b/testsuite/tests/util/StyleJson.test.ts
@@ -36,4 +36,20 @@ describe('StyleJsonSheet object', () => {
expect(styles.cssText).toBe('');
});
+ test('Compound style', () => {
+ expect(new StyleJsonSheet({
+ '@media (prefers-color-scheme: dark)': {
+ 'mjx-container': {
+ 'color': '#E0E0E0',
+ },
+ }
+ }).cssText).toBe([
+ '@media (prefers-color-scheme: dark) {',
+ ' mjx-container {',
+ ' color: #E0E0E0;',
+ ' }',
+ '}'
+ ].join('\n'));
+ });
+
});
diff --git a/ts/util/StyleJson.ts b/ts/util/StyleJson.ts
index 61611cd48..f08263576 100644
--- a/ts/util/StyleJson.ts
+++ b/ts/util/StyleJson.ts
@@ -100,13 +100,17 @@ export class StyleJsonSheet {
* @param {StyleJson} styles The style list to convert
* @returns {string[]} An array of rule strings for the style list
*/
- public getStyleRules(styles: StyleJson = this.styles): string[] {
+ public getStyleRules(
+ styles: StyleJson = this.styles,
+ spaces: string = ''
+ ): string[] {
const selectors = Object.keys(styles);
const defs: string[] = new Array(selectors.length);
let i = 0;
for (const selector of selectors) {
const data = styles[selector];
- defs[i++] = `${selector} {\n${this.getStyleDefString(data)}\n}`;
+ defs[i++] =
+ `${spaces}${selector} {\n${this.getStyleDefString(data, spaces)}\n${spaces}}`;
}
return defs;
}
@@ -115,19 +119,25 @@ export class StyleJsonSheet {
* @param {StyleJsonData | StyleJson} styles The style data to be stringified
* @returns {string} The CSS string for the given data
*/
- public getStyleDefString(styles: StyleJsonData | StyleJson): string {
+ public getStyleDefString(
+ styles: StyleJsonData | StyleJson,
+ spaces: string
+ ): string {
const properties = Object.keys(styles);
const values: string[] = new Array(properties.length);
let i = 0;
for (const property of properties) {
values[i++] =
styles[property] instanceof Object
- ? ' ' +
- this.getStyleRules({
- [property]: styles[property],
- } as StyleJson).join('\n ')
- : ' ' + property + ': ' + styles[property] + ';';
+ ? spaces +
+ this.getStyleRules(
+ {
+ [property]: styles[property],
+ } as StyleJson,
+ spaces + ' '
+ ).join('\n' + spaces)
+ : ' ' + spaces + property + ': ' + styles[property] + ';';
}
- return values.join('\n');
+ return values.join('\n' + spaces);
}
}
From e3d534f3a5a9cd2b2f0c23fc38bca94c8e6844ae Mon Sep 17 00:00:00 2001
From: "Davide P. Cervone"
Date: Sun, 26 Oct 2025 15:57:44 -0400
Subject: [PATCH 16/17] Add missing JSDoc parameters
---
ts/util/StyleJson.ts | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/ts/util/StyleJson.ts b/ts/util/StyleJson.ts
index f08263576..c37e9e365 100644
--- a/ts/util/StyleJson.ts
+++ b/ts/util/StyleJson.ts
@@ -98,7 +98,8 @@ export class StyleJsonSheet {
/**
* @param {StyleJson} styles The style list to convert
- * @returns {string[]} An array of rule strings for the style list
+ * @param {string} spaces The spaces to put at the beginning of each line
+ * @returns {string[]} An array of rule strings for the style list
*/
public getStyleRules(
styles: StyleJson = this.styles,
@@ -117,7 +118,8 @@ export class StyleJsonSheet {
/**
* @param {StyleJsonData | StyleJson} styles The style data to be stringified
- * @returns {string} The CSS string for the given data
+ * @param {string} spaces The spaces to put at the beginning of each line
+ * @returns {string} The CSS string for the given data
*/
public getStyleDefString(
styles: StyleJsonData | StyleJson,
From b6c54a12cc02132d7e3e9245d9f5c67591327197 Mon Sep 17 00:00:00 2001
From: "Davide P. Cervone"
Date: Thu, 13 Nov 2025 07:33:13 -0500
Subject: [PATCH 17/17] Update ts/a11y/explorer/Region.ts
Co-authored-by: Volker Sorge
---
ts/a11y/explorer/Region.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/ts/a11y/explorer/Region.ts b/ts/a11y/explorer/Region.ts
index 9feaabb23..1303fb7f2 100644
--- a/ts/a11y/explorer/Region.ts
+++ b/ts/a11y/explorer/Region.ts
@@ -415,7 +415,7 @@ export class LiveRegion extends StringRegion {
/**
* @param {string} type The type of alpha to set (fg or bg)
- * @param {number} alpha The alpha value to use for the background
+ * @param {number} alpha The alpha value to use
* @param {Document} document The document whose CSS styles are to be adjusted
*/
public static setAlpha(type: string, alpha: number, document: Document) {