downcast.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591
  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/downcast
  7. */
  8. import TableWalker from './../tablewalker';
  9. import { toWidget, toWidgetEditable, setHighlightHandling } from '@ckeditor/ckeditor5-widget/src/utils';
  10. /**
  11. * Model table element to view table element conversion helper.
  12. *
  13. * This conversion helper creates the whole table element with child elements.
  14. *
  15. * @param {Object} options
  16. * @param {Boolean} options.asWidget If set to `true`, the downcast conversion will produce a widget.
  17. * @returns {Function} Conversion helper.
  18. */
  19. export function downcastInsertTable( options = {} ) {
  20. return dispatcher => dispatcher.on( 'insert:table', ( evt, data, conversionApi ) => {
  21. const table = data.item;
  22. if ( !conversionApi.consumable.consume( table, 'insert' ) ) {
  23. return;
  24. }
  25. // Consume attributes if present to not fire attribute change downcast
  26. conversionApi.consumable.consume( table, 'attribute:headingRows:table' );
  27. conversionApi.consumable.consume( table, 'attribute:headingColumns:table' );
  28. const asWidget = options && options.asWidget;
  29. const figureElement = conversionApi.writer.createContainerElement( 'figure', { class: 'table' } );
  30. const tableElement = conversionApi.writer.createContainerElement( 'table' );
  31. conversionApi.writer.insert( conversionApi.writer.createPositionAt( figureElement, 0 ), tableElement );
  32. let tableWidget;
  33. if ( asWidget ) {
  34. tableWidget = toTableWidget( figureElement, conversionApi.writer );
  35. }
  36. const tableWalker = new TableWalker( table );
  37. const tableAttributes = {
  38. headingRows: table.getAttribute( 'headingRows' ) || 0,
  39. headingColumns: table.getAttribute( 'headingColumns' ) || 0
  40. };
  41. // Cache for created table rows.
  42. const viewRows = new Map();
  43. for ( const tableSlot of tableWalker ) {
  44. const { row, cell } = tableSlot;
  45. const tableSection = getOrCreateTableSection( getSectionName( row, tableAttributes ), tableElement, conversionApi );
  46. const tableRow = table.getChild( row );
  47. const trElement = viewRows.get( row ) || createTr( tableRow, row, tableSection, conversionApi );
  48. viewRows.set( row, trElement );
  49. // Consume table cell - it will be always consumed as we convert whole table at once.
  50. conversionApi.consumable.consume( cell, 'insert' );
  51. const insertPosition = conversionApi.writer.createPositionAt( trElement, 'end' );
  52. createViewTableCellElement( tableSlot, tableAttributes, insertPosition, conversionApi, options );
  53. }
  54. const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
  55. conversionApi.mapper.bindElements( table, asWidget ? tableWidget : figureElement );
  56. conversionApi.writer.insert( viewPosition, asWidget ? tableWidget : figureElement );
  57. } );
  58. }
  59. /**
  60. * Model row element to view `<tr>` element conversion helper.
  61. *
  62. * This conversion helper creates the whole `<tr>` element with child elements.
  63. *
  64. * @returns {Function} Conversion helper.
  65. */
  66. export function downcastInsertRow( options = {} ) {
  67. return dispatcher => dispatcher.on( 'insert:tableRow', ( evt, data, conversionApi ) => {
  68. const tableRow = data.item;
  69. if ( !conversionApi.consumable.consume( tableRow, 'insert' ) ) {
  70. return;
  71. }
  72. const table = tableRow.parent;
  73. const figureElement = conversionApi.mapper.toViewElement( table );
  74. const tableElement = getViewTable( figureElement );
  75. const row = table.getChildIndex( tableRow );
  76. const tableWalker = new TableWalker( table, { row } );
  77. const tableAttributes = {
  78. headingRows: table.getAttribute( 'headingRows' ) || 0,
  79. headingColumns: table.getAttribute( 'headingColumns' ) || 0
  80. };
  81. // Cache for created table rows.
  82. const viewRows = new Map();
  83. for ( const tableSlot of tableWalker ) {
  84. const tableSection = getOrCreateTableSection( getSectionName( row, tableAttributes ), tableElement, conversionApi );
  85. const trElement = viewRows.get( row ) || createTr( tableRow, row, tableSection, conversionApi );
  86. viewRows.set( row, trElement );
  87. // Consume table cell - it will be always consumed as we convert whole row at once.
  88. conversionApi.consumable.consume( tableSlot.cell, 'insert' );
  89. const insertPosition = conversionApi.writer.createPositionAt( trElement, 'end' );
  90. createViewTableCellElement( tableSlot, tableAttributes, insertPosition, conversionApi, options );
  91. }
  92. } );
  93. }
  94. /**
  95. * Model table cell element to view `<td>` or `<th>` element conversion helper.
  96. *
  97. * This conversion helper will create proper `<th>` elements for table cells that are in the heading section (heading row or column)
  98. * and `<td>` otherwise.
  99. *
  100. * @returns {Function} Conversion helper.
  101. */
  102. export function downcastInsertCell( options = {} ) {
  103. return dispatcher => dispatcher.on( 'insert:tableCell', ( evt, data, conversionApi ) => {
  104. const tableCell = data.item;
  105. if ( !conversionApi.consumable.consume( tableCell, 'insert' ) ) {
  106. return;
  107. }
  108. const tableRow = tableCell.parent;
  109. const table = tableRow.parent;
  110. const rowIndex = table.getChildIndex( tableRow );
  111. const tableWalker = new TableWalker( table, { row: rowIndex } );
  112. const tableAttributes = {
  113. headingRows: table.getAttribute( 'headingRows' ) || 0,
  114. headingColumns: table.getAttribute( 'headingColumns' ) || 0
  115. };
  116. // We need to iterate over a table in order to get proper row & column values from a walker
  117. for ( const tableSlot of tableWalker ) {
  118. if ( tableSlot.cell === tableCell ) {
  119. const trElement = conversionApi.mapper.toViewElement( tableRow );
  120. const insertPosition = conversionApi.writer.createPositionAt( trElement, tableRow.getChildIndex( tableCell ) );
  121. createViewTableCellElement( tableSlot, tableAttributes, insertPosition, conversionApi, options );
  122. // No need to iterate further.
  123. return;
  124. }
  125. }
  126. } );
  127. }
  128. /**
  129. * Conversion helper that acts on heading row table attribute change.
  130. *
  131. * This converter will:
  132. *
  133. * * Rename `<td>` to `<th>` elements or vice versa depending on headings.
  134. * * Create `<thead>` or `<tbody>` elements if needed.
  135. * * Remove empty `<thead>` or `<tbody>` if needed.
  136. *
  137. * @returns {Function} Conversion helper.
  138. */
  139. export function downcastTableHeadingRowsChange( options = {} ) {
  140. const asWidget = !!options.asWidget;
  141. return dispatcher => dispatcher.on( 'attribute:headingRows:table', ( evt, data, conversionApi ) => {
  142. const table = data.item;
  143. if ( !conversionApi.consumable.consume( data.item, evt.name ) ) {
  144. return;
  145. }
  146. const figureElement = conversionApi.mapper.toViewElement( table );
  147. const viewTable = getViewTable( figureElement );
  148. const oldRows = data.attributeOldValue;
  149. const newRows = data.attributeNewValue;
  150. // The head section has grown so move rows from <tbody> to <thead>.
  151. if ( newRows > oldRows ) {
  152. // Filter out only those rows that are in wrong section.
  153. const rowsToMove = Array.from( table.getChildren() ).filter( ( { index } ) => isBetween( index, oldRows - 1, newRows ) );
  154. const viewTableHead = getOrCreateTableSection( 'thead', viewTable, conversionApi );
  155. moveViewRowsToTableSection( rowsToMove, viewTableHead, conversionApi, 'end' );
  156. // Rename all table cells from moved rows to 'th' as they lands in <thead>.
  157. for ( const tableRow of rowsToMove ) {
  158. for ( const tableCell of tableRow.getChildren() ) {
  159. renameViewTableCell( tableCell, 'th', conversionApi, asWidget );
  160. }
  161. }
  162. }
  163. // The head section has shrunk so move rows from <thead> to <tbody>.
  164. else {
  165. // Filter out only those rows that are in wrong section.
  166. const rowsToMove = Array.from( table.getChildren() )
  167. .filter( ( { index } ) => isBetween( index, newRows - 1, oldRows ) )
  168. .reverse(); // The rows will be moved from <thead> to <tbody> in reverse order at the beginning of a <tbody>.
  169. const viewTableBody = getOrCreateTableSection( 'tbody', viewTable, conversionApi );
  170. moveViewRowsToTableSection( rowsToMove, viewTableBody, conversionApi, 0 );
  171. // Check if cells moved from <thead> to <tbody> requires renaming to <td> as this depends on current heading columns attribute.
  172. const tableWalker = new TableWalker( table, { startRow: newRows ? newRows - 1 : newRows, endRow: oldRows - 1 } );
  173. const tableAttributes = {
  174. headingRows: table.getAttribute( 'headingRows' ) || 0,
  175. headingColumns: table.getAttribute( 'headingColumns' ) || 0
  176. };
  177. for ( const tableSlot of tableWalker ) {
  178. renameViewTableCellIfRequired( tableSlot, tableAttributes, conversionApi, asWidget );
  179. }
  180. }
  181. // Cleanup: Ensure that thead & tbody sections are removed if left empty after moving rows. See #6437, #6391.
  182. removeTableSectionIfEmpty( 'thead', viewTable, conversionApi );
  183. removeTableSectionIfEmpty( 'tbody', viewTable, conversionApi );
  184. function isBetween( index, lower, upper ) {
  185. return index > lower && index < upper;
  186. }
  187. } );
  188. }
  189. /**
  190. * Conversion helper that acts on heading column table attribute change.
  191. *
  192. * Depending on changed attributes this converter will rename `<td` to `<th>` elements or vice versa depending on the cell column index.
  193. *
  194. * @returns {Function} Conversion helper.
  195. */
  196. export function downcastTableHeadingColumnsChange( options = {} ) {
  197. const asWidget = !!options.asWidget;
  198. return dispatcher => dispatcher.on( 'attribute:headingColumns:table', ( evt, data, conversionApi ) => {
  199. const table = data.item;
  200. if ( !conversionApi.consumable.consume( data.item, evt.name ) ) {
  201. return;
  202. }
  203. const tableAttributes = {
  204. headingRows: table.getAttribute( 'headingRows' ) || 0,
  205. headingColumns: table.getAttribute( 'headingColumns' ) || 0
  206. };
  207. const oldColumns = data.attributeOldValue;
  208. const newColumns = data.attributeNewValue;
  209. const lastColumnToCheck = ( oldColumns > newColumns ? oldColumns : newColumns ) - 1;
  210. for ( const tableSlot of new TableWalker( table, { endColumn: lastColumnToCheck } ) ) {
  211. renameViewTableCellIfRequired( tableSlot, tableAttributes, conversionApi, asWidget );
  212. }
  213. } );
  214. }
  215. /**
  216. * Conversion helper that acts on a removed row.
  217. *
  218. * @returns {Function} Conversion helper.
  219. */
  220. export function downcastRemoveRow() {
  221. return dispatcher => dispatcher.on( 'remove:tableRow', ( evt, data, conversionApi ) => {
  222. // Prevent default remove converter.
  223. evt.stop();
  224. const viewWriter = conversionApi.writer;
  225. const mapper = conversionApi.mapper;
  226. const viewStart = mapper.toViewPosition( data.position ).getLastMatchingPosition( value => !value.item.is( 'tr' ) );
  227. const viewItem = viewStart.nodeAfter;
  228. const tableSection = viewItem.parent;
  229. const viewTable = tableSection.parent;
  230. // Remove associated <tr> from the view.
  231. const removeRange = viewWriter.createRangeOn( viewItem );
  232. const removed = viewWriter.remove( removeRange );
  233. for ( const child of viewWriter.createRangeIn( removed ).getItems() ) {
  234. mapper.unbindViewElement( child );
  235. }
  236. // Cleanup: Ensure that thead & tbody sections are removed if left empty after removing rows. See #6437, #6391.
  237. removeTableSectionIfEmpty( 'thead', viewTable, conversionApi );
  238. removeTableSectionIfEmpty( 'tbody', viewTable, conversionApi );
  239. }, { priority: 'higher' } );
  240. }
  241. // Converts a given {@link module:engine/view/element~Element} to a table widget:
  242. // * Adds a {@link module:engine/view/element~Element#_setCustomProperty custom property} allowing to recognize the table widget element.
  243. // * Calls the {@link module:widget/utils~toWidget} function with the proper element's label creator.
  244. //
  245. // @param {module:engine/view/element~Element} viewElement
  246. // @param {module:engine/view/downcastwriter~DowncastWriter} writer An instance of the view writer.
  247. // @param {String} label The element's label. It will be concatenated with the table `alt` attribute if one is present.
  248. // @returns {module:engine/view/element~Element}
  249. function toTableWidget( viewElement, writer ) {
  250. writer.setCustomProperty( 'table', true, viewElement );
  251. return toWidget( viewElement, writer, { hasSelectionHandle: true } );
  252. }
  253. // Renames an existing table cell in the view to a given element name.
  254. //
  255. // **Note** This method will not do anything if a view table cell has not been converted yet.
  256. //
  257. // @param {module:engine/model/element~Element} tableCell
  258. // @param {String} desiredCellElementName
  259. // @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi
  260. // @param {Boolean} asWidget
  261. function renameViewTableCell( tableCell, desiredCellElementName, conversionApi, asWidget ) {
  262. const viewWriter = conversionApi.writer;
  263. const viewCell = conversionApi.mapper.toViewElement( tableCell );
  264. // View cell might be not yet converted - skip it as it will be properly created by cell converter later on.
  265. if ( !viewCell ) {
  266. return;
  267. }
  268. let renamedCell;
  269. if ( asWidget ) {
  270. const editable = viewWriter.createEditableElement( desiredCellElementName, viewCell.getAttributes() );
  271. renamedCell = toWidgetEditable( editable, viewWriter );
  272. setHighlightHandling(
  273. renamedCell,
  274. viewWriter,
  275. ( element, descriptor, writer ) => writer.addClass( normalizeToArray( descriptor.classes ), element ),
  276. ( element, descriptor, writer ) => writer.removeClass( normalizeToArray( descriptor.classes ), element )
  277. );
  278. viewWriter.insert( viewWriter.createPositionAfter( viewCell ), renamedCell );
  279. viewWriter.move( viewWriter.createRangeIn( viewCell ), viewWriter.createPositionAt( renamedCell, 0 ) );
  280. viewWriter.remove( viewWriter.createRangeOn( viewCell ) );
  281. } else {
  282. renamedCell = viewWriter.rename( desiredCellElementName, viewCell );
  283. }
  284. conversionApi.mapper.unbindViewElement( viewCell );
  285. conversionApi.mapper.bindElements( tableCell, renamedCell );
  286. }
  287. function normalizeToArray( classes ) {
  288. return Array.isArray( classes ) ? classes : [ classes ];
  289. }
  290. // Renames a table cell element in the view according to its location in the table.
  291. //
  292. // @param {module:table/tablewalker~TableSlot} tableSlot
  293. // @param {{headingColumns, headingRows}} tableAttributes
  294. // @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi
  295. // @param {Boolean} asWidget
  296. function renameViewTableCellIfRequired( tableSlot, tableAttributes, conversionApi, asWidget ) {
  297. const { cell } = tableSlot;
  298. // Check whether current columnIndex is overlapped by table cells from previous rows.
  299. const desiredCellElementName = getCellElementName( tableSlot, tableAttributes );
  300. const viewCell = conversionApi.mapper.toViewElement( cell );
  301. // If in single change we're converting attribute changes and inserting cell the table cell might not be inserted into view
  302. // because of child conversion is done after parent.
  303. if ( viewCell && viewCell.name !== desiredCellElementName ) {
  304. renameViewTableCell( cell, desiredCellElementName, conversionApi, asWidget );
  305. }
  306. }
  307. // Creates a table cell element in the view.
  308. //
  309. // @param {module:table/tablewalker~TableSlot} tableSlot
  310. // @param {module:engine/view/position~Position} insertPosition
  311. // @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi
  312. function createViewTableCellElement( tableSlot, tableAttributes, insertPosition, conversionApi, options ) {
  313. const asWidget = options && options.asWidget;
  314. const cellElementName = getCellElementName( tableSlot, tableAttributes );
  315. const cellElement = asWidget ?
  316. toWidgetEditable( conversionApi.writer.createEditableElement( cellElementName ), conversionApi.writer ) :
  317. conversionApi.writer.createContainerElement( cellElementName );
  318. if ( asWidget ) {
  319. setHighlightHandling(
  320. cellElement,
  321. conversionApi.writer,
  322. ( element, descriptor, writer ) => writer.addClass( normalizeToArray( descriptor.classes ), element ),
  323. ( element, descriptor, writer ) => writer.removeClass( normalizeToArray( descriptor.classes ), element )
  324. );
  325. }
  326. const tableCell = tableSlot.cell;
  327. const firstChild = tableCell.getChild( 0 );
  328. const isSingleParagraph = tableCell.childCount === 1 && firstChild.name === 'paragraph';
  329. conversionApi.writer.insert( insertPosition, cellElement );
  330. if ( isSingleParagraph && !hasAnyAttribute( firstChild ) ) {
  331. const innerParagraph = tableCell.getChild( 0 );
  332. const paragraphInsertPosition = conversionApi.writer.createPositionAt( cellElement, 'end' );
  333. conversionApi.consumable.consume( innerParagraph, 'insert' );
  334. if ( options.asWidget ) {
  335. // Use display:inline-block to force Chrome/Safari to limit text mutations to this element.
  336. // See #6062.
  337. const fakeParagraph = conversionApi.writer.createContainerElement( 'span', { style: 'display:inline-block' } );
  338. conversionApi.mapper.bindElements( innerParagraph, fakeParagraph );
  339. conversionApi.writer.insert( paragraphInsertPosition, fakeParagraph );
  340. conversionApi.mapper.bindElements( tableCell, cellElement );
  341. } else {
  342. conversionApi.mapper.bindElements( tableCell, cellElement );
  343. conversionApi.mapper.bindElements( innerParagraph, cellElement );
  344. }
  345. } else {
  346. conversionApi.mapper.bindElements( tableCell, cellElement );
  347. }
  348. }
  349. // Creates a `<tr>` view element.
  350. //
  351. // @param {module:engine/view/element~Element} tableRow
  352. // @param {Number} rowIndex
  353. // @param {module:engine/view/element~Element} tableSection
  354. // @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi
  355. // @returns {module:engine/view/element~Element}
  356. function createTr( tableRow, rowIndex, tableSection, conversionApi ) {
  357. // Will always consume since we're converting <tableRow> element from a parent <table>.
  358. conversionApi.consumable.consume( tableRow, 'insert' );
  359. const trElement = conversionApi.writer.createContainerElement( 'tr' );
  360. conversionApi.mapper.bindElements( tableRow, trElement );
  361. const headingRows = tableRow.parent.getAttribute( 'headingRows' ) || 0;
  362. const offset = headingRows > 0 && rowIndex >= headingRows ? rowIndex - headingRows : rowIndex;
  363. const position = conversionApi.writer.createPositionAt( tableSection, offset );
  364. conversionApi.writer.insert( position, trElement );
  365. return trElement;
  366. }
  367. // Returns `th` for heading cells and `td` for other cells for the current table walker value.
  368. //
  369. // @param {module:table/tablewalker~TableSlot} tableSlot
  370. // @param {{headingColumns, headingRows}} tableAttributes
  371. // @returns {String}
  372. function getCellElementName( tableSlot, tableAttributes ) {
  373. const { row, column } = tableSlot;
  374. const { headingColumns, headingRows } = tableAttributes;
  375. // Column heading are all tableCells in the first `columnHeading` rows.
  376. const isColumnHeading = headingRows && headingRows > row;
  377. // So a whole row gets <th> element.
  378. if ( isColumnHeading ) {
  379. return 'th';
  380. }
  381. // Row heading are tableCells which columnIndex is lower then headingColumns.
  382. const isRowHeading = headingColumns && headingColumns > column;
  383. return isRowHeading ? 'th' : 'td';
  384. }
  385. // Returns the table section name for the current table walker value.
  386. //
  387. // @param {Number} row
  388. // @param {{headingColumns, headingRows}} tableAttributes
  389. // @returns {String}
  390. function getSectionName( row, tableAttributes ) {
  391. return row < tableAttributes.headingRows ? 'thead' : 'tbody';
  392. }
  393. // Creates or returns an existing `<tbody>` or `<thead>` element with caching.
  394. //
  395. // @param {String} sectionName
  396. // @param {module:engine/view/element~Element} viewTable
  397. // @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi
  398. // @param {Object} cachedTableSection An object that stores cached elements.
  399. // @returns {module:engine/view/containerelement~ContainerElement}
  400. function getOrCreateTableSection( sectionName, viewTable, conversionApi ) {
  401. const viewTableSection = getExistingTableSectionElement( sectionName, viewTable );
  402. return viewTableSection ? viewTableSection : createTableSection( sectionName, viewTable, conversionApi );
  403. }
  404. // Finds an existing `<tbody>` or `<thead>` element or returns undefined.
  405. //
  406. // @param {String} sectionName
  407. // @param {module:engine/view/element~Element} tableElement
  408. // @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi
  409. function getExistingTableSectionElement( sectionName, tableElement ) {
  410. for ( const tableSection of tableElement.getChildren() ) {
  411. if ( tableSection.name == sectionName ) {
  412. return tableSection;
  413. }
  414. }
  415. }
  416. // Creates a table section at the end of the table.
  417. //
  418. // @param {String} sectionName
  419. // @param {module:engine/view/element~Element} tableElement
  420. // @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi
  421. // @returns {module:engine/view/containerelement~ContainerElement}
  422. function createTableSection( sectionName, tableElement, conversionApi ) {
  423. const tableChildElement = conversionApi.writer.createContainerElement( sectionName );
  424. const insertPosition = conversionApi.writer.createPositionAt( tableElement, sectionName == 'tbody' ? 'end' : 0 );
  425. conversionApi.writer.insert( insertPosition, tableChildElement );
  426. return tableChildElement;
  427. }
  428. // Removes an existing `<tbody>` or `<thead>` element if it is empty.
  429. //
  430. // @param {String} sectionName
  431. // @param {module:engine/view/element~Element} tableElement
  432. // @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi
  433. function removeTableSectionIfEmpty( sectionName, tableElement, conversionApi ) {
  434. const tableSection = getExistingTableSectionElement( sectionName, tableElement );
  435. if ( tableSection && tableSection.childCount === 0 ) {
  436. conversionApi.writer.remove( conversionApi.writer.createRangeOn( tableSection ) );
  437. }
  438. }
  439. // Moves view table rows associated with passed model rows to the provided table section element.
  440. //
  441. // **Note**: This method will skip not converted table rows.
  442. //
  443. // @param {Array.<module:engine/model/element~Element>} rowsToMove
  444. // @param {module:engine/view/element~Element} viewTableSection
  445. // @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi
  446. // @param {Number|'end'|'before'|'after'} offset Offset or one of the flags.
  447. function moveViewRowsToTableSection( rowsToMove, viewTableSection, conversionApi, offset ) {
  448. for ( const tableRow of rowsToMove ) {
  449. const viewTableRow = conversionApi.mapper.toViewElement( tableRow );
  450. // View table row might be not yet converted - skip it as it will be properly created by cell converter later on.
  451. if ( viewTableRow ) {
  452. conversionApi.writer.move(
  453. conversionApi.writer.createRangeOn( viewTableRow ),
  454. conversionApi.writer.createPositionAt( viewTableSection, offset )
  455. );
  456. }
  457. }
  458. }
  459. // Finds a '<table>' element inside the `<figure>` widget.
  460. //
  461. // @param {module:engine/view/element~Element} viewFigure
  462. function getViewTable( viewFigure ) {
  463. for ( const child of viewFigure.getChildren() ) {
  464. if ( child.name === 'table' ) {
  465. return child;
  466. }
  467. }
  468. }
  469. // Checks if an element has any attributes set.
  470. //
  471. // @param {module:engine/model/element~Element element
  472. // @returns {Boolean}
  473. function hasAnyAttribute( element ) {
  474. return !![ ...element.getAttributeKeys() ].length;
  475. }