Menu.ts 40 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177
  1. /*************************************************************
  2. *
  3. * Copyright (c) 2019-2022 The MathJax Consortium
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. /**
  18. * @fileoverview Implements a subclass of ContextMenu specific to MathJax
  19. *
  20. * @author dpvc@mathjax.org (Davide Cervone)
  21. */
  22. import {mathjax} from '../../mathjax.js';
  23. import {MathItem, STATE} from '../../core/MathItem.js';
  24. import {OutputJax} from '../../core/OutputJax.js';
  25. import {MathJax as MJX} from '../../components/global.js';
  26. import {MathJaxObject as StartupObject} from '../../components/startup.js';
  27. import {MathJaxObject as LoaderObject} from '../../components/loader.js';
  28. import {OptionList, userOptions, defaultOptions, expandable} from '../../util/Options.js';
  29. import {MJContextMenu} from './MJContextMenu.js';
  30. import {MmlVisitor} from './MmlVisitor.js';
  31. import {SelectableInfo} from './SelectableInfo.js';
  32. import {MenuMathDocument} from './MenuHandler.js';
  33. import {Info} from 'mj-context-menu/js/info.js';
  34. import {Parser} from 'mj-context-menu/js/parse.js';
  35. import {Rule} from 'mj-context-menu/js/item_rule.js';
  36. import {CssStyles} from 'mj-context-menu/js/css_util.js';
  37. import {Submenu} from 'mj-context-menu/js/item_submenu.js';
  38. import Sre from '../../a11y/sre.js';
  39. /*==========================================================================*/
  40. /**
  41. * Declare the MathJax global and the navigator object (to check platform for MacOS)
  42. */
  43. declare namespace window {
  44. const navigator: {platform: string};
  45. }
  46. /**
  47. * The global MathJax object
  48. */
  49. const MathJax = MJX as StartupObject & LoaderObject;
  50. /**
  51. * True when platform is a Mac (so we can enable CMD menu item for zoom trigger)
  52. */
  53. const isMac = (typeof window !== 'undefined' &&
  54. window.navigator && window.navigator.platform.substr(0, 3) === 'Mac');
  55. /*==========================================================================*/
  56. /**
  57. * The various values that are stored in the menu
  58. */
  59. export interface MenuSettings {
  60. texHints: boolean;
  61. semantics: boolean;
  62. zoom: string;
  63. zscale: string;
  64. renderer: string;
  65. alt: boolean;
  66. cmd: boolean;
  67. ctrl: boolean;
  68. shift: boolean;
  69. scale: string;
  70. autocollapse: boolean;
  71. collapsible: boolean;
  72. inTabOrder: boolean;
  73. assistiveMml: boolean;
  74. // A11y settings
  75. backgroundColor: string;
  76. backgroundOpacity: string;
  77. braille: boolean;
  78. explorer: boolean;
  79. foregroundColor: string;
  80. foregroundOpacity: string;
  81. highlight: string;
  82. locale: string;
  83. infoPrefix: boolean;
  84. infoRole: boolean;
  85. infoType: boolean;
  86. magnification: string;
  87. magnify: string;
  88. speech: boolean;
  89. speechRules: string;
  90. subtitles: boolean;
  91. treeColoring: boolean;
  92. viewBraille: boolean;
  93. }
  94. export type HTMLMATHITEM = MathItem<HTMLElement, Text, Document>;
  95. /*==========================================================================*/
  96. /**
  97. * The Menu object that handles the MathJax contextual menu and its actions
  98. */
  99. export class Menu {
  100. /**
  101. * The key for the localStorage for the menu settings
  102. */
  103. public static MENU_STORAGE = 'MathJax-Menu-Settings';
  104. /**
  105. * The options for the menu, including the default settings, the various output jax
  106. * and the list of annotation types and their encodings
  107. */
  108. public static OPTIONS: OptionList = {
  109. settings: {
  110. texHints: true,
  111. semantics: false,
  112. zoom: 'NoZoom',
  113. zscale: '200%',
  114. renderer: 'CHTML',
  115. alt: false,
  116. cmd: false,
  117. ctrl: false,
  118. shift: false,
  119. scale: 1,
  120. autocollapse: false,
  121. collapsible: false,
  122. inTabOrder: true,
  123. assistiveMml: true,
  124. explorer: false
  125. },
  126. jax: {
  127. CHTML: null,
  128. SVG: null
  129. },
  130. annotationTypes: expandable({
  131. TeX: ['TeX', 'LaTeX', 'application/x-tex'],
  132. StarMath: ['StarMath 5.0'],
  133. Maple: ['Maple'],
  134. ContentMathML: ['MathML-Content', 'application/mathml-content+xml'],
  135. OpenMath: ['OpenMath']
  136. })
  137. };
  138. /**
  139. * The number of startup modules that are currently being loaded
  140. */
  141. protected static loading: number = 0;
  142. /**
  143. * Promises for the loading components
  144. */
  145. protected static loadingPromises: Map<string, Promise<void>> = new Map();
  146. /**
  147. * A promise that is resolved when all components are loaded
  148. */
  149. protected static _loadingPromise: Promise<void> = null;
  150. /**
  151. * Function used to resolve the _loadingPromise
  152. */
  153. protected static _loadingOK: Function = null;
  154. /**
  155. * Function used to reject the _loadingPromise
  156. */
  157. protected static _loadingFailed: Function = null;
  158. /**
  159. * The options for this menu
  160. */
  161. public options: OptionList;
  162. /**
  163. * The current settings for this menu (the variables attached to the menu's pool)
  164. */
  165. public settings: MenuSettings = null;
  166. /**
  167. * The original settings (with page options factored in) for use with the reset command
  168. */
  169. public defaultSettings: MenuSettings = null;
  170. /**
  171. * The contextual menu object that is managed by this Menu
  172. */
  173. public menu: MJContextMenu = null;
  174. /**
  175. * A MathML serializer that has options corresponding to the menu settings
  176. */
  177. public MmlVisitor = new MmlVisitor<HTMLElement, Text, Document>();
  178. /**
  179. * The MathDocument in which we are working
  180. */
  181. protected document: MenuMathDocument;
  182. /**
  183. * Instances of the various output jax that we can switch to
  184. */
  185. protected jax: {[name: string]: OutputJax<HTMLElement, Text, Document>} = {
  186. CHTML: null,
  187. SVG: null
  188. };
  189. /**
  190. * The minium initial state for pending rerender requests (so final rerender gets the right start)
  191. */
  192. protected rerenderStart: number = STATE.LAST;
  193. /**
  194. * @returns {boolean} true when the menu is loading some component
  195. */
  196. public get isLoading(): boolean {
  197. return Menu.loading > 0;
  198. }
  199. /**
  200. * @returns {Promise} A promise that is resolved when all pending loads are complete
  201. */
  202. public get loadingPromise(): Promise<void> {
  203. if (!this.isLoading) {
  204. return Promise.resolve();
  205. }
  206. if (!Menu._loadingPromise) {
  207. Menu._loadingPromise = new Promise<void>((ok, failed) => {
  208. Menu._loadingOK = ok;
  209. Menu._loadingFailed = failed;
  210. });
  211. }
  212. return Menu._loadingPromise;
  213. }
  214. /*======================================================================*/
  215. /**
  216. * The "About MathJax" info box
  217. */
  218. protected about = new Info(
  219. '<b style="font-size:120%;">MathJax</b> v' + mathjax.version,
  220. () => {
  221. const lines = [] as string[];
  222. lines.push('Input Jax: ' + this.document.inputJax.map(jax => jax.name).join(', '));
  223. lines.push('Output Jax: ' + this.document.outputJax.name);
  224. lines.push('Document Type: ' + this.document.kind);
  225. return lines.join('<br/>');
  226. },
  227. '<a href="https://www.mathjax.org">www.mathjax.org</a>'
  228. );
  229. /**
  230. * The "MathJax Help" info box
  231. */
  232. protected help = new Info(
  233. '<b>MathJax Help</b>',
  234. () => {
  235. return [
  236. '<p><b>MathJax</b> is a JavaScript library that allows page',
  237. ' authors to include mathematics within their web pages.',
  238. ' As a reader, you don\'t need to do anything to make that happen.</p>',
  239. '<p><b>Browsers</b>: MathJax works with all modern browsers including',
  240. ' Edge, Firefox, Chrome, Safari, Opera, and most mobile browsers.</p>',
  241. '<p><b>Math Menu</b>: MathJax adds a contextual menu to equations.',
  242. ' Right-click or CTRL-click on any mathematics to access the menu.</p>',
  243. '<div style="margin-left: 1em;">',
  244. '<p><b>Show Math As:</b> These options allow you to view the formula\'s',
  245. ' source markup (as MathML or in its original format).</p>',
  246. '<p><b>Copy to Clipboard:</b> These options copy the formula\'s source markup,',
  247. ' as MathML or in its original format, to the clipboard',
  248. ' (in browsers that support that).</p>',
  249. '<p><b>Math Settings:</b> These give you control over features of MathJax,',
  250. ' such the size of the mathematics, and the mechanism used',
  251. ' to display equations.</p>',
  252. '<p><b>Accessibility</b>: MathJax can work with screen',
  253. ' readers to make mathematics accessible to the visually impaired.',
  254. ' Turn on the explorer to enable generation of speech strings',
  255. ' and the ability to investigate expressions interactively.</p>',
  256. '<p><b>Language</b>: This menu lets you select the language used by MathJax',
  257. ' for its menus and warning messages. (Not yet implemented in version 3.)</p>',
  258. '</div>',
  259. '<p><b>Math Zoom</b>: If you are having difficulty reading an',
  260. ' equation, MathJax can enlarge it to help you see it better, or',
  261. ' you can scall all the math on the page to make it larger.',
  262. ' Turn these features on in the <b>Math Settings</b> menu.</p>',
  263. '<p><b>Preferences</b>: MathJax uses your browser\'s localStorage database',
  264. ' to save the preferences set via this menu locally in your browser. These',
  265. ' are not used to track you, and are not transferred or used remotely by',
  266. ' MathJax in any way.</p>'
  267. ]. join('\n');
  268. },
  269. '<a href="https://www.mathjax.org">www.mathjax.org</a>'
  270. );
  271. /**
  272. * The "Show As MathML" info box
  273. */
  274. protected mathmlCode = new SelectableInfo(
  275. 'MathJax MathML Expression',
  276. () => {
  277. if (!this.menu.mathItem) return '';
  278. const text = this.toMML(this.menu.mathItem);
  279. return '<pre>' + this.formatSource(text) + '</pre>';
  280. },
  281. ''
  282. );
  283. /**
  284. * The "Show As (original form)" info box
  285. */
  286. protected originalText = new SelectableInfo(
  287. 'MathJax Original Source',
  288. () => {
  289. if (!this.menu.mathItem) return '';
  290. const text = this.menu.mathItem.math;
  291. return '<pre style="font-size:125%; margin:0">' + this.formatSource(text) + '</pre>';
  292. },
  293. ''
  294. );
  295. /**
  296. * The "Show As Annotation" info box
  297. */
  298. protected annotationText = new SelectableInfo(
  299. 'MathJax Annotation Text',
  300. () => {
  301. if (!this.menu.mathItem) return '';
  302. const text = this.menu.annotation;
  303. return '<pre style="font-size:125%; margin:0">' + this.formatSource(text) + '</pre>';
  304. },
  305. ''
  306. );
  307. /**
  308. * The info box for zoomed expressions
  309. */
  310. protected zoomBox = new Info(
  311. 'MathJax Zoomed Expression',
  312. () => {
  313. if (!this.menu.mathItem) return '';
  314. const element = (this.menu.mathItem.typesetRoot as any).cloneNode(true) as HTMLElement;
  315. element.style.margin = '0';
  316. const scale = 1.25 * parseFloat(this.settings.zscale); // 1.25 is to reverse the default 80% font-size
  317. return '<div style="font-size: ' + scale + '%">' + element.outerHTML + '</div>';
  318. },
  319. ''
  320. );
  321. /*======================================================================*/
  322. /**
  323. * Accept options in addition to the MathDocument, and set up the menu based
  324. * on the defaults, the passed options, and the user's saved settings.
  325. *
  326. * @param {MenuMathDocument} document The MathDcument where this menu will post
  327. * @param {OptionList} options The options for the menu
  328. * @override
  329. */
  330. constructor(document: MenuMathDocument, options: OptionList = {}) {
  331. this.document = document;
  332. this.options = userOptions(defaultOptions({}, (this.constructor as typeof Menu).OPTIONS), options);
  333. this.initSettings();
  334. this.mergeUserSettings();
  335. this.initMenu();
  336. this.applySettings();
  337. }
  338. /**
  339. * Set up the settings and jax objects, and transfer the output jax name and scale to the settings
  340. */
  341. protected initSettings() {
  342. this.settings = this.options.settings;
  343. this.jax = this.options.jax;
  344. const jax = this.document.outputJax;
  345. this.jax[jax.name] = jax;
  346. this.settings.renderer = jax.name;
  347. if (MathJax._.a11y && MathJax._.a11y.explorer) {
  348. Object.assign(this.settings, this.document.options.a11y);
  349. }
  350. this.settings.scale = jax.options.scale;
  351. this.defaultSettings = Object.assign({}, this.settings);
  352. }
  353. /**
  354. * Create the menu object, attach the info boxes to it, and output any CSS needed for it
  355. */
  356. protected initMenu() {
  357. let parser = new Parser([['contextMenu', MJContextMenu.fromJson.bind(MJContextMenu)]]);
  358. this.menu = parser.parse({
  359. type: 'contextMenu',
  360. id: 'MathJax_Menu',
  361. pool: [
  362. this.variable<boolean>('texHints'),
  363. this.variable<boolean>('semantics'),
  364. this.variable<string> ('zoom'),
  365. this.variable<string> ('zscale'),
  366. this.variable<string> ('renderer', jax => this.setRenderer(jax)),
  367. this.variable<boolean>('alt'),
  368. this.variable<boolean>('cmd'),
  369. this.variable<boolean>('ctrl'),
  370. this.variable<boolean>('shift'),
  371. this.variable<string> ('scale', scale => this.setScale(scale)),
  372. this.variable<boolean>('explorer', explore => this.setExplorer(explore)),
  373. this.a11yVar<string> ('highlight'),
  374. this.a11yVar<string> ('backgroundColor'),
  375. this.a11yVar<string> ('backgroundOpacity'),
  376. this.a11yVar<string> ('foregroundColor'),
  377. this.a11yVar<string> ('foregroundOpacity'),
  378. this.a11yVar<boolean>('speech'),
  379. this.a11yVar<boolean>('subtitles'),
  380. this.a11yVar<boolean>('braille'),
  381. this.a11yVar<boolean>('viewBraille'),
  382. this.a11yVar<string>('locale', value => Sre.setupEngine({locale: value as string})),
  383. this.a11yVar<string> ('speechRules', value => {
  384. const [domain, style] = value.split('-');
  385. this.document.options.sre.domain = domain;
  386. this.document.options.sre.style = style;
  387. }),
  388. this.a11yVar<string> ('magnification'),
  389. this.a11yVar<string> ('magnify'),
  390. this.a11yVar<boolean>('treeColoring'),
  391. this.a11yVar<boolean>('infoType'),
  392. this.a11yVar<boolean>('infoRole'),
  393. this.a11yVar<boolean>('infoPrefix'),
  394. this.variable<boolean>('autocollapse'),
  395. this.variable<boolean>('collapsible', collapse => this.setCollapsible(collapse)),
  396. this.variable<boolean>('inTabOrder', tab => this.setTabOrder(tab)),
  397. this.variable<boolean>('assistiveMml', mml => this.setAssistiveMml(mml))
  398. ],
  399. items: [
  400. this.submenu('Show', 'Show Math As', [
  401. this.command('MathMLcode', 'MathML Code', () => this.mathmlCode.post()),
  402. this.command('Original', 'Original Form', () => this.originalText.post()),
  403. this.submenu('Annotation', 'Annotation')
  404. ]),
  405. this.submenu('Copy', 'Copy to Clipboard', [
  406. this.command('MathMLcode', 'MathML Code', () => this.copyMathML()),
  407. this.command('Original', 'Original Form', () => this.copyOriginal()),
  408. this.submenu('Annotation', 'Annotation')
  409. ]),
  410. this.rule(),
  411. this.submenu('Settings', 'Math Settings', [
  412. this.submenu('Renderer', 'Math Renderer', this.radioGroup('renderer', [['CHTML'], ['SVG']])),
  413. this.rule(),
  414. this.submenu('ZoomTrigger', 'Zoom Trigger', [
  415. this.command('ZoomNow', 'Zoom Once Now', () => this.zoom(null, '', this.menu.mathItem)),
  416. this.rule(),
  417. this.radioGroup('zoom', [
  418. ['Click'], ['DoubleClick', 'Double-Click'], ['NoZoom', 'No Zoom']
  419. ]),
  420. this.rule(),
  421. this.label('TriggerRequires', 'Trigger Requires:'),
  422. this.checkbox((isMac ? 'Option' : 'Alt'), (isMac ? 'Option' : 'Alt'), 'alt'),
  423. this.checkbox('Command', 'Command', 'cmd', {hidden: !isMac}),
  424. this.checkbox('Control', 'Control', 'ctrl', {hiddne: isMac}),
  425. this.checkbox('Shift', 'Shift', 'shift')
  426. ]),
  427. this.submenu('ZoomFactor', 'Zoom Factor', this.radioGroup('zscale', [
  428. ['150%'], ['175%'], ['200%'], ['250%'], ['300%'], ['400%']
  429. ])),
  430. this.rule(),
  431. this.command('Scale', 'Scale All Math...', () => this.scaleAllMath()),
  432. this.rule(),
  433. this.checkbox('texHints', 'Add TeX hints to MathML', 'texHints'),
  434. this.checkbox('semantics', 'Add original as annotation', 'semantics'),
  435. this.rule(),
  436. this.command('Reset', 'Reset to defaults', () => this.resetDefaults())
  437. ]),
  438. this.submenu('Accessibility', 'Accessibility', [
  439. this.checkbox('Activate', 'Activate', 'explorer'),
  440. this.submenu('Speech', 'Speech', [
  441. this.checkbox('Speech', 'Speech Output', 'speech'),
  442. this.checkbox('Subtitles', 'Speech Subtitles', 'subtitles'),
  443. this.checkbox('Braille', 'Braille Output', 'braille'),
  444. this.checkbox('View Braille', 'Braille Subtitles', 'viewBraille'),
  445. this.rule(),
  446. this.submenu('A11yLanguage', 'Language'),
  447. this.rule(),
  448. this.submenu('Mathspeak', 'Mathspeak Rules', this.radioGroup('speechRules', [
  449. ['mathspeak-default', 'Verbose'],
  450. ['mathspeak-brief', 'Brief'],
  451. ['mathspeak-sbrief', 'Superbrief']
  452. ])),
  453. this.submenu('Clearspeak', 'Clearspeak Rules', this.radioGroup('speechRules', [
  454. ['clearspeak-default', 'Auto']
  455. ])),
  456. this.submenu('ChromeVox', 'ChromeVox Rules', this.radioGroup('speechRules', [
  457. ['chromevox-default', 'Standard'],
  458. ['chromevox-alternative', 'Alternative']
  459. ]))
  460. ]),
  461. this.submenu('Highlight', 'Highlight', [
  462. this.submenu('Background', 'Background', this.radioGroup('backgroundColor', [
  463. ['Blue'], ['Red'], ['Green'], ['Yellow'], ['Cyan'], ['Magenta'], ['White'], ['Black']
  464. ])),
  465. {'type': 'slider',
  466. 'variable': 'backgroundOpacity',
  467. 'content': ' '
  468. },
  469. this.submenu('Foreground', 'Foreground', this.radioGroup('foregroundColor', [
  470. ['Black'], ['White'], ['Magenta'], ['Cyan'], ['Yellow'], ['Green'], ['Red'], ['Blue']
  471. ])),
  472. {'type': 'slider',
  473. 'variable': 'foregroundOpacity',
  474. 'content': ' '
  475. },
  476. this.rule(),
  477. this.radioGroup('highlight', [
  478. ['None'], ['Hover'], ['Flame']
  479. ]),
  480. this.rule(),
  481. this.checkbox('TreeColoring', 'Tree Coloring', 'treeColoring')
  482. ]),
  483. this.submenu('Magnification', 'Magnification', [
  484. this.radioGroup('magnification', [
  485. ['None'], ['Keyboard'], ['Mouse']
  486. ]),
  487. this.rule(),
  488. this.radioGroup('magnify', [
  489. ['200%'], ['300%'], ['400%'], ['500%']
  490. ])
  491. ]),
  492. this.submenu('Semantic Info', 'Semantic Info', [
  493. this.checkbox('Type', 'Type', 'infoType'),
  494. this.checkbox('Role', 'Role', 'infoRole'),
  495. this.checkbox('Prefix', 'Prefix', 'infoPrefix')
  496. ], true),
  497. this.rule(),
  498. this.checkbox('Collapsible', 'Collapsible Math', 'collapsible'),
  499. this.checkbox('AutoCollapse', 'Auto Collapse', 'autocollapse', {disabled: true}),
  500. this.rule(),
  501. this.checkbox('InTabOrder', 'Include in Tab Order', 'inTabOrder'),
  502. this.checkbox('AssistiveMml', 'Include Hidden MathML', 'assistiveMml')
  503. ]),
  504. this.submenu('Language', 'Language'),
  505. this.rule(),
  506. this.command('About', 'About MathJax', () => this.about.post()),
  507. this.command('Help', 'MathJax Help', () => this.help.post())
  508. ]
  509. }) as MJContextMenu;
  510. const menu = this.menu;
  511. this.about.attachMenu(menu);
  512. this.help.attachMenu(menu);
  513. this.originalText.attachMenu(menu);
  514. this.annotationText.attachMenu(menu);
  515. this.mathmlCode.attachMenu(menu);
  516. this.zoomBox.attachMenu(menu);
  517. this.checkLoadableItems();
  518. this.enableExplorerItems(this.settings.explorer);
  519. menu.showAnnotation = this.annotationText;
  520. menu.copyAnnotation = this.copyAnnotation.bind(this);
  521. menu.annotationTypes = this.options.annotationTypes;
  522. CssStyles.addInfoStyles(this.document.document as any);
  523. CssStyles.addMenuStyles(this.document.document as any);
  524. }
  525. /**
  526. * Check whether the startup and loader modules are available, and
  527. * if not, disable the a11y modules (since we can't load them
  528. * or know if they are available).
  529. * Otherwise, check if any need to be loaded
  530. */
  531. protected checkLoadableItems() {
  532. if (MathJax && MathJax._ && MathJax.loader && MathJax.startup) {
  533. if (this.settings.collapsible && (!MathJax._.a11y || !MathJax._.a11y.complexity)) {
  534. this.loadA11y('complexity');
  535. }
  536. if (this.settings.explorer && (!MathJax._.a11y || !MathJax._.a11y.explorer)) {
  537. this.loadA11y('explorer');
  538. }
  539. if (this.settings.assistiveMml && (!MathJax._.a11y || !MathJax._.a11y['assistive-mml'])) {
  540. this.loadA11y('assistive-mml');
  541. }
  542. } else {
  543. const menu = this.menu;
  544. for (const name of Object.keys(this.jax)) {
  545. if (!this.jax[name]) {
  546. menu.findID('Settings', 'Renderer', name).disable();
  547. }
  548. }
  549. menu.findID('Accessibility', 'Activate').disable();
  550. menu.findID('Accessibility', 'AutoCollapse').disable();
  551. menu.findID('Accessibility', 'Collapsible').disable();
  552. }
  553. }
  554. /**
  555. * Enable/disable the Explorer submenu items
  556. *
  557. * @param {boolean} enable True to enable, false to disable
  558. */
  559. protected enableExplorerItems(enable: boolean) {
  560. const menu = (this.menu.findID('Accessibility', 'Activate') as Submenu).menu;
  561. for (const item of menu.items.slice(1)) {
  562. if (item instanceof Rule) break;
  563. enable ? item.enable() : item.disable();
  564. }
  565. }
  566. /*======================================================================*/
  567. /**
  568. * Look up the saved settings from localStorage and merge them into the menu settings
  569. */
  570. protected mergeUserSettings() {
  571. try {
  572. const settings = localStorage.getItem(Menu.MENU_STORAGE);
  573. if (!settings) return;
  574. Object.assign(this.settings, JSON.parse(settings));
  575. this.setA11y(this.settings);
  576. } catch (err) {
  577. console.log('MathJax localStorage error: ' + err.message);
  578. }
  579. }
  580. /**
  581. * Save any non-default menu settings in localStorage
  582. */
  583. protected saveUserSettings() {
  584. const settings = {} as {[key: string]: any};
  585. for (const name of Object.keys(this.settings) as (keyof MenuSettings)[]) {
  586. if (this.settings[name] !== this.defaultSettings[name]) {
  587. settings[name] = this.settings[name];
  588. }
  589. }
  590. try {
  591. if (Object.keys(settings).length) {
  592. localStorage.setItem(Menu.MENU_STORAGE, JSON.stringify(settings));
  593. } else {
  594. localStorage.removeItem(Menu.MENU_STORAGE);
  595. }
  596. } catch (err) {
  597. console.log('MathJax localStorage error: ' + err.message);
  598. }
  599. }
  600. /**
  601. * Merge menu settings into the a11y document options.
  602. * @param {[key: string]: any} options The options.
  603. */
  604. protected setA11y(options: {[key: string]: any}) {
  605. if (MathJax._.a11y && MathJax._.a11y.explorer) {
  606. MathJax._.a11y.explorer_ts.setA11yOptions(this.document, options);
  607. }
  608. }
  609. /**
  610. * Get the the value of an a11y option
  611. * @param {string} option The name of the ptions to get
  612. * @return {any} The value of the option
  613. */
  614. protected getA11y(option: string): any {
  615. if (MathJax._.a11y && MathJax._.a11y.explorer) {
  616. if (this.document.options.a11y[option] !== undefined) {
  617. return this.document.options.a11y[option];
  618. }
  619. return this.document.options.sre[option];
  620. }
  621. }
  622. /*======================================================================*/
  623. /**
  624. * Do what is needed to apply the initial user settings
  625. */
  626. protected applySettings() {
  627. this.setTabOrder(this.settings.inTabOrder);
  628. this.document.options.enableAssistiveMml = this.settings.assistiveMml;
  629. this.document.outputJax.options.scale = parseFloat(this.settings.scale);
  630. if (this.settings.renderer !== this.defaultSettings.renderer) {
  631. this.setRenderer(this.settings.renderer);
  632. }
  633. }
  634. /**
  635. * @param {string} scale The new scaling value
  636. */
  637. protected setScale(scale: string) {
  638. this.document.outputJax.options.scale = parseFloat(scale);
  639. this.document.rerender();
  640. }
  641. /**
  642. * If the jax is already on record, just use it, otherwise load the new one
  643. *
  644. * @param {string} jax The name of the jax to switch to
  645. */
  646. protected setRenderer(jax: string) {
  647. if (this.jax[jax]) {
  648. this.setOutputJax(jax);
  649. } else {
  650. const name = jax.toLowerCase();
  651. this.loadComponent('output/' + name, () => {
  652. const startup = MathJax.startup;
  653. if (name in startup.constructors) {
  654. startup.useOutput(name, true);
  655. startup.output = startup.getOutputJax();
  656. this.jax[jax] = startup.output;
  657. this.setOutputJax(jax);
  658. }
  659. });
  660. }
  661. }
  662. /**
  663. * Set up the new jax and link it to the document, then rerender the math
  664. *
  665. * @param {string} jax The name of the jax to switch to
  666. */
  667. protected setOutputJax(jax: string) {
  668. this.jax[jax].setAdaptor(this.document.adaptor);
  669. this.document.outputJax = this.jax[jax];
  670. this.rerender();
  671. }
  672. /**
  673. * @param {boolean} tab True for including math in the tab order, false for not
  674. */
  675. protected setTabOrder(tab: boolean) {
  676. this.menu.store.inTaborder(tab);
  677. }
  678. /**
  679. * @param {boolean} mml True to output hidden Mathml, false to not
  680. */
  681. protected setAssistiveMml(mml: boolean) {
  682. this.document.options.enableAssistiveMml = mml;
  683. if (!mml || (MathJax._.a11y && MathJax._.a11y['assistive-mml'])) {
  684. this.rerender();
  685. } else {
  686. this.loadA11y('assistive-mml');
  687. }
  688. }
  689. /**
  690. * @param {boolean} explore True to enable the explorer, false to not
  691. */
  692. protected setExplorer(explore: boolean) {
  693. this.enableExplorerItems(explore);
  694. this.document.options.enableExplorer = explore;
  695. if (!explore || (MathJax._.a11y && MathJax._.a11y.explorer)) {
  696. this.rerender(this.settings.collapsible ? STATE.RERENDER : STATE.COMPILED);
  697. } else {
  698. this.loadA11y('explorer');
  699. }
  700. }
  701. /**
  702. * @param {boolean} collapse True to enable collapsible math, false to not
  703. */
  704. protected setCollapsible(collapse: boolean) {
  705. this.document.options.enableComplexity = collapse;
  706. if (!collapse || (MathJax._.a11y && MathJax._.a11y.complexity)) {
  707. this.rerender(STATE.COMPILED);
  708. } else {
  709. this.loadA11y('complexity');
  710. }
  711. }
  712. /**
  713. * Request the scaling value from the user and save it in the settings
  714. */
  715. protected scaleAllMath() {
  716. const scale = (parseFloat(this.settings.scale) * 100).toFixed(1).replace(/.0$/, '');
  717. const percent = prompt('Scale all mathematics (compared to surrounding text) by', scale + '%');
  718. if (percent) {
  719. if (percent.match(/^\s*\d+(\.\d*)?\s*%?\s*$/)) {
  720. const scale = parseFloat(percent) / 100;
  721. if (scale) {
  722. this.menu.pool.lookup('scale').setValue(String(scale));
  723. } else {
  724. alert('The scale should not be zero');
  725. }
  726. } else {
  727. alert('The scale should be a percentage (e.g., 120%)');
  728. }
  729. }
  730. }
  731. /**
  732. * Reset all menu settings to the (page) defaults
  733. */
  734. protected resetDefaults() {
  735. Menu.loading++; // pretend we're loading, to suppress rerendering for each variable change
  736. const pool = this.menu.pool;
  737. const settings = this.defaultSettings;
  738. for (const name of Object.keys(this.settings) as (keyof MenuSettings)[]) {
  739. const variable = pool.lookup(name);
  740. if (variable) {
  741. variable.setValue(settings[name] as (string | boolean));
  742. const item = (variable as any).items[0];
  743. if (item) {
  744. item.executeCallbacks_();
  745. }
  746. } else {
  747. (this.settings as any)[name] = settings[name];
  748. }
  749. }
  750. Menu.loading--;
  751. this.rerender(STATE.COMPILED);
  752. }
  753. /*======================================================================*/
  754. /**
  755. * Check if a component is loading, and restart if it is
  756. *
  757. * @param {string} name The name of the component to check if it is loading
  758. */
  759. public checkComponent(name: string) {
  760. const promise = Menu.loadingPromises.get(name);
  761. if (promise) {
  762. mathjax.retryAfter(promise);
  763. }
  764. }
  765. /**
  766. * Attempt to load a component and perform a callback when done
  767. */
  768. protected loadComponent(name: string, callback: () => void) {
  769. if (Menu.loadingPromises.has(name)) return;
  770. const loader = MathJax.loader;
  771. if (!loader) return;
  772. Menu.loading++;
  773. const promise = loader.load(name).then(() => {
  774. Menu.loading--;
  775. Menu.loadingPromises.delete(name);
  776. callback();
  777. if (Menu.loading === 0 && Menu._loadingPromise) {
  778. Menu._loadingPromise = null;
  779. Menu._loadingOK();
  780. }
  781. }).catch((err) => {
  782. if (Menu._loadingPromise) {
  783. Menu._loadingPromise = null;
  784. Menu._loadingFailed(err);
  785. } else {
  786. console.log(err);
  787. }
  788. });
  789. Menu.loadingPromises.set(name, promise);
  790. }
  791. /**
  792. * Attempt to load an a11y component
  793. *
  794. * @param {string} component The name of the a11y component to load
  795. */
  796. public loadA11y(component: string) {
  797. const noEnrich = !STATE.ENRICHED;
  798. this.loadComponent('a11y/' + component, () => {
  799. const startup = MathJax.startup;
  800. //
  801. // Unregister the handler and get a new one (since the component
  802. // will have added a handler extension), then register the new one
  803. //
  804. mathjax.handlers.unregister(startup.handler);
  805. startup.handler = startup.getHandler();
  806. mathjax.handlers.register(startup.handler);
  807. //
  808. // Save the old document and get a new one, attaching the
  809. // original menu (this), and transfering any math items
  810. // from the original document, then rerender with the
  811. // updated document (from the new handler)
  812. //
  813. const document = this.document;
  814. this.document = startup.document = startup.getDocument();
  815. this.document.menu = this;
  816. this.document.outputJax.reset();
  817. this.transferMathList(document);
  818. this.document.processed = document.processed;
  819. if (!Menu._loadingPromise) {
  820. this.document.outputJax.reset();
  821. this.rerender(component === 'complexity' || noEnrich ? STATE.COMPILED : STATE.TYPESET);
  822. }
  823. });
  824. }
  825. /**
  826. * @param {MenuMathDocument} document The original document whose list is to be transferred
  827. */
  828. protected transferMathList(document: MenuMathDocument) {
  829. const MathItem = this.document.options.MathItem; // This has been updated by the new handler
  830. for (const item of document.math) {
  831. const math = new MathItem(); // Make a new MathItem
  832. Object.assign(math, item); // Copy the old data to the new math item
  833. this.document.math.push(math); // Add it to the current document
  834. }
  835. }
  836. /**
  837. * @param {string} text The text to be displayed in an Info box
  838. * @returns {string} The text with HTML specials being escaped
  839. */
  840. protected formatSource(text: string): string {
  841. return text.trim().replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
  842. }
  843. /**
  844. * @param {HTMLMATHITEM} math The MathItem to serialize as MathML
  845. * @returns {string} The serialized version of the internal MathML
  846. */
  847. protected toMML(math: HTMLMATHITEM): string {
  848. return this.MmlVisitor.visitTree(math.root, math, {
  849. texHints: this.settings.texHints,
  850. semantics: (this.settings.semantics && math.inputJax.name !== 'MathML')
  851. });
  852. }
  853. /*======================================================================*/
  854. /**
  855. * @param {MouseEvent|null} event The event triggering the zoom (or null for from a menu pick)
  856. * @param {string} type The type of event occurring (click, dblclick)
  857. * @param {HTMLMATHITEM} math The MathItem triggering the event
  858. */
  859. protected zoom(event: MouseEvent, type: string, math: HTMLMATHITEM) {
  860. if (!event || this.isZoomEvent(event, type)) {
  861. this.menu.mathItem = math;
  862. if (event) {
  863. //
  864. // The zoomBox.post() below assumes the menu is open,
  865. // so if this zoom() call is from an event (not the menu),
  866. // make sure the menu is open before posting the zoom box
  867. //
  868. this.menu.post(event);
  869. }
  870. this.zoomBox.post();
  871. }
  872. }
  873. /**
  874. * @param {MouseEvent} Event The event triggering the zoom action
  875. * @param {string} zoom The type of event (click, dblclick) that occurred
  876. * @returns {boolean} True if the event is the right type and has the needed modifiers
  877. */
  878. protected isZoomEvent(event: MouseEvent, zoom: string): boolean {
  879. return (this.settings.zoom === zoom &&
  880. (!this.settings.alt || event.altKey) &&
  881. (!this.settings.ctrl || event.ctrlKey) &&
  882. (!this.settings.cmd || event.metaKey) &&
  883. (!this.settings.shift || event.shiftKey));
  884. }
  885. /**
  886. * Rerender the output if we aren't in the middle of loading a new component
  887. * (in which case, we will rerender in the callback performed after it is loaded)
  888. *
  889. * @param {number=} start The state at which to start rerendering
  890. */
  891. protected rerender(start: number = STATE.TYPESET) {
  892. this.rerenderStart = Math.min(start, this.rerenderStart);
  893. if (!Menu.loading) {
  894. if (this.rerenderStart <= STATE.COMPILED) {
  895. this.document.reset({inputJax: []});
  896. }
  897. this.document.rerender(this.rerenderStart);
  898. this.rerenderStart = STATE.LAST;
  899. }
  900. }
  901. /**
  902. * Copy the serialzied MathML to the clipboard
  903. */
  904. protected copyMathML() {
  905. this.copyToClipboard(this.toMML(this.menu.mathItem));
  906. }
  907. /**
  908. * Copy the original form to the clipboard
  909. */
  910. protected copyOriginal() {
  911. this.copyToClipboard(this.menu.mathItem.math.trim());
  912. }
  913. /**
  914. * Copy the original annotation text to the clipboard
  915. */
  916. public copyAnnotation() {
  917. this.copyToClipboard(this.menu.annotation.trim());
  918. }
  919. /**
  920. * @param {string} text The text to be copied ot the clopboard
  921. */
  922. protected copyToClipboard(text: string) {
  923. const input = document.createElement('textarea');
  924. input.value = text;
  925. input.setAttribute('readonly', '');
  926. input.style.cssText = 'height: 1px; width: 1px; padding: 1px; position: absolute; left: -10px';
  927. document.body.appendChild(input);
  928. input.select();
  929. try {
  930. document.execCommand('copy');
  931. } catch (error) {
  932. alert('Can\'t copy to clipboard: ' + error.message);
  933. }
  934. document.body.removeChild(input);
  935. }
  936. /*======================================================================*/
  937. /**
  938. * @param {HTMLMATHITEM} math The math to attach the context menu and zoom triggers to
  939. */
  940. public addMenu(math: HTMLMATHITEM) {
  941. const element = math.typesetRoot;
  942. element.addEventListener('contextmenu', () => this.menu.mathItem = math, true);
  943. element.addEventListener('keydown', () => this.menu.mathItem = math, true);
  944. element.addEventListener('click', event => this.zoom(event, 'Click', math), true);
  945. element.addEventListener('dblclick', event => this.zoom(event, 'DoubleClick', math), true);
  946. this.menu.store.insert(element);
  947. }
  948. /**
  949. * Clear the information about stored context menus
  950. */
  951. public clear() {
  952. this.menu.store.clear();
  953. }
  954. /*======================================================================*/
  955. /**
  956. * Create JSON for a variable controlling a menu setting
  957. *
  958. * @param {keyof MenuSettings} name The setting for which to make a variable
  959. * @param {(T) => void} action Optional function to perform after setting the value
  960. * @returns {Object} The JSON for the variable
  961. *
  962. * @tempate T The type of variable being defined
  963. */
  964. public variable<T extends (string | boolean)>(name: keyof MenuSettings, action?: (value: T) => void): Object {
  965. return {
  966. name: name,
  967. getter: () => this.settings[name],
  968. setter: (value: T) => {
  969. (this.settings as any)[name] = value;
  970. action && action(value);
  971. this.saveUserSettings();
  972. }
  973. };
  974. }
  975. /**
  976. * Create JSON for an a11y specific variable.
  977. *
  978. * @param {keyof MenuSettings} name The setting for which to make a variable
  979. * @returns {Object} The JSON for the variable
  980. *
  981. * @tempate T The type of variable being defined
  982. */
  983. public a11yVar<T extends (string | boolean)>(name: keyof MenuSettings, action?: (value: T) => void): Object {
  984. return {
  985. name: name,
  986. getter: () => this.getA11y(name),
  987. setter: (value: T) => {
  988. (this.settings as any)[name] = value;
  989. let options: {[key: string]: any} = {};
  990. options[name] = value;
  991. this.setA11y(options);
  992. action && action(value);
  993. this.saveUserSettings();
  994. }
  995. };
  996. }
  997. /**
  998. * Create JSON for a submenu item
  999. *
  1000. * @param {string} id The id for the item
  1001. * @param {string} content The content for the item
  1002. * @param {any[]} entries The JSON for the entries
  1003. * @param {boolean=} disabled True if this item is diabled initially
  1004. * @returns {Object} The JSON for the submenu item
  1005. */
  1006. public submenu(id: string, content: string, entries: any[] = [], disabled: boolean = false): Object {
  1007. let items = [] as Array<Object>;
  1008. for (const entry of entries) {
  1009. if (Array.isArray(entry)) {
  1010. items = items.concat(entry);
  1011. } else {
  1012. items.push(entry);
  1013. }
  1014. }
  1015. return {type: 'submenu', id, content, menu: {items}, disabled: (items.length === 0) || disabled};
  1016. }
  1017. /**
  1018. * Create JSON for a command item
  1019. *
  1020. * @param {string} id The id for the item
  1021. * @param {string} content The content for the item
  1022. * @param {() => void} action The action function for the command
  1023. * @param {Object} other Other values to include in the generated JSON object
  1024. * @returns {Object} The JSON for the command item
  1025. */
  1026. public command(id: string, content: string, action: () => void, other: Object = {}): Object {
  1027. return Object.assign({type: 'command', id, content, action}, other);
  1028. }
  1029. /**
  1030. * Create JSON for a checkbox item
  1031. *
  1032. * @param {string} id The id for the item
  1033. * @param {string} content The content for the item
  1034. * @param {string} variable The (pool) variable to attach to this checkbox
  1035. * @param {Object} other Other values to include in the generated JSON object
  1036. * @returns {Object} The JSON for the checkbox item
  1037. */
  1038. public checkbox(id: string, content: string, variable: string, other: Object = {}): Object {
  1039. return Object.assign({type: 'checkbox', id, content, variable}, other);
  1040. }
  1041. /**
  1042. * Create JSON for a group of connected radio buttons
  1043. *
  1044. * @param {string} variable The (pool) variable to attach to each radio button
  1045. * @param {string[][]} radios An array of [string] or [string, string], giving the id and content
  1046. * for each radio button (if only one string is given it is used for both)
  1047. * @returns {Object[]} An array of JSON objects for radion buttons
  1048. */
  1049. public radioGroup(variable: string, radios: string[][]): Object[] {
  1050. return radios.map(def => this.radio(def[0], def[1] || def[0], variable));
  1051. }
  1052. /**
  1053. * Create JSON for a radio button item
  1054. *
  1055. * @param {string} id The id for the item
  1056. * @param {string} content The content for the item
  1057. * @param {string} variable The (pool) variable to attach to this radio button
  1058. * @param {Object} other Other values to include in the generated JSON object
  1059. * @returns {Object} The JSON for the radio button item
  1060. */
  1061. public radio(id: string, content: string, variable: string, other: Object = {}): Object {
  1062. return Object.assign({type: 'radio', id, content, variable}, other);
  1063. }
  1064. /**
  1065. * Create JSON for a label item
  1066. *
  1067. * @param {string} id The id for the item
  1068. * @param {string} content The content for the item
  1069. * @returns {Object} The JSON for the label item
  1070. */
  1071. public label(id: string, content: string): Object {
  1072. return {type: 'label', id, content};
  1073. }
  1074. /**
  1075. * Create JSON for a menu rule
  1076. *
  1077. * @returns {Object} The JSON for the rule item
  1078. */
  1079. public rule(): Object {
  1080. return {type: 'rule'};
  1081. }
  1082. /*======================================================================*/
  1083. }