mtable.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580
  1. /*************************************************************
  2. *
  3. * Copyright (c) 2017-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 the CHTMLmtable wrapper for the MmlMtable object
  19. *
  20. * @author dpvc@mathjax.org (Davide Cervone)
  21. */
  22. import {CHTMLWrapper, CHTMLConstructor} from '../Wrapper.js';
  23. import {CHTMLWrapperFactory} from '../WrapperFactory.js';
  24. import {CommonMtableMixin} from '../../common/Wrappers/mtable.js';
  25. import {CHTMLmtr} from './mtr.js';
  26. import {CHTMLmtd} from './mtd.js';
  27. import {MmlMtable} from '../../../core/MmlTree/MmlNodes/mtable.js';
  28. import {MmlNode} from '../../../core/MmlTree/MmlNode.js';
  29. import {StyleList} from '../../../util/StyleList.js';
  30. import {isPercent} from '../../../util/string.js';
  31. import {OptionList} from '../../../util/Options.js';
  32. /*****************************************************************/
  33. /**
  34. * The CHTMLmtable wrapper for the MmlMtable object
  35. *
  36. * @template N The HTMLElement node class
  37. * @template T The Text node class
  38. * @template D The Document class
  39. */
  40. export class CHTMLmtable<N, T, D> extends
  41. CommonMtableMixin<CHTMLmtd<any, any, any>, CHTMLmtr<any, any, any>, CHTMLConstructor<any, any, any>>(CHTMLWrapper) {
  42. /**
  43. * The mtable wrapper
  44. */
  45. public static kind = MmlMtable.prototype.kind;
  46. /**
  47. * @override
  48. */
  49. public static styles: StyleList = {
  50. 'mjx-mtable': {
  51. 'vertical-align': '.25em',
  52. 'text-align': 'center',
  53. 'position': 'relative',
  54. 'box-sizing': 'border-box',
  55. 'border-spacing': 0, // prevent this from being inherited from an outer table
  56. 'border-collapse': 'collapse' // similarly here
  57. },
  58. 'mjx-mstyle[size="s"] mjx-mtable': {
  59. 'vertical-align': '.354em'
  60. },
  61. 'mjx-labels': {
  62. position: 'absolute',
  63. left: 0,
  64. top: 0
  65. },
  66. 'mjx-table': {
  67. 'display': 'inline-block',
  68. 'vertical-align': '-.5ex',
  69. 'box-sizing': 'border-box'
  70. },
  71. 'mjx-table > mjx-itable': {
  72. 'vertical-align': 'middle',
  73. 'text-align': 'left',
  74. 'box-sizing': 'border-box'
  75. },
  76. 'mjx-labels > mjx-itable': {
  77. position: 'absolute',
  78. top: 0
  79. },
  80. 'mjx-mtable[justify="left"]': {
  81. 'text-align': 'left'
  82. },
  83. 'mjx-mtable[justify="right"]': {
  84. 'text-align': 'right'
  85. },
  86. 'mjx-mtable[justify="left"][side="left"]': {
  87. 'padding-right': '0 ! important'
  88. },
  89. 'mjx-mtable[justify="left"][side="right"]': {
  90. 'padding-left': '0 ! important'
  91. },
  92. 'mjx-mtable[justify="right"][side="left"]': {
  93. 'padding-right': '0 ! important'
  94. },
  95. 'mjx-mtable[justify="right"][side="right"]': {
  96. 'padding-left': '0 ! important'
  97. },
  98. 'mjx-mtable[align]': {
  99. 'vertical-align': 'baseline'
  100. },
  101. 'mjx-mtable[align="top"] > mjx-table': {
  102. 'vertical-align': 'top'
  103. },
  104. 'mjx-mtable[align="bottom"] > mjx-table': {
  105. 'vertical-align': 'bottom'
  106. },
  107. 'mjx-mtable[side="right"] mjx-labels': {
  108. 'min-width': '100%'
  109. }
  110. };
  111. /**
  112. * The column for labels
  113. */
  114. public labels: N;
  115. /**
  116. * The inner table DOM node
  117. */
  118. public itable: N;
  119. /******************************************************************/
  120. /**
  121. * @override
  122. */
  123. constructor(factory: CHTMLWrapperFactory<N, T, D>, node: MmlNode, parent: CHTMLWrapper<N, T, D> = null) {
  124. super(factory, node, parent);
  125. this.itable = this.html('mjx-itable');
  126. this.labels = this.html('mjx-itable');
  127. }
  128. /**
  129. * @override
  130. */
  131. public getAlignShift() {
  132. const data = super.getAlignShift();
  133. if (!this.isTop) {
  134. data[1] = 0;
  135. }
  136. return data;
  137. }
  138. /**
  139. * @override
  140. */
  141. public toCHTML(parent: N) {
  142. //
  143. // Create the rows inside an mjx-itable (which will be used to center the table on the math axis)
  144. //
  145. const chtml = this.standardCHTMLnode(parent);
  146. this.adaptor.append(chtml, this.html('mjx-table', {}, [this.itable]));
  147. for (const child of this.childNodes) {
  148. child.toCHTML(this.itable);
  149. }
  150. //
  151. // Pad the rows of the table, if needed
  152. // Then set the column and row attributes for alignment, spacing, and lines
  153. // Finally, add the frame, if needed
  154. //
  155. this.padRows();
  156. this.handleColumnSpacing();
  157. this.handleColumnLines();
  158. this.handleColumnWidths();
  159. this.handleRowSpacing();
  160. this.handleRowLines();
  161. this.handleRowHeights();
  162. this.handleFrame();
  163. this.handleWidth();
  164. this.handleLabels();
  165. this.handleAlign();
  166. this.handleJustify();
  167. this.shiftColor();
  168. }
  169. /**
  170. * Move background color (if any) to inner itable node so that labeled tables are
  171. * only colored on the main part of the table.
  172. */
  173. protected shiftColor() {
  174. const adaptor = this.adaptor;
  175. const color = adaptor.getStyle(this.chtml, 'backgroundColor');
  176. if (color) {
  177. adaptor.setStyle(this.chtml, 'backgroundColor', '');
  178. adaptor.setStyle(this.itable, 'backgroundColor', color);
  179. }
  180. }
  181. /******************************************************************/
  182. /**
  183. * Pad any short rows with extra cells
  184. */
  185. protected padRows() {
  186. const adaptor = this.adaptor;
  187. for (const row of adaptor.childNodes(this.itable) as N[]) {
  188. while (adaptor.childNodes(row).length < this.numCols) {
  189. adaptor.append(row, this.html('mjx-mtd', {'extra': true}));
  190. }
  191. }
  192. }
  193. /**
  194. * Set the inter-column spacing for all columns
  195. * (Use frame spacing on the outsides, if needed, and use half the column spacing on each
  196. * neighboring column, so that if column lines are needed, they fall in the middle
  197. * of the column space.)
  198. */
  199. protected handleColumnSpacing() {
  200. const scale = (this.childNodes[0] ? 1 / this.childNodes[0].getBBox().rscale : 1);
  201. const spacing = this.getEmHalfSpacing(this.fSpace[0], this.cSpace, scale);
  202. const frame = this.frame;
  203. //
  204. // For each row...
  205. //
  206. for (const row of this.tableRows) {
  207. let i = 0;
  208. //
  209. // For each cell in the row...
  210. //
  211. for (const cell of row.tableCells) {
  212. //
  213. // Get the left and right-hand spacing
  214. //
  215. const lspace = spacing[i++];
  216. const rspace = spacing[i];
  217. //
  218. // Set the style for the spacing, if it is needed, and isn't the
  219. // default already set in the mtd styles
  220. //
  221. const styleNode = (cell ? cell.chtml : this.adaptor.childNodes(row.chtml)[i] as N);
  222. if ((i > 1 && lspace !== '0.4em') || (frame && i === 1)) {
  223. this.adaptor.setStyle(styleNode, 'paddingLeft', lspace);
  224. }
  225. if ((i < this.numCols && rspace !== '0.4em') || (frame && i === this.numCols)) {
  226. this.adaptor.setStyle(styleNode, 'paddingRight', rspace);
  227. }
  228. }
  229. }
  230. }
  231. /**
  232. * Add borders to the left of cells to make the column lines
  233. */
  234. protected handleColumnLines() {
  235. if (this.node.attributes.get('columnlines') === 'none') return;
  236. const lines = this.getColumnAttributes('columnlines');
  237. for (const row of this.childNodes) {
  238. let i = 0;
  239. for (const cell of this.adaptor.childNodes(row.chtml).slice(1) as N[]) {
  240. const line = lines[i++];
  241. if (line === 'none') continue;
  242. this.adaptor.setStyle(cell, 'borderLeft', '.07em ' + line);
  243. }
  244. }
  245. }
  246. /**
  247. * Add widths to the cells for the column widths
  248. */
  249. protected handleColumnWidths() {
  250. for (const row of this.childNodes) {
  251. let i = 0;
  252. for (const cell of this.adaptor.childNodes(row.chtml) as N[]) {
  253. const w = this.cWidths[i++];
  254. if (w !== null) {
  255. const width = (typeof w === 'number' ? this.em(w) : w);
  256. this.adaptor.setStyle(cell, 'width', width);
  257. this.adaptor.setStyle(cell, 'maxWidth', width);
  258. this.adaptor.setStyle(cell, 'minWidth', width);
  259. }
  260. }
  261. }
  262. }
  263. /**
  264. * Set the inter-row spacing for all rows
  265. * (Use frame spacing on the outsides, if needed, and use half the row spacing on each
  266. * neighboring row, so that if row lines are needed, they fall in the middle
  267. * of the row space.)
  268. */
  269. protected handleRowSpacing() {
  270. const scale = (this.childNodes[0] ? 1 / this.childNodes[0].getBBox().rscale : 1);
  271. const spacing = this.getEmHalfSpacing(this.fSpace[1], this.rSpace, scale);
  272. const frame = this.frame;
  273. //
  274. // For each row...
  275. //
  276. let i = 0;
  277. for (const row of this.childNodes) {
  278. //
  279. // Get the top and bottom spacing
  280. //
  281. const tspace = spacing[i++];
  282. const bspace = spacing[i];
  283. //
  284. // For each cell in the row...
  285. //
  286. for (const cell of row.childNodes) {
  287. //
  288. // Set the style for the spacing, if it is needed, and isn't the
  289. // default already set in the mtd styles
  290. //
  291. if ((i > 1 && tspace !== '0.215em') || (frame && i === 1)) {
  292. this.adaptor.setStyle(cell.chtml, 'paddingTop', tspace);
  293. }
  294. if ((i < this.numRows && bspace !== '0.215em') || (frame && i === this.numRows)) {
  295. this.adaptor.setStyle(cell.chtml, 'paddingBottom', bspace);
  296. }
  297. }
  298. }
  299. }
  300. /**
  301. * Add borders to the tops of cells to make the row lines
  302. */
  303. protected handleRowLines() {
  304. if (this.node.attributes.get('rowlines') === 'none') return;
  305. const lines = this.getRowAttributes('rowlines');
  306. let i = 0;
  307. for (const row of this.childNodes.slice(1)) {
  308. const line = lines[i++];
  309. if (line === 'none') continue;
  310. for (const cell of this.adaptor.childNodes(row.chtml) as N[]) {
  311. this.adaptor.setStyle(cell, 'borderTop', '.07em ' + line);
  312. }
  313. }
  314. }
  315. /**
  316. * Adjust row heights for equal-sized rows
  317. */
  318. protected handleRowHeights() {
  319. if (this.node.attributes.get('equalrows')) {
  320. this.handleEqualRows();
  321. }
  322. }
  323. /**
  324. * Set the heights of all rows to be the same, and properly center
  325. * baseline or axis rows within the newly sized
  326. */
  327. protected handleEqualRows() {
  328. const space = this.getRowHalfSpacing();
  329. const {H, D, NH, ND} = this.getTableData();
  330. const HD = this.getEqualRowHeight();
  331. //
  332. // Loop through the rows and set their heights
  333. //
  334. for (let i = 0; i < this.numRows; i++) {
  335. const row = this.childNodes[i];
  336. this.setRowHeight(row, HD + space[i] + space[i + 1] + this.rLines[i]);
  337. if (HD !== NH[i] + ND[i]) {
  338. this.setRowBaseline(row, HD, (HD - H[i] + D[i]) / 2);
  339. }
  340. }
  341. }
  342. /**
  343. * @param {CHTMLWrapper} row The row whose height is to be set
  344. * @param {number} HD The height to be set for the row
  345. */
  346. protected setRowHeight(row: CHTMLWrapper<N, T, D>, HD: number) {
  347. this.adaptor.setStyle(row.chtml, 'height', this.em(HD));
  348. }
  349. /**
  350. * Make sure the baseline is in the right position for cells
  351. * that are row aligned to baseline ot axis
  352. *
  353. * @param {CHTMLWrapper} row The row to be set
  354. * @param {number} HD The total height+depth for the row
  355. * @param {number] D The new depth for the row
  356. */
  357. protected setRowBaseline(row: CHTMLWrapper<N, T, D>, HD: number, D: number) {
  358. const ralign = row.node.attributes.get('rowalign') as string;
  359. //
  360. // Loop through the cells and set the strut height and depth.
  361. // The strut is the last element in the cell.
  362. //
  363. for (const cell of row.childNodes) {
  364. if (this.setCellBaseline(cell, ralign, HD, D)) break;
  365. }
  366. }
  367. /**
  368. * Make sure the baseline is in the correct place for cells aligned on baseline or axis
  369. *
  370. * @param {CHTMLWrapper} cell The cell to modify
  371. * @param {string} ralign The alignment of the row
  372. * @param {number} HD The total height+depth for the row
  373. * @param {number] D The new depth for the row
  374. * @return {boolean} True if no other cells in this row need to be processed
  375. */
  376. protected setCellBaseline(cell: CHTMLWrapper<N, T, D>, ralign: string, HD: number, D: number): boolean {
  377. const calign = cell.node.attributes.get('rowalign');
  378. if (calign === 'baseline' || calign === 'axis') {
  379. const adaptor = this.adaptor;
  380. const child = adaptor.lastChild(cell.chtml) as N;
  381. adaptor.setStyle(child, 'height', this.em(HD));
  382. adaptor.setStyle(child, 'verticalAlign', this.em(-D));
  383. const row = cell.parent;
  384. if ((!row.node.isKind('mlabeledtr') || cell !== row.childNodes[0]) &&
  385. (ralign === 'baseline' || ralign === 'axis')) {
  386. return true;
  387. }
  388. }
  389. return false;
  390. }
  391. /**
  392. * Add a frame to the mtable, if needed
  393. */
  394. protected handleFrame() {
  395. if (this.frame && this.fLine) {
  396. this.adaptor.setStyle(this.itable, 'border', '.07em ' + this.node.attributes.get('frame'));
  397. }
  398. }
  399. /**
  400. * Handle percentage widths and fixed widths
  401. */
  402. protected handleWidth() {
  403. const adaptor = this.adaptor;
  404. const {w, L, R} = this.getBBox();
  405. adaptor.setStyle(this.chtml, 'minWidth', this.em(L + w + R));
  406. let W = this.node.attributes.get('width') as string;
  407. if (isPercent(W)) {
  408. adaptor.setStyle(this.chtml, 'width', '');
  409. adaptor.setAttribute(this.chtml, 'width', 'full');
  410. } else if (!this.hasLabels) {
  411. if (W === 'auto') return;
  412. W = this.em(this.length2em(W) + 2 * this.fLine);
  413. }
  414. const table = adaptor.firstChild(this.chtml) as N;
  415. adaptor.setStyle(table, 'width', W);
  416. adaptor.setStyle(table, 'minWidth', this.em(w));
  417. if (L || R) {
  418. adaptor.setStyle(this.chtml, 'margin', '');
  419. const style = (this.node.attributes.get('data-width-includes-label') ? 'padding' : 'margin');
  420. if (L === R) {
  421. adaptor.setStyle(table, style, '0 ' + this.em(R));
  422. } else {
  423. adaptor.setStyle(table, style, '0 ' + this.em(R) + ' 0 ' + this.em(L));
  424. }
  425. }
  426. adaptor.setAttribute(this.itable, 'width', 'full');
  427. }
  428. /**
  429. * Handle alignment of table to surrounding baseline
  430. */
  431. protected handleAlign() {
  432. const [align, row] = this.getAlignmentRow();
  433. if (row === null) {
  434. if (align !== 'axis') {
  435. this.adaptor.setAttribute(this.chtml, 'align', align);
  436. }
  437. } else {
  438. const y = this.getVerticalPosition(row, align);
  439. this.adaptor.setAttribute(this.chtml, 'align', 'top');
  440. this.adaptor.setStyle(this.chtml, 'verticalAlign', this.em(y));
  441. }
  442. }
  443. /**
  444. * Mark the alignment of the table
  445. */
  446. protected handleJustify() {
  447. const align = this.getAlignShift()[0];
  448. if (align !== 'center') {
  449. this.adaptor.setAttribute(this.chtml, 'justify', align);
  450. }
  451. }
  452. /******************************************************************/
  453. /**
  454. * Handle addition of labels to the table
  455. */
  456. protected handleLabels() {
  457. if (!this.hasLabels) return;
  458. const labels = this.labels;
  459. const attributes = this.node.attributes;
  460. const adaptor = this.adaptor;
  461. //
  462. // Set the side for the labels
  463. //
  464. const side = attributes.get('side') as string;
  465. adaptor.setAttribute(this.chtml, 'side', side);
  466. adaptor.setAttribute(labels, 'align', side);
  467. adaptor.setStyle(labels, side, '0');
  468. //
  469. // Make sure labels don't overlap table
  470. //
  471. const [align, shift] = this.addLabelPadding(side);
  472. //
  473. // Handle indentation
  474. //
  475. if (shift) {
  476. const table = adaptor.firstChild(this.chtml) as N;
  477. this.setIndent(table, align, shift);
  478. }
  479. //
  480. // Add the labels to the table
  481. //
  482. this.updateRowHeights();
  483. this.addLabelSpacing();
  484. }
  485. /**
  486. * @param {string} side The side for the labels
  487. * @return {[string, number]} The alignment and shift values
  488. */
  489. protected addLabelPadding(side: string): [string, number] {
  490. const [ , align, shift] = this.getPadAlignShift(side);
  491. const styles: OptionList = {};
  492. if (side === 'right' && !this.node.attributes.get('data-width-includes-label')) {
  493. const W = this.node.attributes.get('width') as string;
  494. const {w, L, R} = this.getBBox();
  495. styles.style = {
  496. width: (isPercent(W) ? 'calc(' + W + ' + ' + this.em(L + R) + ')' : this.em(L + w + R))
  497. };
  498. }
  499. this.adaptor.append(this.chtml, this.html('mjx-labels', styles, [this.labels]));
  500. return [align, shift] as [string, number];
  501. }
  502. /**
  503. * Update any rows that are not naturally tall enough for the labels,
  504. * and set the baseline for labels that are baseline aligned.
  505. */
  506. protected updateRowHeights() {
  507. let {H, D, NH, ND} = this.getTableData();
  508. const space = this.getRowHalfSpacing();
  509. for (let i = 0; i < this.numRows; i++) {
  510. const row = this.childNodes[i];
  511. this.setRowHeight(row, H[i] + D[i] + space[i] + space[i + 1] + this.rLines[i]);
  512. if (H[i] !== NH[i] || D[i] !== ND[i]) {
  513. this.setRowBaseline(row, H[i] + D[i], D[i]);
  514. } else if (row.node.isKind('mlabeledtr')) {
  515. this.setCellBaseline(row.childNodes[0], '', H[i] + D[i], D[i]);
  516. }
  517. }
  518. }
  519. /**
  520. * Add spacing elements between the label rows to align them with the rest of the table
  521. */
  522. protected addLabelSpacing() {
  523. const adaptor = this.adaptor;
  524. const equal = this.node.attributes.get('equalrows') as boolean;
  525. const {H, D} = this.getTableData();
  526. const HD = (equal ? this.getEqualRowHeight() : 0);
  527. const space = this.getRowHalfSpacing();
  528. //
  529. // Start with frame size and add in spacing, height and depth,
  530. // and line thickness for each non-labeled row.
  531. //
  532. let h = this.fLine;
  533. let current = adaptor.firstChild(this.labels) as N;
  534. for (let i = 0; i < this.numRows; i++) {
  535. const row = this.childNodes[i];
  536. if (row.node.isKind('mlabeledtr')) {
  537. h && adaptor.insert(this.html('mjx-mtr', {style: {height: this.em(h)}}), current);
  538. adaptor.setStyle(current, 'height', this.em((equal ? HD : H[i] + D[i]) + space[i] + space[i + 1]));
  539. current = adaptor.next(current) as N;
  540. h = this.rLines[i];
  541. } else {
  542. h += space[i] + (equal ? HD : H[i] + D[i]) + space[i + 1] + this.rLines[i];
  543. }
  544. }
  545. }
  546. }