upcasttable.js 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. /**
  2. * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  4. */
  5. /**
  6. * @module table/converters/upcasttable
  7. */
  8. import { createEmptyTableCell } from '../commands/utils';
  9. /**
  10. * View table element to model table element conversion helper.
  11. *
  12. * This conversion helper converts the table element as well as table rows.
  13. *
  14. * @returns {Function} Conversion helper.
  15. */
  16. export default function upcastTable() {
  17. return dispatcher => {
  18. dispatcher.on( 'element:table', ( evt, data, conversionApi ) => {
  19. const viewTable = data.viewItem;
  20. // When element was already consumed then skip it.
  21. if ( !conversionApi.consumable.test( viewTable, { name: true } ) ) {
  22. return;
  23. }
  24. const { rows, headingRows, headingColumns } = scanTable( viewTable );
  25. // Only set attributes if values is greater then 0.
  26. const attributes = {};
  27. if ( headingColumns ) {
  28. attributes.headingColumns = headingColumns;
  29. }
  30. if ( headingRows ) {
  31. attributes.headingRows = headingRows;
  32. }
  33. const table = conversionApi.writer.createElement( 'table', attributes );
  34. // Insert element on allowed position.
  35. const splitResult = conversionApi.splitToAllowedParent( table, data.modelCursor );
  36. // When there is no split result it means that we can't insert element to model tree, so let's skip it.
  37. if ( !splitResult ) {
  38. return;
  39. }
  40. conversionApi.writer.insert( table, splitResult.position );
  41. conversionApi.consumable.consume( viewTable, { name: true } );
  42. if ( rows.length ) {
  43. // Upcast table rows in proper order (heading rows first).
  44. rows.forEach( row => conversionApi.convertItem( row, conversionApi.writer.createPositionAt( table, 'end' ) ) );
  45. } else {
  46. // Create one row and one table cell for empty table.
  47. const row = conversionApi.writer.createElement( 'tableRow' );
  48. conversionApi.writer.insert( row, conversionApi.writer.createPositionAt( table, 'end' ) );
  49. createEmptyTableCell( conversionApi.writer, conversionApi.writer.createPositionAt( row, 'end' ) );
  50. }
  51. // Set conversion result range.
  52. data.modelRange = conversionApi.writer.createRange(
  53. // Range should start before inserted element
  54. conversionApi.writer.createPositionBefore( table ),
  55. // Should end after but we need to take into consideration that children could split our
  56. // element, so we need to move range after parent of the last converted child.
  57. // before: <allowed>[]</allowed>
  58. // after: <allowed>[<converted><child></child></converted><child></child><converted>]</converted></allowed>
  59. conversionApi.writer.createPositionAfter( table )
  60. );
  61. // Now we need to check where the modelCursor should be.
  62. // If we had to split parent to insert our element then we want to continue conversion inside split parent.
  63. //
  64. // before: <allowed><notAllowed>[]</notAllowed></allowed>
  65. // after: <allowed><notAllowed></notAllowed><converted></converted><notAllowed>[]</notAllowed></allowed>
  66. if ( splitResult.cursorParent ) {
  67. data.modelCursor = conversionApi.writer.createPositionAt( splitResult.cursorParent, 0 );
  68. // Otherwise just continue after inserted element.
  69. } else {
  70. data.modelCursor = data.modelRange.end;
  71. }
  72. } );
  73. };
  74. }
  75. export function upcastTableCell( elementName ) {
  76. return dispatcher => {
  77. dispatcher.on( `element:${ elementName }`, ( evt, data, conversionApi ) => {
  78. const viewTableCell = data.viewItem;
  79. // When element was already consumed then skip it.
  80. if ( !conversionApi.consumable.test( viewTableCell, { name: true } ) ) {
  81. return;
  82. }
  83. const tableCell = conversionApi.writer.createElement( 'tableCell' );
  84. // Insert element on allowed position.
  85. const splitResult = conversionApi.splitToAllowedParent( tableCell, data.modelCursor );
  86. // When there is no split result it means that we can't insert element to model tree, so let's skip it.
  87. if ( !splitResult ) {
  88. return;
  89. }
  90. conversionApi.writer.insert( tableCell, splitResult.position );
  91. conversionApi.consumable.consume( viewTableCell, { name: true } );
  92. const modelCursor = conversionApi.writer.createPositionAt( tableCell, 0 );
  93. conversionApi.convertChildren( viewTableCell, modelCursor );
  94. // Ensure a paragraph in the model for empty table cells.
  95. if ( !tableCell.childCount ) {
  96. conversionApi.writer.insertElement( 'paragraph', modelCursor );
  97. }
  98. // Set conversion result range.
  99. data.modelRange = conversionApi.writer.createRange(
  100. // Range should start before inserted element
  101. conversionApi.writer.createPositionBefore( tableCell ),
  102. // Should end after but we need to take into consideration that children could split our
  103. // element, so we need to move range after parent of the last converted child.
  104. // before: <allowed>[]</allowed>
  105. // after: <allowed>[<converted><child></child></converted><child></child><converted>]</converted></allowed>
  106. conversionApi.writer.createPositionAfter( tableCell )
  107. );
  108. // Continue after inserted element.
  109. data.modelCursor = data.modelRange.end;
  110. } );
  111. };
  112. }
  113. // Scans table rows and extracts required metadata from the table:
  114. //
  115. // headingRows - The number of rows that go as table headers.
  116. // headingColumns - The maximum number of row headings.
  117. // rows - Sorted `<tr>` elements as they should go into the model - ie. if `<thead>` is inserted after `<tbody>` in the view.
  118. //
  119. // @param {module:engine/view/element~Element} viewTable
  120. // @returns {{headingRows, headingColumns, rows}}
  121. function scanTable( viewTable ) {
  122. const tableMeta = {
  123. headingRows: 0,
  124. headingColumns: 0
  125. };
  126. // The `<tbody>` and `<thead>` sections in the DOM do not have to be in order `<thead>` -> `<tbody>` and there might be more than one
  127. // of them.
  128. // As the model does not have these sections, rows from different sections must be sorted.
  129. // For example, below is a valid HTML table:
  130. //
  131. // <table>
  132. // <tbody><tr><td>2</td></tr></tbody>
  133. // <thead><tr><td>1</td></tr></thead>
  134. // <tbody><tr><td>3</td></tr></tbody>
  135. // </table>
  136. //
  137. // But browsers will render rows in order as: 1 as heading and 2 and 3 as the body.
  138. const headRows = [];
  139. const bodyRows = [];
  140. // Currently the editor does not support more then one <thead> section.
  141. // Only the first <thead> from the view will be used as heading rows and others will be converted to body rows.
  142. let firstTheadElement;
  143. for ( const tableChild of Array.from( viewTable.getChildren() ) ) {
  144. // Only <thead>, <tbody> & <tfoot> from allowed table children can have <tr>s.
  145. // The else is for future purposes (mainly <caption>).
  146. if ( tableChild.name === 'tbody' || tableChild.name === 'thead' || tableChild.name === 'tfoot' ) {
  147. // Save the first <thead> in the table as table header - all other ones will be converted to table body rows.
  148. if ( tableChild.name === 'thead' && !firstTheadElement ) {
  149. firstTheadElement = tableChild;
  150. }
  151. // There might be some extra empty text nodes between the `tr`s.
  152. // Make sure further code operates on `tr`s only. (#145)
  153. const trs = Array.from( tableChild.getChildren() ).filter( el => el.is( 'element', 'tr' ) );
  154. for ( const tr of trs ) {
  155. // This <tr> is a child of a first <thead> element.
  156. if ( tr.parent.name === 'thead' && tr.parent === firstTheadElement ) {
  157. tableMeta.headingRows++;
  158. headRows.push( tr );
  159. } else {
  160. bodyRows.push( tr );
  161. // For other rows check how many column headings this row has.
  162. const headingCols = scanRowForHeadingColumns( tr, tableMeta, firstTheadElement );
  163. if ( headingCols > tableMeta.headingColumns ) {
  164. tableMeta.headingColumns = headingCols;
  165. }
  166. }
  167. }
  168. }
  169. }
  170. tableMeta.rows = [ ...headRows, ...bodyRows ];
  171. return tableMeta;
  172. }
  173. // Scans a `<tr>` element and its children for metadata:
  174. // - For heading row:
  175. // - Adds this row to either the heading or the body rows.
  176. // - Updates the number of heading rows.
  177. // - For body rows:
  178. // - Calculates the number of column headings.
  179. //
  180. // @param {module:engine/view/element~Element} tr
  181. // @returns {Number}
  182. function scanRowForHeadingColumns( tr ) {
  183. let headingColumns = 0;
  184. let index = 0;
  185. // Filter out empty text nodes from tr children.
  186. const children = Array.from( tr.getChildren() )
  187. .filter( child => child.name === 'th' || child.name === 'td' );
  188. // Count starting adjacent <th> elements of a <tr>.
  189. while ( index < children.length && children[ index ].name === 'th' ) {
  190. const th = children[ index ];
  191. // Adjust columns calculation by the number of spanned columns.
  192. const colspan = parseInt( th.getAttribute( 'colspan' ) || 1 );
  193. headingColumns = headingColumns + colspan;
  194. index++;
  195. }
  196. return headingColumns;
  197. }