Skip to content
This repository was archived by the owner on Jun 26, 2020. It is now read-only.

Commit efc53c9

Browse files
author
Piotr Jasiun
authored
Merge pull request #132 from ckeditor/t/130
Other: The table cell view post-fixer should use changed elements from the view to run fixes. Closes #130.
2 parents 1eb5d6d + 2fbcc91 commit efc53c9

File tree

3 files changed

+175
-96
lines changed

3 files changed

+175
-96
lines changed

src/converters/tablecell-post-fixer.js

Lines changed: 75 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -71,102 +71,109 @@
7171
* @param {module:engine/controller/editingcontroller~EditingController} editing
7272
*/
7373
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 ) );
7575
}
7676

7777
// The table cell post-fixer.
7878
//
7979
// @param {module:engine/view/writer~Writer} writer
8080
// @param {module:engine/model/model~Model} model
8181
// @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 ) {
8483
let wasFixed = false;
8584

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 );
11096
}
11197

11298
return wasFixed;
11399
}
114100

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.
119102
//
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+
}
130123

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 );
132131

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 );
137135

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 );
141137

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 );
147140

148-
if ( shouldFixSelection ) {
149-
// Re-create view selection based on model selection.
150-
updateRangesInViewSelection( selection, mapper, writer );
141+
return true;
151142
}
152143

153-
return true;
144+
return false;
154145
}
155146

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>
157151
//
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';
163160
}
164161

165-
// Re-create view selection from model selection.
162+
// Method to filter out <span> and <p> elements.
166163
//
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.
170177
function updateRangesInViewSelection( selection, mapper, writer ) {
171178
const fixedRanges = Array.from( selection.getRanges() )
172179
.map( range => mapper.toViewRange( range ) );

tests/_utils/utils.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import {
1515
} from '../../src/converters/downcast';
1616
import upcastTable, { upcastTableCell } from '../../src/converters/upcasttable';
1717

18+
const WIDGET_TABLE_CELL_CLASS = 'ck-editor__editable ck-editor__nested-editable';
19+
1820
/**
1921
* Returns a model representation of a table shorthand notation:
2022
*
@@ -264,7 +266,7 @@ function makeRows( tableData, options ) {
264266
const attributes = isObject ? tableCellData : {};
265267

266268
if ( asWidget ) {
267-
attributes.class = 'ck-editor__editable ck-editor__nested-editable';
269+
attributes.class = WIDGET_TABLE_CELL_CLASS + ( attributes.class ? ` ${ attributes.class }` : '' );
268270
attributes.contenteditable = 'true';
269271
}
270272

0 commit comments

Comments
 (0)