|
71 | 71 | * @param {module:engine/controller/editingcontroller~EditingController} editing
|
72 | 72 | */
|
73 | 73 | export default function injectTableCellPostFixer( model, editing ) {
|
74 |
| - editing.view.document.registerPostFixer( writer => tableCellPostFixer( writer, model, editing.mapper ) ); |
| 74 | + editing.view.document.registerPostFixer( writer => tableCellPostFixer( writer, model, editing.mapper, editing.view ) ); |
75 | 75 | }
|
76 | 76 |
|
77 | 77 | // The table cell post-fixer.
|
78 | 78 | //
|
79 | 79 | // @param {module:engine/view/writer~Writer} writer
|
80 | 80 | // @param {module:engine/model/model~Model} model
|
81 | 81 | // @param {module:engine/conversion/mapper~Mapper} mapper
|
82 |
| -function tableCellPostFixer( writer, model, mapper ) { |
83 |
| - const changes = model.document.differ.getChanges(); |
| 82 | +function tableCellPostFixer( writer, model, mapper, view ) { |
84 | 83 | let wasFixed = false;
|
85 | 84 |
|
86 |
| - // While this is view post fixer only nodes that changed are worth investigating. |
87 |
| - for ( const entry of changes ) { |
88 |
| - // Attribute change - check if it is single paragraph inside table cell that has attributes changed. |
89 |
| - if ( entry.type == 'attribute' && entry.range.start.parent.name == 'tableCell' ) { |
90 |
| - const tableCell = entry.range.start.parent; |
91 |
| - |
92 |
| - if ( tableCell.childCount === 1 ) { |
93 |
| - const singleChild = tableCell.getChild( 0 ); |
94 |
| - const renameTo = Array.from( singleChild.getAttributes() ).length ? 'p' : 'span'; |
95 |
| - |
96 |
| - wasFixed = renameParagraphIfDifferent( singleChild, renameTo, writer, model, mapper ) || wasFixed; |
97 |
| - } |
98 |
| - } else { |
99 |
| - // Check all nodes inside table cell on insert/remove operations (also other blocks). |
100 |
| - const parent = entry.position && entry.position.parent; |
101 |
| - |
102 |
| - if ( parent && parent.is( 'tableCell' ) ) { |
103 |
| - const renameTo = parent.childCount > 1 ? 'p' : 'span'; |
104 |
| - |
105 |
| - for ( const child of parent.getChildren() ) { |
106 |
| - wasFixed = renameParagraphIfDifferent( child, renameTo, writer, model, mapper ) || wasFixed; |
107 |
| - } |
108 |
| - } |
109 |
| - } |
| 85 | + const elementsToCheck = getElementsToCheck( view ); |
| 86 | + |
| 87 | + for ( const element of elementsToCheck ) { |
| 88 | + wasFixed = ensureProperElementName( element, mapper, writer ) || wasFixed; |
| 89 | + } |
| 90 | + |
| 91 | + // Selection in the view might not be updated to renamed elements. Happens mostly when other feature inserts paragraph to the table cell |
| 92 | + // (ie. when deleting table cell contents) and sets selection to it while table-post fixer changes view <p> to <span> element. |
| 93 | + // The view.selection would have outdated nodes. |
| 94 | + if ( wasFixed ) { |
| 95 | + updateRangesInViewSelection( model.document.selection, mapper, writer ); |
110 | 96 | }
|
111 | 97 |
|
112 | 98 | return wasFixed;
|
113 | 99 | }
|
114 | 100 |
|
115 |
| -// Renames associated view element to a desired one. It will only rename if: |
116 |
| -// - model element is a paragraph |
117 |
| -// - view element is converted (mapped) |
118 |
| -// - view element has different name then requested. |
| 101 | +// Returns view elements changed in current view.change() block. |
119 | 102 | //
|
120 |
| -// @param {module:engine/model/element~Element} modelElement |
121 |
| -// @param {String} desiredElementName |
122 |
| -// @param {module:engine/view/writer~Writer} writer |
123 |
| -// @param {module:engine/model/model~Model} model |
124 |
| -// @param {module:engine/conversion/mapper~Mapper} mapper |
125 |
| -function renameParagraphIfDifferent( modelElement, desiredElementName, writer, model, mapper ) { |
126 |
| - // Only rename paragraph elements. |
127 |
| - if ( !modelElement.is( 'paragraph' ) ) { |
128 |
| - return false; |
129 |
| - } |
| 103 | +// **Note**: Currently it uses private property of the view: _renderer to get changed view elements to check. |
| 104 | +// |
| 105 | +// @param {module:engine/view/view~View} view |
| 106 | +function getElementsToCheck( view ) { |
| 107 | + const elementsWithChangedAttributes = Array.from( view._renderer.markedAttributes ) |
| 108 | + .filter( el => !!el.parent ) |
| 109 | + .filter( isSpanOrP ) |
| 110 | + .filter( el => isTdOrTh( el.parent ) ); |
| 111 | + |
| 112 | + const changedChildren = Array.from( view._renderer.markedChildren ) |
| 113 | + .filter( el => !!el.parent ) |
| 114 | + .filter( isTdOrTh ) |
| 115 | + .reduce( ( prev, element ) => { |
| 116 | + const childrenToCheck = Array.from( element.getChildren() ).filter( isSpanOrP ); |
| 117 | + |
| 118 | + return [ ...prev, ...childrenToCheck ]; |
| 119 | + }, [] ); |
| 120 | + |
| 121 | + return [ ...elementsWithChangedAttributes, ...changedChildren ]; |
| 122 | +} |
130 | 123 |
|
131 |
| - const viewElement = mapper.toViewElement( modelElement ); |
| 124 | +// This method checks if view element for model's <paragraph> was properly converter. |
| 125 | +// Paragraph should be either |
| 126 | +// - span: for single paragraph with no attributes. |
| 127 | +// - p : in other cases. |
| 128 | +function ensureProperElementName( currentViewElement, mapper, writer ) { |
| 129 | + const modelParagraph = mapper.toModelElement( currentViewElement ); |
| 130 | + const expectedViewElementName = getExpectedElementName( modelParagraph.parent, modelParagraph ); |
132 | 131 |
|
133 |
| - // Only rename converted elements which aren't desired ones. |
134 |
| - if ( !viewElement || viewElement.name === desiredElementName ) { |
135 |
| - return false; |
136 |
| - } |
| 132 | + if ( currentViewElement.name !== expectedViewElementName ) { |
| 133 | + // Unbind current view element as it should be cleared from mapper. |
| 134 | + mapper.unbindViewElement( currentViewElement ); |
137 | 135 |
|
138 |
| - // After renaming element in the view by a post-fixer the selection would have references to the previous element. |
139 |
| - const selection = model.document.selection; |
140 |
| - const shouldFixSelection = checkSelectionForRenamedElement( selection, modelElement ); |
| 136 | + const renamedViewElement = writer.rename( expectedViewElementName, currentViewElement ); |
141 | 137 |
|
142 |
| - // Unbind current view element as it should be cleared from mapper. |
143 |
| - mapper.unbindViewElement( viewElement ); |
144 |
| - const renamedViewElement = writer.rename( desiredElementName, viewElement ); |
145 |
| - // Bind paragraph inside table cell to the renamed view element. |
146 |
| - mapper.bindElements( modelElement, renamedViewElement ); |
| 138 | + // Bind paragraph inside table cell to the renamed view element. |
| 139 | + mapper.bindElements( modelParagraph, renamedViewElement ); |
147 | 140 |
|
148 |
| - if ( shouldFixSelection ) { |
149 |
| - // Re-create view selection based on model selection. |
150 |
| - updateRangesInViewSelection( selection, mapper, writer ); |
| 141 | + return true; |
151 | 142 | }
|
152 | 143 |
|
153 |
| - return true; |
| 144 | + return false; |
154 | 145 | }
|
155 | 146 |
|
156 |
| -// Checks if model selection contains renamed element. |
| 147 | +// Expected view element name depends on model elements: |
| 148 | +// - <paragraph> with any attribute set should be rendered as <p> |
| 149 | +// - all <paragraphs> in <tableCell> that has more then one children should be rendered as <p> |
| 150 | +// - an only <paragraph> child with no attributes should be rendered as <span> |
157 | 151 | //
|
158 |
| -// @param {module:engine/model/selection~Selection} selection |
159 |
| -// @param {module:engine/model/element~Element} modelElement |
160 |
| -// @returns {boolean} |
161 |
| -function checkSelectionForRenamedElement( selection, modelElement ) { |
162 |
| - return !![ ...selection.getSelectedBlocks() ].find( block => block === modelElement ); |
| 152 | +// @param {module:engine/model/element~Element} tableCell |
| 153 | +// @param {module:engine/model/element~Element} paragraph |
| 154 | +// @returns {String} |
| 155 | +function getExpectedElementName( tableCell, paragraph ) { |
| 156 | + const isOnlyChild = tableCell.childCount > 1; |
| 157 | + const hasAttributes = !![ ...paragraph.getAttributes() ].length; |
| 158 | + |
| 159 | + return ( isOnlyChild || hasAttributes ) ? 'p' : 'span'; |
163 | 160 | }
|
164 | 161 |
|
165 |
| -// Re-create view selection from model selection. |
| 162 | +// Method to filter out <span> and <p> elements. |
166 | 163 | //
|
167 |
| -// @param {module:engine/model/selection~Selection} selection |
168 |
| -// @param {module:engine/view/writer~Writer} writer |
169 |
| -// @param {module:engine/conversion/mapper~Mapper} mapper |
| 164 | +// @param {module:engine/view/element~Element} element |
| 165 | +function isSpanOrP( element ) { |
| 166 | + return element.is( 'p' ) || element.is( 'span' ); |
| 167 | +} |
| 168 | + |
| 169 | +// Method to filter out <td> and <th> elements. |
| 170 | +// |
| 171 | +// @param {module:engine/view/element~Element} element |
| 172 | +function isTdOrTh( element ) { |
| 173 | + return element.is( 'td' ) || element.is( 'th' ); |
| 174 | +} |
| 175 | + |
| 176 | +// Resets view selections based on model selection. |
170 | 177 | function updateRangesInViewSelection( selection, mapper, writer ) {
|
171 | 178 | const fixedRanges = Array.from( selection.getRanges() )
|
172 | 179 | .map( range => mapper.toViewRange( range ) );
|
|
0 commit comments