8
0

tablecell-post-fixer.js 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. /**
  2. * @license Copyright (c) 2003-2019, 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/tablecell-post-fixer
  7. */
  8. /**
  9. * Injects a table cell post-fixer into the editing controller.
  10. *
  11. * The role of the table cell post-fixer is to ensure that the table cell contents in the editing view are properly converted.
  12. *
  13. * This post-fixer will ensure that after model changes in the editing view:
  14. * * single paragraphs are rendered as `<span>
  15. * * single paragraphs with one or more attributes are rendered as `<p>`
  16. * * single paragraphs in table cell with other block elements are rendered as `<p>`
  17. * * paragraphs in table cells with other block elements (including other paragraphs) are rendered as `<p>`.
  18. *
  19. * In the model each table cell has always at least one block element inside. If no other block was defined (empty table cell) the table
  20. * feature will insert empty `<paragraph>`. Similarly text nodes will be wrapped in paragraphs. Rendering in the data pipeline differs
  21. * from rendering in the editing pipeline - text nodes in single `<paragraph>` are rendered in the data pipeline as direct children
  22. * of the `<td>` or `<th>` elements. In other cases `<paragraph>` elements are rendered as `<p>` blocks.
  23. *
  24. * To ensure proper mappings between model and view elements and positions in the editing pipeline the table feature will always render
  25. * an element in the view: `<span>` for single or empty `<paragraph>` and `<p>` otherwise.
  26. *
  27. * Example:
  28. *
  29. * <table>
  30. * <tableRow>
  31. * <tableCell><paragraph></paragraph></tableCell>
  32. * <tableCell><paragraph>foo</paragraph></tableCell>
  33. * <tableCell><paragraph baz="bar">foo</paragraph></tableCell>
  34. * <tableCell><heading2>bar</heading2><paragraph>baz</paragraph></tableCell>
  35. * </tableRow>
  36. * </table>
  37. *
  38. * The editor will render in the data pipeline:
  39. *
  40. * <figure>
  41. * <table>
  42. * <tbody>
  43. * <tr>
  44. * <td></td>
  45. * <td>foo</td>
  46. * <td><p baz="bar">foo</p></td>
  47. * <td><h3>bar</h3><p>baz</p></td>
  48. * </tr>
  49. * </tbody>
  50. * </table>
  51. * </figure>
  52. *
  53. * and in the editing view (without widget markup):
  54. *
  55. * <figure>
  56. * <table>
  57. * <tbody>
  58. * <tr>
  59. * <td><span></span></td>
  60. * <td><span>foo</span></td>
  61. * <td><p baz="bar">foo</p></td>
  62. * <td><h3>bar</h3><p>baz</p></td>
  63. * </tr>
  64. * </tbody>
  65. * </table>
  66. * </figure>
  67. *
  68. * @param {module:engine/model/model~Model} model
  69. * @param {module:engine/controller/editingcontroller~EditingController} editing
  70. */
  71. export default function injectTableCellPostFixer( model, editing ) {
  72. editing.view.document.registerPostFixer( writer => tableCellPostFixer( writer, model, editing.mapper, editing.view ) );
  73. }
  74. // The table cell post-fixer.
  75. //
  76. // @param {module:engine/view/writer~Writer} writer
  77. // @param {module:engine/model/model~Model} model
  78. // @param {module:engine/conversion/mapper~Mapper} mapper
  79. function tableCellPostFixer( writer, model, mapper, view ) {
  80. let wasFixed = false;
  81. const elementsToCheck = getElementsToCheck( view );
  82. for ( const element of elementsToCheck ) {
  83. wasFixed = ensureProperElementName( element, mapper, writer ) || wasFixed;
  84. }
  85. // Selection in the view might not be updated to renamed elements. Happens mostly when other feature inserts paragraph to the table cell
  86. // (ie. when deleting table cell contents) and sets selection to it while table-post fixer changes view <p> to <span> element.
  87. // The view.selection would have outdated nodes.
  88. if ( wasFixed ) {
  89. updateRangesInViewSelection( model.document.selection, mapper, writer );
  90. }
  91. return wasFixed;
  92. }
  93. // Returns view elements changed in current view.change() block.
  94. //
  95. // **Note**: Currently it uses private property of the view: _renderer to get changed view elements to check.
  96. //
  97. // @param {module:engine/view/view~View} view
  98. function getElementsToCheck( view ) {
  99. const elementsWithChangedAttributes = Array.from( view._renderer.markedAttributes )
  100. .filter( el => !!el.parent )
  101. .filter( isSpanOrP )
  102. .filter( el => isTdOrTh( el.parent ) );
  103. const changedChildren = Array.from( view._renderer.markedChildren )
  104. .filter( el => !!el.parent )
  105. .filter( isTdOrTh )
  106. .reduce( ( prev, element ) => {
  107. const childrenToCheck = Array.from( element.getChildren() ).filter( isSpanOrP );
  108. return [ ...prev, ...childrenToCheck ];
  109. }, [] );
  110. return [ ...elementsWithChangedAttributes, ...changedChildren ];
  111. }
  112. // This method checks if view element for model's <paragraph> was properly converter.
  113. // Paragraph should be either
  114. // - span: for single paragraph with no attributes.
  115. // - p : in other cases.
  116. function ensureProperElementName( currentViewElement, mapper, writer ) {
  117. // This situation may happen if a view element was changed and removed at the same time.
  118. // In this case, the view element is already unbound so the post-fixer would crash.
  119. if ( !currentViewElement.root.is( 'rootElement' ) ) {
  120. return false;
  121. }
  122. const modelParagraph = mapper.toModelElement( currentViewElement );
  123. const expectedViewElementName = getExpectedElementName( modelParagraph.parent, modelParagraph );
  124. if ( currentViewElement.name !== expectedViewElementName ) {
  125. // Unbind current view element as it should be cleared from mapper.
  126. mapper.unbindViewElement( currentViewElement );
  127. const renamedViewElement = writer.rename( expectedViewElementName, currentViewElement );
  128. // Bind paragraph inside table cell to the renamed view element.
  129. mapper.bindElements( modelParagraph, renamedViewElement );
  130. return true;
  131. }
  132. return false;
  133. }
  134. // Expected view element name depends on model elements:
  135. // - <paragraph> with any attribute set should be rendered as <p>
  136. // - all <paragraphs> in <tableCell> that has more then one children should be rendered as <p>
  137. // - an only <paragraph> child with no attributes should be rendered as <span>
  138. //
  139. // @param {module:engine/model/element~Element} tableCell
  140. // @param {module:engine/model/element~Element} paragraph
  141. // @returns {String}
  142. function getExpectedElementName( tableCell, paragraph ) {
  143. const isOnlyChild = tableCell.childCount > 1;
  144. const hasAttributes = !![ ...paragraph.getAttributes() ].length;
  145. return ( isOnlyChild || hasAttributes ) ? 'p' : 'span';
  146. }
  147. // Method to filter out <span> and <p> elements.
  148. //
  149. // @param {module:engine/view/element~Element} element
  150. function isSpanOrP( element ) {
  151. return element.is( 'p' ) || element.is( 'span' );
  152. }
  153. // Method to filter out <td> and <th> elements.
  154. //
  155. // @param {module:engine/view/element~Element} element
  156. function isTdOrTh( element ) {
  157. return element.is( 'td' ) || element.is( 'th' );
  158. }
  159. // Resets view selections based on model selection.
  160. function updateRangesInViewSelection( selection, mapper, writer ) {
  161. const fixedRanges = Array.from( selection.getRanges() )
  162. .map( range => mapper.toViewRange( range ) );
  163. writer.setSelection( fixedRanges, { backward: selection.isBackward } );
  164. }