Region.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519
  1. /*************************************************************
  2. *
  3. * Copyright (c) 2009-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 Regions for A11y purposes.
  19. *
  20. * @author v.sorge@mathjax.org (Volker Sorge)
  21. */
  22. import {MathDocument} from '../../core/MathDocument.js';
  23. import {CssStyles} from '../../util/StyleList.js';
  24. import Sre from '../sre.js';
  25. export type A11yDocument = MathDocument<HTMLElement, Text, Document>;
  26. export interface Region<T> {
  27. /**
  28. * Adds a style sheet for the live region to the document.
  29. */
  30. AddStyles(): void;
  31. /**
  32. * Adds the region element to the document.
  33. */
  34. AddElement(): void;
  35. /**
  36. * Shows the live region in the document.
  37. * @param {HTMLElement} node
  38. * @param {Sre.highlighter} highlighter
  39. */
  40. Show(node: HTMLElement, highlighter: Sre.highlighter): void;
  41. /**
  42. * Takes the element out of the document flow.
  43. */
  44. Hide(): void;
  45. /**
  46. * Clears the content of the region.
  47. */
  48. Clear(): void;
  49. /**
  50. * Updates the content of the region.
  51. * @template T
  52. */
  53. Update(content: T): void;
  54. }
  55. export abstract class AbstractRegion<T> implements Region<T> {
  56. /**
  57. * CSS Classname of the element.
  58. * @type {String}
  59. */
  60. protected static className: string;
  61. /**
  62. * True if the style has already been added to the document.
  63. * @type {boolean}
  64. */
  65. protected static styleAdded: boolean = false;
  66. /**
  67. * The CSS style that needs to be added for this type of region.
  68. * @type {CssStyles}
  69. */
  70. protected static style: CssStyles;
  71. /**
  72. * The outer div node.
  73. * @type {HTMLElement}
  74. */
  75. protected div: HTMLElement;
  76. /**
  77. * The inner node.
  78. * @type {HTMLElement}
  79. */
  80. protected inner: HTMLElement;
  81. /**
  82. * The actual class name to refer to static elements of a class.
  83. * @type {typeof AbstractRegion}
  84. */
  85. protected CLASS: typeof AbstractRegion;
  86. /**
  87. * @constructor
  88. * @param {A11yDocument} document The document the live region is added to.
  89. */
  90. constructor(public document: A11yDocument) {
  91. this.CLASS = this.constructor as typeof AbstractRegion;
  92. this.AddStyles();
  93. this.AddElement();
  94. }
  95. /**
  96. * @override
  97. */
  98. public AddStyles() {
  99. if (this.CLASS.styleAdded) {
  100. return;
  101. }
  102. // TODO: should that be added to document.documentStyleSheet()?
  103. let node = this.document.adaptor.node('style');
  104. node.innerHTML = this.CLASS.style.cssText;
  105. this.document.adaptor.head(this.document.adaptor.document).
  106. appendChild(node);
  107. this.CLASS.styleAdded = true;
  108. }
  109. /**
  110. * @override
  111. */
  112. public AddElement() {
  113. let element = this.document.adaptor.node('div');
  114. element.classList.add(this.CLASS.className);
  115. element.style.backgroundColor = 'white';
  116. this.div = element;
  117. this.inner = this.document.adaptor.node('div');
  118. this.div.appendChild(this.inner);
  119. this.document.adaptor.
  120. body(this.document.adaptor.document).
  121. appendChild(this.div);
  122. }
  123. /**
  124. * @override
  125. */
  126. public Show(node: HTMLElement, highlighter: Sre.highlighter) {
  127. this.position(node);
  128. this.highlight(highlighter);
  129. this.div.classList.add(this.CLASS.className + '_Show');
  130. }
  131. /**
  132. * Computes the position where to place the element wrt. to the given node.
  133. * @param {HTMLElement} node The reference node.
  134. */
  135. protected abstract position(node: HTMLElement): void;
  136. /**
  137. * Highlights the region.
  138. * @param {Sre.highlighter} highlighter The Sre highlighter.
  139. */
  140. protected abstract highlight(highlighter: Sre.highlighter): void;
  141. /**
  142. * @override
  143. */
  144. public Hide() {
  145. this.div.classList.remove(this.CLASS.className + '_Show');
  146. }
  147. /**
  148. * @override
  149. */
  150. public abstract Clear(): void;
  151. /**
  152. * @override
  153. */
  154. public abstract Update(content: T): void;
  155. /**
  156. * Auxiliary position method that stacks shown regions of the same type.
  157. * @param {HTMLElement} node The reference node.
  158. */
  159. protected stackRegions(node: HTMLElement) {
  160. // TODO: This could be made more efficient by caching regions of a class.
  161. const rect = node.getBoundingClientRect();
  162. let baseBottom = 0;
  163. let baseLeft = Number.POSITIVE_INFINITY;
  164. let regions = this.document.adaptor.document.getElementsByClassName(
  165. this.CLASS.className + '_Show');
  166. // Get all the shown regions (one is this element!) and append at bottom.
  167. for (let i = 0, region; region = regions[i]; i++) {
  168. if (region !== this.div) {
  169. baseBottom = Math.max(region.getBoundingClientRect().bottom, baseBottom);
  170. baseLeft = Math.min(region.getBoundingClientRect().left, baseLeft);
  171. }
  172. }
  173. const bot = (baseBottom ? baseBottom : rect.bottom + 10) + window.pageYOffset;
  174. const left = (baseLeft < Number.POSITIVE_INFINITY ? baseLeft : rect.left) + window.pageXOffset;
  175. this.div.style.top = bot + 'px';
  176. this.div.style.left = left + 'px';
  177. }
  178. }
  179. export class DummyRegion extends AbstractRegion<void> {
  180. /**
  181. * @override
  182. */
  183. public Clear() {}
  184. /**
  185. * @override
  186. */
  187. public Update() {}
  188. /**
  189. * @override
  190. */
  191. public Hide() {}
  192. /**
  193. * @override
  194. */
  195. public Show() {}
  196. /**
  197. * @override
  198. */
  199. public AddElement() {}
  200. /**
  201. * @override
  202. */
  203. public AddStyles() {}
  204. /**
  205. * @override
  206. */
  207. public position() {}
  208. /**
  209. * @override
  210. */
  211. public highlight(_highlighter: Sre.highlighter) {}
  212. }
  213. export class StringRegion extends AbstractRegion<string> {
  214. /**
  215. * @override
  216. */
  217. public Clear(): void {
  218. this.Update('');
  219. this.inner.style.top = '';
  220. this.inner.style.backgroundColor = '';
  221. }
  222. /**
  223. * @override
  224. */
  225. public Update(speech: string) {
  226. this.inner.textContent = '';
  227. this.inner.textContent = speech;
  228. }
  229. /**
  230. * @override
  231. */
  232. protected position(node: HTMLElement) {
  233. this.stackRegions(node);
  234. }
  235. /**
  236. * @override
  237. */
  238. protected highlight(highlighter: Sre.highlighter) {
  239. const color = highlighter.colorString();
  240. this.inner.style.backgroundColor = color.background;
  241. this.inner.style.color = color.foreground;
  242. }
  243. }
  244. export class ToolTip extends StringRegion {
  245. /**
  246. * @override
  247. */
  248. protected static className = 'MJX_ToolTip';
  249. /**
  250. * @override
  251. */
  252. protected static style: CssStyles =
  253. new CssStyles({
  254. ['.' + ToolTip.className]: {
  255. position: 'absolute', display: 'inline-block',
  256. height: '1px', width: '1px'
  257. },
  258. ['.' + ToolTip.className + '_Show']: {
  259. width: 'auto', height: 'auto', opacity: 1, 'text-align': 'center',
  260. 'border-radius': '6px', padding: '0px 0px',
  261. 'border-bottom': '1px dotted black', position: 'absolute',
  262. 'z-index': 202
  263. }
  264. });
  265. }
  266. export class LiveRegion extends StringRegion {
  267. /**
  268. * @override
  269. */
  270. protected static className = 'MJX_LiveRegion';
  271. /**
  272. * @override
  273. */
  274. protected static style: CssStyles =
  275. new CssStyles({
  276. ['.' + LiveRegion.className]: {
  277. position: 'absolute', top: '0', height: '1px', width: '1px',
  278. padding: '1px', overflow: 'hidden'
  279. },
  280. ['.' + LiveRegion.className + '_Show']: {
  281. top: '0', position: 'absolute', width: 'auto', height: 'auto',
  282. padding: '0px 0px', opacity: 1, 'z-index': '202',
  283. left: 0, right: 0, 'margin': '0 auto',
  284. 'background-color': 'rgba(0, 0, 255, 0.2)', 'box-shadow': '0px 10px 20px #888',
  285. border: '2px solid #CCCCCC'
  286. }
  287. });
  288. /**
  289. * @constructor
  290. * @param {A11yDocument} document The document the live region is added to.
  291. */
  292. constructor(public document: A11yDocument) {
  293. super(document);
  294. this.div.setAttribute('aria-live', 'assertive');
  295. }
  296. }
  297. // Region that overlays the current element.
  298. export class HoverRegion extends AbstractRegion<HTMLElement> {
  299. /**
  300. * @override
  301. */
  302. protected static className = 'MJX_HoverRegion';
  303. /**
  304. * @override
  305. */
  306. protected static style: CssStyles =
  307. new CssStyles({
  308. ['.' + HoverRegion.className]: {
  309. position: 'absolute', height: '1px', width: '1px',
  310. padding: '1px', overflow: 'hidden'
  311. },
  312. ['.' + HoverRegion.className + '_Show']: {
  313. position: 'absolute', width: 'max-content', height: 'auto',
  314. padding: '0px 0px', opacity: 1, 'z-index': '202', 'margin': '0 auto',
  315. 'background-color': 'rgba(0, 0, 255, 0.2)',
  316. 'box-shadow': '0px 10px 20px #888', border: '2px solid #CCCCCC'
  317. }
  318. });
  319. /**
  320. * @constructor
  321. * @param {A11yDocument} document The document the live region is added to.
  322. */
  323. constructor(public document: A11yDocument) {
  324. super(document);
  325. this.inner.style.lineHeight = '0';
  326. }
  327. /**
  328. * Sets the position of the region with respect to align parameter. There are
  329. * three options: top, bottom and center. Center is the default.
  330. *
  331. * @param {HTMLElement} node The node that is displayed.
  332. */
  333. protected position(node: HTMLElement) {
  334. const nodeRect = node.getBoundingClientRect();
  335. const divRect = this.div.getBoundingClientRect();
  336. const xCenter = nodeRect.left + (nodeRect.width / 2);
  337. let left = xCenter - (divRect.width / 2);
  338. left = (left < 0) ? 0 : left;
  339. left = left + window.pageXOffset;
  340. let top;
  341. switch (this.document.options.a11y.align) {
  342. case 'top':
  343. top = nodeRect.top - divRect.height - 10 ;
  344. break;
  345. case 'bottom':
  346. top = nodeRect.bottom + 10;
  347. break;
  348. case 'center':
  349. default:
  350. const yCenter = nodeRect.top + (nodeRect.height / 2);
  351. top = yCenter - (divRect.height / 2);
  352. }
  353. top = top + window.pageYOffset;
  354. top = (top < 0) ? 0 : top;
  355. this.div.style.top = top + 'px';
  356. this.div.style.left = left + 'px';
  357. }
  358. /**
  359. * @override
  360. */
  361. protected highlight(highlighter: Sre.highlighter) {
  362. // TODO Do this with styles to avoid the interaction of SVG/CHTML.
  363. if (this.inner.firstChild &&
  364. !(this.inner.firstChild as HTMLElement).hasAttribute('sre-highlight')) {
  365. return;
  366. }
  367. const color = highlighter.colorString();
  368. this.inner.style.backgroundColor = color.background;
  369. this.inner.style.color = color.foreground;
  370. }
  371. /**
  372. * @override
  373. */
  374. public Show(node: HTMLElement, highlighter: Sre.highlighter) {
  375. this.div.style.fontSize = this.document.options.a11y.magnify;
  376. this.Update(node);
  377. super.Show(node, highlighter);
  378. }
  379. /**
  380. * @override
  381. */
  382. public Clear() {
  383. this.inner.textContent = '';
  384. this.inner.style.top = '';
  385. this.inner.style.backgroundColor = '';
  386. }
  387. /**
  388. * @override
  389. */
  390. public Update(node: HTMLElement) {
  391. this.Clear();
  392. let mjx = this.cloneNode(node);
  393. this.inner.appendChild(mjx);
  394. }
  395. /**
  396. * Clones the node to put into the hover region.
  397. * @param {HTMLElement} node The original node.
  398. * @return {HTMLElement} The cloned node.
  399. */
  400. private cloneNode(node: HTMLElement): HTMLElement {
  401. let mjx = node.cloneNode(true) as HTMLElement;
  402. if (mjx.nodeName !== 'MJX-CONTAINER') {
  403. // remove element spacing (could be done in CSS)
  404. if (mjx.nodeName !== 'g') {
  405. mjx.style.marginLeft = mjx.style.marginRight = '0';
  406. }
  407. let container = node;
  408. while (container && container.nodeName !== 'MJX-CONTAINER') {
  409. container = container.parentNode as HTMLElement;
  410. }
  411. if (mjx.nodeName !== 'MJX-MATH' && mjx.nodeName !== 'svg') {
  412. const child = container.firstChild;
  413. mjx = child.cloneNode(false).appendChild(mjx).parentNode as HTMLElement;
  414. //
  415. // SVG specific
  416. //
  417. if (mjx.nodeName === 'svg') {
  418. (mjx.firstChild as HTMLElement).setAttribute('transform', 'matrix(1 0 0 -1 0 0)');
  419. const W = parseFloat(mjx.getAttribute('viewBox').split(/ /)[2]);
  420. const w = parseFloat(mjx.getAttribute('width'));
  421. const {x, y, width, height} = (node as any).getBBox();
  422. mjx.setAttribute('viewBox', [x, -(y + height), width, height].join(' '));
  423. mjx.removeAttribute('style');
  424. mjx.setAttribute('width', (w / W * width) + 'ex');
  425. mjx.setAttribute('height', (w / W * height) + 'ex');
  426. container.setAttribute('sre-highlight', 'false');
  427. }
  428. }
  429. mjx = container.cloneNode(false).appendChild(mjx).parentNode as HTMLElement;
  430. // remove displayed math margins (could be done in CSS)
  431. mjx.style.margin = '0';
  432. }
  433. return mjx;
  434. }
  435. }