downcasttable.js 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. /**
  2. * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module table/converters/downcasttable
  7. */
  8. import ViewPosition from '@ckeditor/ckeditor5-engine/src/view/position';
  9. /**
  10. * Model table element to view table element conversion helper.
  11. *
  12. * This conversion helper creates whole table element with child elements.
  13. *
  14. * @returns {Function} Conversion helper.
  15. */
  16. export default function downcastTable() {
  17. return dispatcher => dispatcher.on( 'insert:table', ( evt, data, conversionApi ) => {
  18. const table = data.item;
  19. if ( !conversionApi.consumable.consume( table, 'insert' ) ) {
  20. return;
  21. }
  22. // The <thead> and <tbody> elements are created on the fly when needed by inner `getTableSection()` function.
  23. let tHead, tBody;
  24. const tableElement = conversionApi.writer.createContainerElement( 'table' );
  25. const headingRows = parseInt( table.getAttribute( 'headingRows' ) ) || 0;
  26. const tableRows = Array.from( table.getChildren() );
  27. const cellSpans = new CellSpans();
  28. for ( const tableRow of tableRows ) {
  29. const rowIndex = tableRows.indexOf( tableRow );
  30. const tableSectionElement = getTableSection( rowIndex, headingRows, tableElement, conversionApi );
  31. downcastTableRow( tableRow, rowIndex, cellSpans, tableSectionElement, conversionApi );
  32. // Drop table cell spans information for downcasted row.
  33. cellSpans.drop( rowIndex );
  34. }
  35. const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
  36. conversionApi.mapper.bindElements( table, tableElement );
  37. conversionApi.writer.insert( viewPosition, tableElement );
  38. // Creates if not existing and returns <tbody> or <thead> element for given rowIndex.
  39. function getTableSection( rowIndex, headingRows, tableElement, conversionApi ) {
  40. if ( headingRows && rowIndex < headingRows ) {
  41. if ( !tHead ) {
  42. tHead = createTableSection( 'thead', tableElement, conversionApi );
  43. }
  44. return tHead;
  45. }
  46. if ( !tBody ) {
  47. tBody = createTableSection( 'tbody', tableElement, conversionApi );
  48. }
  49. return tBody;
  50. }
  51. }, { priority: 'normal' } );
  52. }
  53. // Downcast converter for tableRow model element. Converts tableCells as well.
  54. //
  55. // @param {module:engine/model/element~Element} tableRow
  56. // @param {Number} rowIndex
  57. // @param {CellSpans} cellSpans
  58. // @param {module:engine/view/containerelement~ContainerElement} tableSection
  59. // @param {Object} conversionApi
  60. function downcastTableRow( tableRow, rowIndex, cellSpans, tableSection, conversionApi ) {
  61. // Will always consume since we're converting <tableRow> element from a parent <table>.
  62. conversionApi.consumable.consume( tableRow, 'insert' );
  63. const trElement = conversionApi.writer.createContainerElement( 'tr' );
  64. conversionApi.mapper.bindElements( tableRow, trElement );
  65. conversionApi.writer.insert( ViewPosition.createAt( tableSection, 'end' ), trElement );
  66. // Defines tableCell horizontal position in table.
  67. // Might be different then position of tableCell in parent tableRow
  68. // as tableCells from previous rows might overlaps current row's cells.
  69. let columnIndex = 0;
  70. const headingRows = tableRow.parent.getAttribute( 'headingRows' ) || 0;
  71. const headingColumns = tableRow.parent.getAttribute( 'headingColumns' ) || 0;
  72. for ( const tableCell of Array.from( tableRow.getChildren() ) ) {
  73. // Check whether current columnIndex is overlapped by table cells from previous rows.
  74. columnIndex = cellSpans.getNextFreeColumnIndex( rowIndex, columnIndex );
  75. const colspan = tableCell.hasAttribute( 'colspan' ) ? parseInt( tableCell.getAttribute( 'colspan' ) ) : 1;
  76. const rowspan = tableCell.hasAttribute( 'rowspan' ) ? parseInt( tableCell.getAttribute( 'rowspan' ) ) : 1;
  77. cellSpans.recordSpans( rowIndex, columnIndex, rowspan, colspan );
  78. // Will always consume since we're converting <tableRow> element from a parent <table>.
  79. conversionApi.consumable.consume( tableCell, 'insert' );
  80. const cellElementName = getCellElementName( rowIndex, columnIndex, headingRows, headingColumns );
  81. const cellElement = conversionApi.writer.createContainerElement( cellElementName );
  82. conversionApi.mapper.bindElements( tableCell, cellElement );
  83. conversionApi.writer.insert( ViewPosition.createAt( trElement, 'end' ), cellElement );
  84. // Skip to next "free" column index.
  85. columnIndex += colspan;
  86. }
  87. }
  88. // Creates table section at the end of a table.
  89. //
  90. // @param {String} elementName
  91. // @param {module:engine/view/element~Element} tableElement
  92. // @param conversionApi
  93. // @return {module:engine/view/containerelement~ContainerElement}
  94. function createTableSection( elementName, tableElement, conversionApi ) {
  95. const tableChildElement = conversionApi.writer.createContainerElement( elementName );
  96. conversionApi.writer.insert( ViewPosition.createAt( tableElement, 'end' ), tableChildElement );
  97. return tableChildElement;
  98. }
  99. // Returns `th` for heading cells and `td` for other cells.
  100. // It is based on tableCell location (rowIndex x columnIndex) and the sizes of column & row headings sizes.
  101. //
  102. // @param {Number} rowIndex
  103. // @param {Number} columnIndex
  104. // @param {Number} headingRows
  105. // @param {Number} headingColumns
  106. // @returns {String}
  107. function getCellElementName( rowIndex, columnIndex, headingRows, headingColumns ) {
  108. // Column heading are all tableCells in the first `columnHeading` rows.
  109. const isHeadingForAColumn = headingRows && headingRows > rowIndex;
  110. // So a whole row gets <th> element.
  111. if ( isHeadingForAColumn ) {
  112. return 'th';
  113. }
  114. // Row heading are tableCells which columnIndex is lower then headingColumns.
  115. const isHeadingForARow = headingColumns && headingColumns > columnIndex;
  116. return isHeadingForARow ? 'th' : 'td';
  117. }
  118. /**
  119. * Holds information about spanned table cells.
  120. *
  121. * @private
  122. */
  123. class CellSpans {
  124. /**
  125. * Creates CellSpans instance.
  126. */
  127. constructor() {
  128. /**
  129. * Holds table cell spans mapping.
  130. *
  131. * @type {Map<Number, Number>}
  132. * @private
  133. */
  134. this._spans = new Map();
  135. }
  136. /**
  137. * Returns proper column index if a current cell index is overlapped by other (has a span defined).
  138. *
  139. * @param {Number} row
  140. * @param {Number} column
  141. * @return {Number} Returns current column or updated column index.
  142. */
  143. getNextFreeColumnIndex( row, column ) {
  144. let span = this._check( row, column ) || 0;
  145. // Offset current table cell columnIndex by spanning cells from rows above.
  146. while ( span ) {
  147. column += span;
  148. span = this._check( row, column );
  149. }
  150. return column;
  151. }
  152. /**
  153. * Updates spans based on current table cell height & width. Spans with height <= 1 will not be recorded.
  154. *
  155. * For instance if a table cell at row 0 and column 0 has height of 3 and width of 2 we're setting spans:
  156. *
  157. * 0 1 2 3 4 5
  158. * 0:
  159. * 1: 2
  160. * 2: 2
  161. * 3:
  162. *
  163. * Adding another spans for a table cell at row 2 and column 1 that has height of 2 and width of 4 will update above to:
  164. *
  165. * 0 1 2 3 4 5
  166. * 0:
  167. * 1: 2
  168. * 2: 2
  169. * 3: 4
  170. *
  171. * The above span mapping was calculated from a table below (cells 03 & 12 were not added as their height is 1):
  172. *
  173. * +----+----+----+----+----+----+
  174. * | 00 | 02 | 03 | 05 |
  175. * | +--- +----+----+----+
  176. * | | 12 | 24 | 25 |
  177. * | +----+----+----+----+
  178. * | | 22 |
  179. * |----+----+ +
  180. * | 31 | 32 | |
  181. * +----+----+----+----+----+----+
  182. *
  183. * @param {Number} rowIndex
  184. * @param {Number} columnIndex
  185. * @param {Number} height
  186. * @param {Number} width
  187. */
  188. recordSpans( rowIndex, columnIndex, height, width ) {
  189. // This will update all rows below up to row height with value of span width.
  190. for ( let rowToUpdate = rowIndex + 1; rowToUpdate < rowIndex + height; rowToUpdate++ ) {
  191. if ( !this._spans.has( rowToUpdate ) ) {
  192. this._spans.set( rowToUpdate, new Map() );
  193. }
  194. const rowSpans = this._spans.get( rowToUpdate );
  195. rowSpans.set( columnIndex, width );
  196. }
  197. }
  198. /**
  199. * Removes row from mapping.
  200. *
  201. * @param {Number} rowIndex
  202. */
  203. drop( rowIndex ) {
  204. if ( this._spans.has( rowIndex ) ) {
  205. this._spans.delete( rowIndex );
  206. }
  207. }
  208. /**
  209. * Checks if given table cell is spanned by other.
  210. *
  211. * @param {Number} rowIndex
  212. * @param {Number} columnIndex
  213. * @return {Boolean|Number} Returns false or width of a span.
  214. * @private
  215. */
  216. _check( rowIndex, columnIndex ) {
  217. if ( !this._spans.has( rowIndex ) ) {
  218. return false;
  219. }
  220. const rowSpans = this._spans.get( rowIndex );
  221. return rowSpans.has( columnIndex ) ? rowSpans.get( columnIndex ) : false;
  222. }
  223. }