/** * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ /** * @module table/converters/tablecell-post-fixer */ /** * Injects a table cell post-fixer into the editing controller. * * The role of the table cell post-fixer is to ensure that the table cell contents in the editing view are properly converted. * * This post-fixer will ensure that after model changes in the editing view: * * single paragraphs are rendered as ` * * single paragraphs with one or more attributes are rendered as `

` * * single paragraphs in table cell with other block elements are rendered as `

` * * paragraphs in table cells with other block elements (including other paragraphs) are rendered as `

`. * * 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 * feature will insert empty ``. Similarly text nodes will be wrapped in paragraphs. Rendering in the data pipeline differs * from rendering in the editing pipeline - text nodes in single `` are rendered in the data pipeline as direct children * of the `` or `` elements. In other cases `` elements are rendered as `

` blocks. * * To ensure proper mappings between model and view elements and positions in the editing pipeline the table feature will always render * an element in the view: `` for single or empty `` and `

` otherwise. * * Example: * * * * * foo * foo * barbaz * *
* * The editor will render in the data pipeline: * *

* * * * * * * * * *
foo

foo

bar

baz

*
* * and in the editing view (without widget markup): * *
* * * * * * * * * *
foo

foo

bar

baz

*
* * @param {module:engine/model/model~Model} model * @param {module:engine/controller/editingcontroller~EditingController} editing */ export default function injectTableCellPostFixer( model, editing ) { editing.view.document.registerPostFixer( writer => tableCellPostFixer( writer, model, editing.mapper, editing.view ) ); } // The table cell post-fixer. // // @param {module:engine/view/writer~Writer} writer // @param {module:engine/model/model~Model} model // @param {module:engine/conversion/mapper~Mapper} mapper function tableCellPostFixer( writer, model, mapper, view ) { let wasFixed = false; const elementsToCheck = getElementsToCheck( view ); for ( const element of elementsToCheck ) { wasFixed = ensureProperElementName( element, mapper, writer ) || wasFixed; } // Selection in the view might not be updated to renamed elements. Happens mostly when other feature inserts paragraph to the table cell // (ie. when deleting table cell contents) and sets selection to it while table-post fixer changes view

to element. // The view.selection would have outdated nodes. if ( wasFixed ) { updateRangesInViewSelection( model.document.selection, mapper, writer ); } return wasFixed; } // Returns view elements changed in current view.change() block. // // **Note**: Currently it uses private property of the view: _renderer to get changed view elements to check. // // @param {module:engine/view/view~View} view function getElementsToCheck( view ) { const elementsWithChangedAttributes = Array.from( view._renderer.markedAttributes ) .filter( el => !!el.parent ) .filter( isSpanOrP ) .filter( el => isTdOrTh( el.parent ) ); const changedChildren = Array.from( view._renderer.markedChildren ) .filter( el => !!el.parent ) .filter( isTdOrTh ) .reduce( ( prev, element ) => { const childrenToCheck = Array.from( element.getChildren() ).filter( isSpanOrP ); return [ ...prev, ...childrenToCheck ]; }, [] ); return [ ...elementsWithChangedAttributes, ...changedChildren ]; } // This method checks if view element for model's was properly converter. // Paragraph should be either // - span: for single paragraph with no attributes. // - p : in other cases. function ensureProperElementName( currentViewElement, mapper, writer ) { // This situation may happen if a view element was changed and removed at the same time. // In this case, the view element is already unbound so the post-fixer would crash. if ( !currentViewElement.root.is( 'rootElement' ) ) { return false; } const modelParagraph = mapper.toModelElement( currentViewElement ); const expectedViewElementName = getExpectedElementName( modelParagraph.parent, modelParagraph ); if ( currentViewElement.name !== expectedViewElementName ) { // Unbind current view element as it should be cleared from mapper. mapper.unbindViewElement( currentViewElement ); const renamedViewElement = writer.rename( expectedViewElementName, currentViewElement ); // Bind paragraph inside table cell to the renamed view element. mapper.bindElements( modelParagraph, renamedViewElement ); return true; } return false; } // Expected view element name depends on model elements: // - with any attribute set should be rendered as

// - all in that has more then one children should be rendered as

// - an only child with no attributes should be rendered as // // @param {module:engine/model/element~Element} tableCell // @param {module:engine/model/element~Element} paragraph // @returns {String} function getExpectedElementName( tableCell, paragraph ) { const isOnlyChild = tableCell.childCount > 1; const hasAttributes = !![ ...paragraph.getAttributes() ].length; return ( isOnlyChild || hasAttributes ) ? 'p' : 'span'; } // Method to filter out and

elements. // // @param {module:engine/view/element~Element} element function isSpanOrP( element ) { return element.is( 'p' ) || element.is( 'span' ); } // Method to filter out and elements. // // @param {module:engine/view/element~Element} element function isTdOrTh( element ) { return element.is( 'td' ) || element.is( 'th' ); } // Resets view selections based on model selection. function updateRangesInViewSelection( selection, mapper, writer ) { const fixedRanges = Array.from( selection.getRanges() ) .map( range => mapper.toViewRange( range ) ); writer.setSelection( fixedRanges, { backward: selection.isBackward } ); }