Kaynağa Gözat

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.
Piotr Jasiun 7 yıl önce
ebeveyn
işleme
30b89a840a

+ 75 - 68
packages/ckeditor5-table/src/converters/tablecell-post-fixer.js

@@ -71,7 +71,7 @@
  * @param {module:engine/controller/editingcontroller~EditingController} editing
  * @param {module:engine/controller/editingcontroller~EditingController} editing
  */
  */
 export default function injectTableCellPostFixer( model, editing ) {
 export default function injectTableCellPostFixer( model, editing ) {
-	editing.view.document.registerPostFixer( writer => tableCellPostFixer( writer, model, editing.mapper ) );
+	editing.view.document.registerPostFixer( writer => tableCellPostFixer( writer, model, editing.mapper, editing.view ) );
 }
 }
 
 
 // The table cell post-fixer.
 // The table cell post-fixer.
@@ -79,94 +79,101 @@ export default function injectTableCellPostFixer( model, editing ) {
 // @param {module:engine/view/writer~Writer} writer
 // @param {module:engine/view/writer~Writer} writer
 // @param {module:engine/model/model~Model} model
 // @param {module:engine/model/model~Model} model
 // @param {module:engine/conversion/mapper~Mapper} mapper
 // @param {module:engine/conversion/mapper~Mapper} mapper
-function tableCellPostFixer( writer, model, mapper ) {
-	const changes = model.document.differ.getChanges();
+function tableCellPostFixer( writer, model, mapper, view ) {
 	let wasFixed = false;
 	let wasFixed = false;
 
 
-	// While this is view post fixer only nodes that changed are worth investigating.
-	for ( const entry of changes ) {
-		// Attribute change - check if it is single paragraph inside table cell that has attributes changed.
-		if ( entry.type == 'attribute' && entry.range.start.parent.name == 'tableCell' ) {
-			const tableCell = entry.range.start.parent;
-
-			if ( tableCell.childCount === 1 ) {
-				const singleChild = tableCell.getChild( 0 );
-				const renameTo = Array.from( singleChild.getAttributes() ).length ? 'p' : 'span';
-
-				wasFixed = renameParagraphIfDifferent( singleChild, renameTo, writer, model, mapper ) || wasFixed;
-			}
-		} else {
-			// Check all nodes inside table cell on insert/remove operations (also other blocks).
-			const parent = entry.position && entry.position.parent;
-
-			if ( parent && parent.is( 'tableCell' ) ) {
-				const renameTo = parent.childCount > 1 ? 'p' : 'span';
-
-				for ( const child of parent.getChildren() ) {
-					wasFixed = renameParagraphIfDifferent( child, renameTo, writer, model, mapper ) || wasFixed;
-				}
-			}
-		}
+	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 <p> to <span> element.
+	// The view.selection would have outdated nodes.
+	if ( wasFixed ) {
+		updateRangesInViewSelection( model.document.selection, mapper, writer );
 	}
 	}
 
 
 	return wasFixed;
 	return wasFixed;
 }
 }
 
 
-// Renames associated view element to a desired one. It will only rename if:
-// - model element is a paragraph
-// - view element is converted (mapped)
-// - view element has different name then requested.
+// Returns view elements changed in current view.change() block.
 //
 //
-// @param {module:engine/model/element~Element} modelElement
-// @param {String} desiredElementName
-// @param {module:engine/view/writer~Writer} writer
-// @param {module:engine/model/model~Model} model
-// @param {module:engine/conversion/mapper~Mapper} mapper
-function renameParagraphIfDifferent( modelElement, desiredElementName, writer, model, mapper ) {
-	// Only rename paragraph elements.
-	if ( !modelElement.is( 'paragraph' ) ) {
-		return false;
-	}
+// **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 ];
+}
 
 
-	const viewElement = mapper.toViewElement( modelElement );
+// This method checks if view element for model's <paragraph> was properly converter.
+// Paragraph should be either
+// - span: for single paragraph with no attributes.
+// - p   : in other cases.
+function ensureProperElementName( currentViewElement, mapper, writer ) {
+	const modelParagraph = mapper.toModelElement( currentViewElement );
+	const expectedViewElementName = getExpectedElementName( modelParagraph.parent, modelParagraph );
 
 
-	// Only rename converted elements which aren't desired ones.
-	if ( !viewElement || viewElement.name === desiredElementName ) {
-		return false;
-	}
+	if ( currentViewElement.name !== expectedViewElementName ) {
+		// Unbind current view element as it should be cleared from mapper.
+		mapper.unbindViewElement( currentViewElement );
 
 
-	// After renaming element in the view by a post-fixer the selection would have references to the previous element.
-	const selection = model.document.selection;
-	const shouldFixSelection = checkSelectionForRenamedElement( selection, modelElement );
+		const renamedViewElement = writer.rename( expectedViewElementName, currentViewElement );
 
 
-	// Unbind current view element as it should be cleared from mapper.
-	mapper.unbindViewElement( viewElement );
-	const renamedViewElement = writer.rename( desiredElementName, viewElement );
-	// Bind paragraph inside table cell to the renamed view element.
-	mapper.bindElements( modelElement, renamedViewElement );
+		// Bind paragraph inside table cell to the renamed view element.
+		mapper.bindElements( modelParagraph, renamedViewElement );
 
 
-	if ( shouldFixSelection ) {
-		// Re-create view selection based on model selection.
-		updateRangesInViewSelection( selection, mapper, writer );
+		return true;
 	}
 	}
 
 
-	return true;
+	return false;
 }
 }
 
 
-// Checks if model selection contains renamed element.
+// Expected view element name depends on model elements:
+// - <paragraph> with any attribute set should be rendered as <p>
+// - all <paragraphs> in <tableCell> that has more then one children should be rendered as <p>
+// - an only <paragraph> child with no attributes should be rendered as <span>
 //
 //
-// @param {module:engine/model/selection~Selection} selection
-// @param {module:engine/model/element~Element} modelElement
-// @returns {boolean}
-function checkSelectionForRenamedElement( selection, modelElement ) {
-	return !![ ...selection.getSelectedBlocks() ].find( block => block === modelElement );
+// @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';
 }
 }
 
 
-// Re-create view selection from model selection.
+// Method to filter out <span> and <p> elements.
 //
 //
-// @param {module:engine/model/selection~Selection} selection
-// @param {module:engine/view/writer~Writer} writer
-// @param {module:engine/conversion/mapper~Mapper} mapper
+// @param {module:engine/view/element~Element} element
+function isSpanOrP( element ) {
+	return element.is( 'p' ) || element.is( 'span' );
+}
+
+// Method to filter out <td> and <th> 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 ) {
 function updateRangesInViewSelection( selection, mapper, writer ) {
 	const fixedRanges = Array.from( selection.getRanges() )
 	const fixedRanges = Array.from( selection.getRanges() )
 		.map( range => mapper.toViewRange( range ) );
 		.map( range => mapper.toViewRange( range ) );

+ 3 - 1
packages/ckeditor5-table/tests/_utils/utils.js

@@ -15,6 +15,8 @@ import {
 } from '../../src/converters/downcast';
 } from '../../src/converters/downcast';
 import upcastTable, { upcastTableCell } from '../../src/converters/upcasttable';
 import upcastTable, { upcastTableCell } from '../../src/converters/upcasttable';
 
 
+const WIDGET_TABLE_CELL_CLASS = 'ck-editor__editable ck-editor__nested-editable';
+
 /**
 /**
  * Returns a model representation of a table shorthand notation:
  * Returns a model representation of a table shorthand notation:
  *
  *
@@ -264,7 +266,7 @@ function makeRows( tableData, options ) {
 				const attributes = isObject ? tableCellData : {};
 				const attributes = isObject ? tableCellData : {};
 
 
 				if ( asWidget ) {
 				if ( asWidget ) {
-					attributes.class = 'ck-editor__editable ck-editor__nested-editable';
+					attributes.class = WIDGET_TABLE_CELL_CLASS + ( attributes.class ? ` ${ attributes.class }` : '' );
 					attributes.contenteditable = 'true';
 					attributes.contenteditable = 'true';
 				}
 				}
 
 

+ 97 - 27
packages/ckeditor5-table/tests/converters/tablecell-post-fixer.js

@@ -3,32 +3,38 @@
  * For licensing, see LICENSE.md.
  * For licensing, see LICENSE.md.
  */
  */
 
 
-import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
+/* globals document */
+
 import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
 import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
-import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
 
-import { defaultConversion, defaultSchema, formatTable, formattedViewTable, modelTable } from '../_utils/utils';
+import { defaultConversion, defaultSchema, formatTable, formattedViewTable, viewTable } from '../_utils/utils';
 import injectTableCellPostFixer from '../../src/converters/tablecell-post-fixer';
 import injectTableCellPostFixer from '../../src/converters/tablecell-post-fixer';
+import Range from '@ckeditor/ckeditor5-engine/src/model/range';
 
 
 import env from '@ckeditor/ckeditor5-utils/src/env';
 import env from '@ckeditor/ckeditor5-utils/src/env';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+import Position from '@ckeditor/ckeditor5-engine/src/model/position';
+import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
 
 
 describe( 'TableCell post-fixer', () => {
 describe( 'TableCell post-fixer', () => {
-	let editor, model, doc, root, viewDocument;
+	let editor, model, doc, root, view;
 
 
 	testUtils.createSinonSandbox();
 	testUtils.createSinonSandbox();
 
 
 	beforeEach( () => {
 	beforeEach( () => {
+		const element = document.createElement( 'div' );
+		document.body.appendChild( element );
+
 		// Most tests assume non-edge environment but we do not set `contenteditable=false` on Edge so stub `env.isEdge`.
 		// Most tests assume non-edge environment but we do not set `contenteditable=false` on Edge so stub `env.isEdge`.
 		testUtils.sinon.stub( env, 'isEdge' ).get( () => false );
 		testUtils.sinon.stub( env, 'isEdge' ).get( () => false );
 
 
-		return VirtualTestEditor.create()
+		return ClassicTestEditor.create( element )
 			.then( newEditor => {
 			.then( newEditor => {
 				editor = newEditor;
 				editor = newEditor;
 				model = editor.model;
 				model = editor.model;
 				doc = model.document;
 				doc = model.document;
 				root = doc.getRoot( 'main' );
 				root = doc.getRoot( 'main' );
-				viewDocument = editor.editing.view;
+				view = editor.editing.view;
 
 
 				defaultSchema( model.schema );
 				defaultSchema( model.schema );
 				defaultConversion( editor.conversion, true );
 				defaultConversion( editor.conversion, true );
@@ -46,7 +52,7 @@ describe( 'TableCell post-fixer', () => {
 	} );
 	} );
 
 
 	it( 'should rename <span> to <p> when adding more <paragraph> elements to the same table cell', () => {
 	it( 'should rename <span> to <p> when adding more <paragraph> elements to the same table cell', () => {
-		setModelData( model, modelTable( [ [ '00[]' ] ] ) );
+		editor.setData( viewTable( [ [ '<p>00</p>' ] ] ) );
 
 
 		const table = root.getChild( 0 );
 		const table = root.getChild( 0 );
 
 
@@ -60,13 +66,13 @@ describe( 'TableCell post-fixer', () => {
 			writer.setSelection( nodeByPath.nextSibling, 0 );
 			writer.setSelection( nodeByPath.nextSibling, 0 );
 		} );
 		} );
 
 
-		expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
+		expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
 			[ '<p>00</p><p></p>' ]
 			[ '<p>00</p><p></p>' ]
 		], { asWidget: true } ) );
 		], { asWidget: true } ) );
 	} );
 	} );
 
 
 	it( 'should rename <span> to <p> on adding other block element to the same table cell', () => {
 	it( 'should rename <span> to <p> on adding other block element to the same table cell', () => {
-		setModelData( model, modelTable( [ [ '00[]' ] ] ) );
+		editor.setData( viewTable( [ [ '<p>00</p>' ] ] ) );
 
 
 		const table = root.getChild( 0 );
 		const table = root.getChild( 0 );
 
 
@@ -80,13 +86,13 @@ describe( 'TableCell post-fixer', () => {
 			writer.setSelection( nodeByPath.nextSibling, 0 );
 			writer.setSelection( nodeByPath.nextSibling, 0 );
 		} );
 		} );
 
 
-		expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
+		expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
 			[ '<p>00</p><div></div>' ]
 			[ '<p>00</p><div></div>' ]
 		], { asWidget: true } ) );
 		], { asWidget: true } ) );
 	} );
 	} );
 
 
 	it( 'should properly rename the same element on consecutive changes', () => {
 	it( 'should properly rename the same element on consecutive changes', () => {
-		setModelData( model, modelTable( [ [ '00[]' ] ] ) );
+		editor.setData( viewTable( [ [ '<p>00</p>' ] ] ) );
 
 
 		const table = root.getChild( 0 );
 		const table = root.getChild( 0 );
 
 
@@ -98,7 +104,7 @@ describe( 'TableCell post-fixer', () => {
 			writer.setSelection( nodeByPath.nextSibling, 0 );
 			writer.setSelection( nodeByPath.nextSibling, 0 );
 		} );
 		} );
 
 
-		expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
+		expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
 			[ '<p>00</p><p></p>' ]
 			[ '<p>00</p><p></p>' ]
 		], { asWidget: true } ) );
 		], { asWidget: true } ) );
 
 
@@ -106,13 +112,13 @@ describe( 'TableCell post-fixer', () => {
 			writer.remove( table.getNodeByPath( [ 0, 0, 1 ] ) );
 			writer.remove( table.getNodeByPath( [ 0, 0, 1 ] ) );
 		} );
 		} );
 
 
-		expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
+		expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
 			[ '00' ]
 			[ '00' ]
 		], { asWidget: true } ) );
 		], { asWidget: true } ) );
 	} );
 	} );
 
 
 	it( 'should rename <span> to <p> when setting attribute on <paragraph>', () => {
 	it( 'should rename <span> to <p> when setting attribute on <paragraph>', () => {
-		setModelData( model, modelTable( [ [ '<paragraph>00[]</paragraph>' ] ] ) );
+		editor.setData( '<table><tr><td><p>00</p></td></tr></table>' );
 
 
 		const table = root.getChild( 0 );
 		const table = root.getChild( 0 );
 
 
@@ -120,13 +126,13 @@ describe( 'TableCell post-fixer', () => {
 			writer.setAttribute( 'foo', 'bar', table.getNodeByPath( [ 0, 0, 0 ] ) );
 			writer.setAttribute( 'foo', 'bar', table.getNodeByPath( [ 0, 0, 0 ] ) );
 		} );
 		} );
 
 
-		expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
+		expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
 			[ '<p foo="bar">00</p>' ]
 			[ '<p foo="bar">00</p>' ]
 		], { asWidget: true } ) );
 		], { asWidget: true } ) );
 	} );
 	} );
 
 
 	it( 'should rename <p> to <span> when removing all but one paragraph inside table cell', () => {
 	it( 'should rename <p> to <span> when removing all but one paragraph inside table cell', () => {
-		setModelData( model, modelTable( [ [ '<paragraph>00[]</paragraph><paragraph>foo</paragraph>' ] ] ) );
+		editor.setData( viewTable( [ [ '<p>00</p><p>foo</p>' ] ] ) );
 
 
 		const table = root.getChild( 0 );
 		const table = root.getChild( 0 );
 
 
@@ -134,13 +140,13 @@ describe( 'TableCell post-fixer', () => {
 			writer.remove( table.getNodeByPath( [ 0, 0, 1 ] ) );
 			writer.remove( table.getNodeByPath( [ 0, 0, 1 ] ) );
 		} );
 		} );
 
 
-		expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
+		expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
 			[ '00' ]
 			[ '00' ]
 		], { asWidget: true } ) );
 		], { asWidget: true } ) );
 	} );
 	} );
 
 
 	it( 'should rename <p> to <span> when removing attribute from <paragraph>', () => {
 	it( 'should rename <p> to <span> when removing attribute from <paragraph>', () => {
-		setModelData( model, modelTable( [ [ '<paragraph foo="bar">00[]</paragraph>' ] ] ) );
+		editor.setData( '<table><tr><td><p foo="bar">00</p></td></tr></table>' );
 
 
 		const table = root.getChild( 0 );
 		const table = root.getChild( 0 );
 
 
@@ -148,13 +154,13 @@ describe( 'TableCell post-fixer', () => {
 			writer.removeAttribute( 'foo', table.getNodeByPath( [ 0, 0, 0 ] ) );
 			writer.removeAttribute( 'foo', table.getNodeByPath( [ 0, 0, 0 ] ) );
 		} );
 		} );
 
 
-		expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
+		expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
 			[ '<span>00</span>' ]
 			[ '<span>00</span>' ]
 		], { asWidget: true } ) );
 		], { asWidget: true } ) );
 	} );
 	} );
 
 
 	it( 'should keep <p> in the view when <paragraph> attribute value is changed', () => {
 	it( 'should keep <p> in the view when <paragraph> attribute value is changed', () => {
-		setModelData( model, modelTable( [ [ '<paragraph foo="bar">00[]</paragraph>' ] ] ) );
+		editor.setData( viewTable( [ [ '<p foo="bar">00</p>' ] ] ) );
 
 
 		const table = root.getChild( 0 );
 		const table = root.getChild( 0 );
 
 
@@ -162,13 +168,13 @@ describe( 'TableCell post-fixer', () => {
 			writer.setAttribute( 'foo', 'baz', table.getNodeByPath( [ 0, 0, 0 ] ) );
 			writer.setAttribute( 'foo', 'baz', table.getNodeByPath( [ 0, 0, 0 ] ) );
 		} );
 		} );
 
 
-		expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
+		expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
 			[ '<p foo="baz">00</p>' ]
 			[ '<p foo="baz">00</p>' ]
 		], { asWidget: true } ) );
 		], { asWidget: true } ) );
 	} );
 	} );
 
 
 	it( 'should keep <p> in the view when <paragraph> attribute value is changed (table cell with multiple blocks)', () => {
 	it( 'should keep <p> in the view when <paragraph> attribute value is changed (table cell with multiple blocks)', () => {
-		setModelData( model, modelTable( [ [ '<paragraph foo="bar">00[]</paragraph><paragraph>00</paragraph>' ] ] ) );
+		editor.setData( viewTable( [ [ '<p foo="bar">00</p><p>00</p>' ] ] ) );
 
 
 		const table = root.getChild( 0 );
 		const table = root.getChild( 0 );
 
 
@@ -176,13 +182,13 @@ describe( 'TableCell post-fixer', () => {
 			writer.setAttribute( 'foo', 'baz', table.getNodeByPath( [ 0, 0, 0 ] ) );
 			writer.setAttribute( 'foo', 'baz', table.getNodeByPath( [ 0, 0, 0 ] ) );
 		} );
 		} );
 
 
-		expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
+		expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
 			[ '<p foo="baz">00</p><p>00</p>' ]
 			[ '<p foo="baz">00</p><p>00</p>' ]
 		], { asWidget: true } ) );
 		], { asWidget: true } ) );
 	} );
 	} );
 
 
 	it( 'should do nothing on rename <paragraph> to other block', () => {
 	it( 'should do nothing on rename <paragraph> to other block', () => {
-		setModelData( model, modelTable( [ [ '00' ] ] ) );
+		editor.setData( viewTable( [ [ '<p>00</p>' ] ] ) );
 
 
 		const table = root.getChild( 0 );
 		const table = root.getChild( 0 );
 
 
@@ -190,13 +196,13 @@ describe( 'TableCell post-fixer', () => {
 			writer.rename( table.getNodeByPath( [ 0, 0, 0 ] ), 'block' );
 			writer.rename( table.getNodeByPath( [ 0, 0, 0 ] ), 'block' );
 		} );
 		} );
 
 
-		expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
+		expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
 			[ '<div>00</div>' ]
 			[ '<div>00</div>' ]
 		], { asWidget: true } ) );
 		], { asWidget: true } ) );
 	} );
 	} );
 
 
 	it( 'should do nothing when setting attribute on block item other then <paragraph>', () => {
 	it( 'should do nothing when setting attribute on block item other then <paragraph>', () => {
-		setModelData( model, modelTable( [ [ '<block>foo</block>' ] ] ) );
+		editor.setData( viewTable( [ [ '<div>foo</div>' ] ] ) );
 
 
 		const table = root.getChild( 0 );
 		const table = root.getChild( 0 );
 
 
@@ -204,8 +210,72 @@ describe( 'TableCell post-fixer', () => {
 			writer.setAttribute( 'foo', 'bar', table.getNodeByPath( [ 0, 0, 0 ] ) );
 			writer.setAttribute( 'foo', 'bar', table.getNodeByPath( [ 0, 0, 0 ] ) );
 		} );
 		} );
 
 
-		expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
+		expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
 			[ '<div foo="bar">foo</div>' ]
 			[ '<div foo="bar">foo</div>' ]
 		], { asWidget: true } ) );
 		], { asWidget: true } ) );
 	} );
 	} );
+
+	it( 'should not crash when view.change() block was called in model.change()', () => {
+		editor.setData( viewTable( [ [ '<p>foobar</p>' ] ] ) );
+
+		const table = root.getChild( 0 );
+
+		expect( formatTable( getViewData( view, { withoutSelection: true } ) ) )
+			.to.equal( formattedViewTable( [ [ 'foobar' ] ], { asWidget: true } ) );
+
+		expect( () => {
+			model.change( writer => {
+				const tableCell = table.getNodeByPath( [ 0, 0 ] );
+
+				writer.insertElement( 'paragraph', null, Position.createAt( tableCell, 'end' ) );
+				writer.setSelection( Range.createIn( tableCell ) );
+
+				// Do some change in the view while inside model change.
+				editor.editing.view.change( writer => {
+					writer.addClass( 'foo', editor.editing.mapper.toViewElement( tableCell ) );
+				} );
+			} );
+		} ).to.not.throw();
+
+		expect( formatTable( getViewData( view ) ) ).to.equal( formattedViewTable( [
+			[ { class: 'foo', contents: '<p>{foobar</p><p>]</p>' } ]
+		], { asWidget: true } ) );
+	} );
+
+	it( 'should keep <p> in the view when <paragraph> attribute value is changed (table cell with multiple blocks)', () => {
+		editor.setData( viewTable( [ [ '<p>00</p><p>00</p>' ] ] ) );
+
+		const table = root.getChild( 0 );
+
+		model.change( writer => {
+			writer.remove( Range.createOn( table.getNodeByPath( [ 0, 0, 1 ] ) ) );
+		} );
+
+		expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
+			[ '<span>00</span>' ]
+		], { asWidget: true } ) );
+	} );
+
+	it( 'should update view selection after deleting content', () => {
+		editor.setData( viewTable( [ [ '<p>foo</p><p>bar</p>' ] ] ) );
+
+		const tableCell = root.getNodeByPath( [ 0, 0, 0 ] );
+
+		// Replace table cell contents with paragraph - as model.deleteContent() does.
+		model.change( writer => {
+			writer.remove( Range.createIn( tableCell ) );
+
+			const paragraph = writer.createElement( 'paragraph' );
+
+			writer.insert( paragraph, Position.createAt( tableCell ) );
+
+			// Set selection to newly created paragraph.
+			writer.setSelection( paragraph, 0 );
+		} );
+
+		const viewRange = view.document.selection.getFirstRange();
+
+		// Trying to map view selection to DOM range shouldn't throw after post-fixer will fix inserted <p> to <span>.
+		expect( () => view.domConverter.viewRangeToDom( viewRange ) ).to.not.throw();
+	} );
 } );
 } );