MJContextMenu.ts 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  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 {MathItem} from '../../core/MathItem.js';
  23. import {MmlNode} from '../../core/MmlTree/MmlNode.js';
  24. import {SelectableInfo} from './SelectableInfo.js';
  25. import {ContextMenu} from 'mj-context-menu/js/context_menu.js';
  26. import {SubMenu} from 'mj-context-menu/js/sub_menu.js';
  27. import {Submenu} from 'mj-context-menu/js/item_submenu.js';
  28. import {Menu} from 'mj-context-menu/js/menu.js';
  29. import {Item} from 'mj-context-menu/js/item.js';
  30. /*==========================================================================*/
  31. /**
  32. * The subclass of ContextMenu that handles the needs of the MathJax
  33. * contextual menu (in particular, tying it to a MathItem).
  34. */
  35. export class MJContextMenu extends ContextMenu {
  36. /**
  37. * Static map to hold methods for re-computing dynamic submenus.
  38. * @type {Map<string, (menu: MJContextMenu, sub: Submenu)}
  39. */
  40. public static DynamicSubmenus: Map<string,
  41. (menu: MJContextMenu, sub: Submenu) =>
  42. SubMenu> = new Map();
  43. /**
  44. * The MathItem that has posted the menu
  45. */
  46. public mathItem: MathItem<HTMLElement, Text, Document> = null;
  47. /**
  48. * The annotation selected in the Annotation submenu (neede for the info box to be able to show it)
  49. */
  50. public annotation: string = '';
  51. /**
  52. * The info box for showing annotations (created by the Menu object that contains this MJContextMenu)
  53. */
  54. public showAnnotation: SelectableInfo;
  55. /**
  56. * The function to copy the selected annotation (set by the containing Menu item)
  57. */
  58. public copyAnnotation: () => void;
  59. /**
  60. * The annotation types to look for in a MathItem
  61. */
  62. public annotationTypes: {[type: string]: string[]} = {};
  63. /*======================================================================*/
  64. /**
  65. * Before posting the menu, set the name for the ShowAs and CopyToClipboard menus,
  66. * enable/disable the semantics check item, and get the annotations for the MathItem
  67. *
  68. * @override
  69. */
  70. public post(x?: any, y?: number) {
  71. if (this.mathItem) {
  72. if (y !== undefined) {
  73. // FIXME: handle error output jax
  74. const input = this.mathItem.inputJax.name;
  75. const original = this.findID('Show', 'Original');
  76. original.content = (input === 'MathML' ? 'Original MathML' : input + ' Commands');
  77. const clipboard = this.findID('Copy', 'Original');
  78. clipboard.content = original.content;
  79. const semantics = this.findID('Settings', 'semantics');
  80. input === 'MathML' ? semantics.disable() : semantics.enable();
  81. this.getAnnotationMenu();
  82. this.dynamicSubmenus();
  83. }
  84. super.post(x, y);
  85. }
  86. }
  87. /**
  88. * Clear the stored MathItem when the menu is removed
  89. *
  90. * @override
  91. */
  92. public unpost() {
  93. super.unpost();
  94. this.mathItem = null;
  95. }
  96. /*======================================================================*/
  97. /**
  98. * Find an item in the menu (recursively descending into submenus, if needed)
  99. *
  100. * @param {string[]} names The menu IDs to look for
  101. * @returns {Item} The menu item (or null if not found)
  102. */
  103. public findID(...names: string[]) {
  104. let menu = this as Menu;
  105. let item = null as Item;
  106. for (const name of names) {
  107. if (menu) {
  108. item = menu.find(name);
  109. menu = (item instanceof Submenu ? item.submenu : null);
  110. } else {
  111. item = null;
  112. }
  113. }
  114. return item;
  115. }
  116. /*======================================================================*/
  117. /**
  118. * Look up the annotations in the MathItem and set the ShowAs and CopyToClipboard menus
  119. */
  120. protected getAnnotationMenu() {
  121. const annotations = this.getAnnotations(this.getSemanticNode());
  122. this.createAnnotationMenu('Show', annotations, () => this.showAnnotation.post());
  123. this.createAnnotationMenu('Copy', annotations, () => this.copyAnnotation());
  124. }
  125. /**
  126. * Find the top-most semantics element that encloses the contents of the expression (if any)
  127. *
  128. * @returns {MmlNode | null} The semantics node that was found (or null)
  129. */
  130. protected getSemanticNode(): MmlNode | null {
  131. let node: MmlNode = this.mathItem.root;
  132. while (node && !node.isKind('semantics')) {
  133. if (node.isToken || node.childNodes.length !== 1) return null;
  134. node = node.childNodes[0] as MmlNode;
  135. }
  136. return node;
  137. }
  138. /**
  139. * @param {MmlNode} node The semantics node whose annotations are to be obtained
  140. * @returns {[string, string][]} Array of [type, text] where the type is the annotation type
  141. * and text is the content of the annotation of that type
  142. */
  143. protected getAnnotations(node: MmlNode): [string, string][] {
  144. const annotations = [] as [string, string][];
  145. if (!node) return annotations;
  146. for (const child of node.childNodes as MmlNode[]) {
  147. if (child.isKind('annotation')) {
  148. const match = this.annotationMatch(child);
  149. if (match) {
  150. const value = child.childNodes.reduce((text, chars) => text + chars.toString(), '');
  151. annotations.push([match, value]);
  152. }
  153. }
  154. }
  155. return annotations;
  156. }
  157. /**
  158. * @param {MmlNode} child The annotation node to check if its encoding is one of the displayable ones
  159. * @returns {string | null} The annotation type if it does, or null if it doesn't
  160. */
  161. protected annotationMatch(child: MmlNode): string | null {
  162. const encoding = child.attributes.get('encoding') as string;
  163. for (const type of Object.keys(this.annotationTypes)) {
  164. if (this.annotationTypes[type].indexOf(encoding) >= 0) {
  165. return type;
  166. }
  167. }
  168. return null;
  169. }
  170. /**
  171. * Create a submenu from the available annotations and attach it to the proper menu item
  172. *
  173. * @param {string} id The id of the menu to attach to (Show or Copy)
  174. * @param {[string, string][]} annotations The annotations to use for the submenu
  175. * @param {() => void} action The action to perform when the annotation is selected
  176. */
  177. protected createAnnotationMenu(id: string, annotations: [string, string][], action: () => void) {
  178. const menu = this.findID(id, 'Annotation') as Submenu;
  179. menu.submenu = this.factory.get('subMenu')(this.factory, {
  180. items: annotations.map(([type, value]) => {
  181. return {
  182. type: 'command',
  183. id: type,
  184. content: type,
  185. action: () => {
  186. this.annotation = value;
  187. action();
  188. }
  189. };
  190. }),
  191. id: 'annotations'
  192. }, menu);
  193. if (annotations.length) {
  194. menu.enable();
  195. } else {
  196. menu.disable();
  197. }
  198. }
  199. /*======================================================================*/
  200. /**
  201. * Renews the dynamic submenus.
  202. */
  203. public dynamicSubmenus() {
  204. for (const [id, method] of MJContextMenu.DynamicSubmenus) {
  205. const menu = this.find(id) as Submenu;
  206. if (!menu) continue;
  207. const sub = method(this, menu);
  208. menu.submenu = sub;
  209. if (sub.items.length) {
  210. menu.enable();
  211. } else {
  212. menu.disable();
  213. }
  214. }
  215. }
  216. }