8
0
فهرست منبع

Merge branch 'master' into i/7457

panr 5 سال پیش
والد
کامیت
40d3e4f485
57فایلهای تغییر یافته به همراه1311 افزوده شده و 459 حذف شده
  1. 0 6
      docs/_snippets/examples/document-editor.html
  2. 0 4
      docs/_snippets/framework/tutorials/using-react-in-widget.html
  3. 6 0
      docs/assets/snippet-styles.css
  4. 3 3
      docs/framework/guides/contributing/git-commit-message-convention.md
  5. 1 1
      packages/ckeditor5-core/tests/_utils-tests/assertions/attribute.js
  6. 2 2
      packages/ckeditor5-core/tests/_utils/assertions/attribute.js
  7. 22 0
      packages/ckeditor5-engine/src/model/element.js
  8. 16 0
      packages/ckeditor5-engine/src/model/position.js
  9. 1 1
      packages/ckeditor5-engine/src/view/domconverter.js
  10. 27 0
      packages/ckeditor5-engine/tests/model/element.js
  11. 20 0
      packages/ckeditor5-engine/tests/model/position.js
  12. 0 6
      packages/ckeditor5-table/docs/_snippets/features/table.html
  13. 2 3
      packages/ckeditor5-table/src/commands/insertcolumncommand.js
  14. 2 3
      packages/ckeditor5-table/src/commands/insertrowcommand.js
  15. 3 4
      packages/ckeditor5-table/src/commands/inserttablecommand.js
  16. 3 3
      packages/ckeditor5-table/src/commands/mergecellcommand.js
  17. 3 3
      packages/ckeditor5-table/src/commands/mergecellscommand.js
  18. 1 2
      packages/ckeditor5-table/src/commands/removecolumncommand.js
  19. 4 11
      packages/ckeditor5-table/src/commands/removerowcommand.js
  20. 1 2
      packages/ckeditor5-table/src/commands/selectcolumncommand.js
  21. 1 2
      packages/ckeditor5-table/src/commands/selectrowcommand.js
  22. 1 2
      packages/ckeditor5-table/src/commands/setheadercolumncommand.js
  23. 2 2
      packages/ckeditor5-table/src/commands/setheaderrowcommand.js
  24. 0 100
      packages/ckeditor5-table/src/converters/downcast.js
  25. 54 0
      packages/ckeditor5-table/src/converters/table-heading-rows-refresh-post-fixer.js
  26. 3 3
      packages/ckeditor5-table/src/converters/table-layout-post-fixer.js
  27. 33 17
      packages/ckeditor5-table/src/tableclipboard.js
  28. 4 4
      packages/ckeditor5-table/src/tableediting.js
  29. 2 3
      packages/ckeditor5-table/src/tablekeyboard.js
  30. 1 6
      packages/ckeditor5-table/src/tablemouse.js
  31. 2 4
      packages/ckeditor5-table/src/tableproperties/commands/tablepropertycommand.js
  32. 2 3
      packages/ckeditor5-table/src/tableselection.js
  33. 41 27
      packages/ckeditor5-table/src/tableutils.js
  34. 4 20
      packages/ckeditor5-table/src/utils/common.js
  35. 13 8
      packages/ckeditor5-table/src/utils/selection.js
  36. 31 17
      packages/ckeditor5-table/src/utils/structure.js
  37. 2 3
      packages/ckeditor5-table/src/utils/ui/contextualballoon.js
  38. 12 1
      packages/ckeditor5-table/src/utils/ui/widget.js
  39. 15 0
      packages/ckeditor5-table/tests/commands/inserttablecommand.js
  40. 47 0
      packages/ckeditor5-table/tests/commands/mergecellscommand.js
  41. 33 32
      packages/ckeditor5-table/tests/converters/downcast.js
  42. 13 2
      packages/ckeditor5-table/tests/manual/tablemocking.html
  43. 5 2
      packages/ckeditor5-table/tests/manual/tablemocking.js
  44. 0 2
      packages/ckeditor5-table/tests/manual/tablemocking.md
  45. 353 4
      packages/ckeditor5-table/tests/tableclipboard-paste.js
  46. 81 0
      packages/ckeditor5-table/tests/tableutils.js
  47. 1 18
      packages/ckeditor5-table/tests/utils/common.js
  48. 1 2
      packages/ckeditor5-table/tests/utils/ui/contextualballoon.js
  49. BIN
      packages/ckeditor5-widget/docs/assets/img/framework-deep-dive-widget-type-around-buttons.gif
  50. 67 0
      packages/ckeditor5-widget/docs/framework/guides/deep-dive/widget-internals.md
  51. 1 0
      packages/ckeditor5-widget/package.json
  52. 6 3
      packages/ckeditor5-widget/src/widget.js
  53. 57 11
      packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js
  54. 77 2
      packages/ckeditor5-widget/tests/widget-integration.js
  55. 6 0
      packages/ckeditor5-widget/tests/widgettypearound/utils.js
  56. 216 105
      packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js
  57. 7 0
      packages/ckeditor5-widget/theme/widgettypearound.css

+ 0 - 6
docs/_snippets/examples/document-editor.html

@@ -195,12 +195,6 @@
 		margin-right: calc( 2 * var(--ck-spacing-large) );
 		margin-right: calc( 2 * var(--ck-spacing-large) );
 	}
 	}
 
 
-	/* Some table cells have a lot content and some not. Align them vertically
-	to make reading easier. */
-	.document-editor .ck-content table td {
-		vertical-align: middle;
-	}
-
 	@media only screen and (max-width: 960px) {
 	@media only screen and (max-width: 960px) {
 		/* Because on mobile 2cm paddings are to big. */
 		/* Because on mobile 2cm paddings are to big. */
 		.document-editor__editable-container .document-editor__editable.ck-editor__editable {
 		.document-editor__editable-container .document-editor__editable.ck-editor__editable {

+ 0 - 4
docs/_snippets/framework/tutorials/using-react-in-widget.html

@@ -141,10 +141,6 @@
 	animation: slideUp 0.3s ease;
 	animation: slideUp 0.3s ease;
 }
 }
 
 
-.app .ck-content .table td {
-	vertical-align: middle;
-}
-
 @keyframes slideUp {
 @keyframes slideUp {
 	0% {
 	0% {
 		opacity: 0;
 		opacity: 0;

+ 6 - 0
docs/assets/snippet-styles.css

@@ -232,6 +232,12 @@ It breaks CKEditor 5 (see how <p><code>[]</code></p> looks). */
 	display: table;
 	display: table;
 }
 }
 
 
+/* https://github.com/ckeditor/ckeditor5/issues/7310 */
+.live-snippet .ck.ck-content .table td,
+.live-snippet .ck.ck-content .table th {
+	vertical-align: middle;
+}
+
 /* https://github.com/ckeditor/ckeditor5/issues/1282 */
 /* https://github.com/ckeditor/ckeditor5/issues/1282 */
 .live-snippet .ck.ck-content .table p:first-child {
 .live-snippet .ck.ck-content .table p:first-child {
 	padding-top: 0;
 	padding-top: 0;

+ 3 - 3
docs/framework/guides/contributing/git-commit-message-convention.md

@@ -24,8 +24,8 @@ Type (another-package-name): If the change affects more than one package, it's p
 
 
 Optional description.
 Optional description.
 
 
-BREAKING CHANGE (scope): If any breaking changes were done, they need to be listed here.
-BREAKING CHANGE (scope): Another breaking change if needed. Closes #ZZZ.
+BREAKING CHANGE (package-name): If any breaking changes were done, they need to be listed here.
+BREAKING CHANGE (package-name): Another breaking change if needed. Closes #ZZZ.
 ```
 ```
 
 
 ### Commit types
 ### Commit types
@@ -50,7 +50,7 @@ If any change contains the `MAJOR BREAKING CHANGE` note, the next release will b
 
 
 For reference on how to identify minor or major breaking change see the {@link framework/guides/support/versioning-policy versioning policy guide}.
 For reference on how to identify minor or major breaking change see the {@link framework/guides/support/versioning-policy versioning policy guide}.
 
 
-Each `BREAKING CHANGE` note must be followed by the scope of changes.
+Each `BREAKING CHANGE` note must be followed by the package name.
 
 
 ### Package name
 ### Package name
 
 

+ 1 - 1
packages/ckeditor5-core/tests/_utils-tests/assertions/attribute.js

@@ -54,7 +54,7 @@ describe( 'attribute chai assertion', () => {
 				hasAttribute: () => true,
 				hasAttribute: () => true,
 				getAttribute: () => 'bar'
 				getAttribute: () => 'bar'
 			} ).to.have.attribute( 'foo', 'baz' );
 			} ).to.have.attribute( 'foo', 'baz' );
-		} ).to.throw( 'expected { Object (hasAttribute, getAttribute) } to have attribute \'foo\' of \'bar\', but got \'baz\'' );
+		} ).to.throw( 'expected { Object (hasAttribute, getAttribute) } to have attribute \'foo\' of \'baz\', but got \'bar\'' );
 	} );
 	} );
 
 
 	it( 'negated, should assert for the given type the \'target.getAttribute\' returns a value different than the given one', () => {
 	it( 'negated, should assert for the given type the \'target.getAttribute\' returns a value different than the given one', () => {

+ 2 - 2
packages/ckeditor5-core/tests/_utils/assertions/attribute.js

@@ -54,8 +54,8 @@ chai.Assertion.addMethod( 'attribute', function attributeAssertion( key, value,
 			attributeValue === value,
 			attributeValue === value,
 			`expected #{this} to have attribute '${ key }' of #{exp}, but got #{act}`,
 			`expected #{this} to have attribute '${ key }' of #{exp}, but got #{act}`,
 			`expected #{this} to not have attribute '${ key }' of #{exp}`,
 			`expected #{this} to not have attribute '${ key }' of #{exp}`,
-			attributeValue,
-			value
+			value,
+			attributeValue
 		);
 		);
 	}
 	}
 } );
 } );

+ 22 - 0
packages/ckeditor5-engine/src/model/element.js

@@ -209,6 +209,28 @@ export default class Element extends Node {
 		return node;
 		return node;
 	}
 	}
 
 
+	/**
+	 * Returns the parent element of the given name. Returns null if the element is not inside the desired parent.
+	 *
+	 * @param {String} parentName The name of the parent element to find.
+	 * @param {Object} [options] Options object.
+	 * @param {Boolean} [options.includeSelf=false] When set to `true` this node will be also included while searching.
+	 * @returns {module:engine/model/element~Element|null}
+	 */
+	findAncestor( parentName, options = { includeSelf: false } ) {
+		let parent = options.includeSelf ? this : this.parent;
+
+		while ( parent ) {
+			if ( parent.name === parentName ) {
+				return parent;
+			}
+
+			parent = parent.parent;
+		}
+
+		return null;
+	}
+
 	/**
 	/**
 	 * Converts `Element` instance to plain object and returns it. Takes care of converting all of this element's children.
 	 * Converts `Element` instance to plain object and returns it. Takes care of converting all of this element's children.
 	 *
 	 *

+ 16 - 0
packages/ckeditor5-engine/src/model/position.js

@@ -354,6 +354,22 @@ export default class Position {
 		}
 		}
 	}
 	}
 
 
+	/**
+	 * Returns the parent element of the given name. Returns null if the position is not inside the desired parent.
+	 *
+	 * @param {String} parentName The name of the parent element to find.
+	 * @returns {module:engine/model/element~Element|null}
+	 */
+	findAncestor( parentName ) {
+		const parent = this.parent;
+
+		if ( parent.is( 'element' ) ) {
+			return parent.findAncestor( parentName, { includeSelf: true } );
+		}
+
+		return null;
+	}
+
 	/**
 	/**
 	 * Returns the slice of two position {@link #path paths} which is identical. The {@link #root roots}
 	 * Returns the slice of two position {@link #path paths} which is identical. The {@link #root roots}
 	 * of these two paths must be identical.
 	 * of these two paths must be identical.

+ 1 - 1
packages/ckeditor5-engine/src/view/domconverter.js

@@ -82,7 +82,7 @@ export default class DomConverter {
 		 * @readonly
 		 * @readonly
 		 * @member {Array.<String>} module:engine/view/domconverter~DomConverter#blockElements
 		 * @member {Array.<String>} module:engine/view/domconverter~DomConverter#blockElements
 		 */
 		 */
-		this.blockElements = [ 'p', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'dd', 'dt', 'figcaption' ];
+		this.blockElements = [ 'p', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'dd', 'dt', 'figcaption', 'td', 'th' ];
 
 
 		/**
 		/**
 		 * Block {@link module:engine/view/filler filler} creator, which is used to create all block fillers during the
 		 * Block {@link module:engine/view/filler filler} creator, which is used to create all block fillers during the

+ 27 - 0
packages/ckeditor5-engine/tests/model/element.js

@@ -247,6 +247,33 @@ describe( 'Element', () => {
 		} );
 		} );
 	} );
 	} );
 
 
+	describe( 'findAncestor', () => {
+		let p, td, tr, table;
+
+		beforeEach( () => {
+			p = new Element( 'p', [], [ new Text( 'foo' ) ] );
+			td = new Element( 'td', [], [ p ] );
+			tr = new Element( 'tr', [], [ td ] );
+			table = new Element( 'table', [], [ tr ] );
+		} );
+
+		it( 'should return ancestor', () => {
+			expect( p.findAncestor( 'p' ) ).to.be.null;
+			expect( p.findAncestor( 'td' ) ).to.equal( td );
+			expect( p.findAncestor( 'tr' ) ).to.equal( tr );
+			expect( p.findAncestor( 'table' ) ).to.equal( table );
+			expect( p.findAncestor( 'abc' ) ).to.be.null;
+		} );
+
+		it( 'should return ancestor or self (includeSelf = true)', () => {
+			expect( p.findAncestor( 'p', { includeSelf: true } ) ).to.equal( p );
+			expect( p.findAncestor( 'td', { includeSelf: true } ) ).to.equal( td );
+			expect( p.findAncestor( 'tr', { includeSelf: true } ) ).to.equal( tr );
+			expect( p.findAncestor( 'table', { includeSelf: true } ) ).to.equal( table );
+			expect( p.findAncestor( 'abc', { includeSelf: true } ) ).to.be.null;
+		} );
+	} );
+
 	describe( 'getChildIndex', () => {
 	describe( 'getChildIndex', () => {
 		it( 'should return child index', () => {
 		it( 'should return child index', () => {
 			const element = new Element( 'elem', [], [ new Element( 'p' ), new Text( 'bar' ), new Element( 'h' ) ] );
 			const element = new Element( 'elem', [], [ new Element( 'p' ), new Text( 'bar' ), new Element( 'h' ) ] );

+ 20 - 0
packages/ckeditor5-engine/tests/model/position.js

@@ -684,6 +684,26 @@ describe( 'Position', () => {
 		} );
 		} );
 	} );
 	} );
 
 
+	describe( 'findAncestor()', () => {
+		it( 'should return position parent element', () => {
+			expect( new Position( root, [ 1, 1, 1 ] ).findAncestor( 'li' ) ).to.equal( li2 );
+		} );
+
+		it( 'should return deeper ancestor element', () => {
+			expect( new Position( root, [ 1, 1, 1 ] ).findAncestor( 'ul' ) ).to.equal( ul );
+		} );
+
+		it( 'should return null if ancestor is not found', () => {
+			expect( new Position( root, [ 1, 1, 1 ] ).findAncestor( 'p' ) ).to.be.null;
+		} );
+
+		it( 'should return null if position is not in an element', () => {
+			const docFrag = new DocumentFragment();
+
+			expect( new Position( docFrag, [ 0 ] ).findAncestor( 'li' ) ).to.be.null;
+		} );
+	} );
+
 	describe( 'getCommonPath()', () => {
 	describe( 'getCommonPath()', () => {
 		it( 'returns the common part', () => {
 		it( 'returns the common part', () => {
 			const pos1 = new Position( root, [ 1, 0, 0 ] );
 			const pos1 = new Position( root, [ 1, 0, 0 ] );

+ 0 - 6
packages/ckeditor5-table/docs/_snippets/features/table.html

@@ -46,9 +46,3 @@
 	</table>
 	</table>
 </div>
 </div>
 
 
-<style>
-	.ck.ck-content table td,
-	.ck.ck-content table th {
-		vertical-align: middle;
-	}
-</style>

+ 2 - 3
packages/ckeditor5-table/src/commands/insertcolumncommand.js

@@ -9,7 +9,6 @@
 
 
 import Command from '@ckeditor/ckeditor5-core/src/command';
 import Command from '@ckeditor/ckeditor5-core/src/command';
 import { getColumnIndexes, getSelectionAffectedTableCells } from '../utils/selection';
 import { getColumnIndexes, getSelectionAffectedTableCells } from '../utils/selection';
-import { findAncestor } from '../utils/common';
 
 
 /**
 /**
  * The insert column command.
  * The insert column command.
@@ -54,7 +53,7 @@ export default class InsertColumnCommand extends Command {
 	refresh() {
 	refresh() {
 		const selection = this.editor.model.document.selection;
 		const selection = this.editor.model.document.selection;
 
 
-		const tableParent = findAncestor( 'table', selection.getFirstPosition() );
+		const tableParent = selection.getFirstPosition().findAncestor( 'table' );
 
 
 		this.isEnabled = !!tableParent;
 		this.isEnabled = !!tableParent;
 	}
 	}
@@ -77,7 +76,7 @@ export default class InsertColumnCommand extends Command {
 		const columnIndexes = getColumnIndexes( affectedTableCells );
 		const columnIndexes = getColumnIndexes( affectedTableCells );
 
 
 		const column = insertBefore ? columnIndexes.first : columnIndexes.last;
 		const column = insertBefore ? columnIndexes.first : columnIndexes.last;
-		const table = findAncestor( 'table', affectedTableCells[ 0 ] );
+		const table = affectedTableCells[ 0 ].findAncestor( 'table' );
 
 
 		tableUtils.insertColumns( table, { columns: 1, at: insertBefore ? column : column + 1 } );
 		tableUtils.insertColumns( table, { columns: 1, at: insertBefore ? column : column + 1 } );
 	}
 	}

+ 2 - 3
packages/ckeditor5-table/src/commands/insertrowcommand.js

@@ -9,7 +9,6 @@
 
 
 import Command from '@ckeditor/ckeditor5-core/src/command';
 import Command from '@ckeditor/ckeditor5-core/src/command';
 import { getRowIndexes, getSelectionAffectedTableCells } from '../utils/selection';
 import { getRowIndexes, getSelectionAffectedTableCells } from '../utils/selection';
-import { findAncestor } from '../utils/common';
 
 
 /**
 /**
  * The insert row command.
  * The insert row command.
@@ -54,7 +53,7 @@ export default class InsertRowCommand extends Command {
 	refresh() {
 	refresh() {
 		const selection = this.editor.model.document.selection;
 		const selection = this.editor.model.document.selection;
 
 
-		const tableParent = findAncestor( 'table', selection.getFirstPosition() );
+		const tableParent = selection.getFirstPosition().findAncestor( 'table' );
 
 
 		this.isEnabled = !!tableParent;
 		this.isEnabled = !!tableParent;
 	}
 	}
@@ -76,7 +75,7 @@ export default class InsertRowCommand extends Command {
 		const rowIndexes = getRowIndexes( affectedTableCells );
 		const rowIndexes = getRowIndexes( affectedTableCells );
 
 
 		const row = insertAbove ? rowIndexes.first : rowIndexes.last;
 		const row = insertAbove ? rowIndexes.first : rowIndexes.last;
-		const table = findAncestor( 'table', affectedTableCells[ 0 ] );
+		const table = affectedTableCells[ 0 ].findAncestor( 'table' );
 
 
 		tableUtils.insertRows( table, { at: insertAbove ? row : row + 1, copyStructureFromAbove: !insertAbove } );
 		tableUtils.insertRows( table, { at: insertAbove ? row : row + 1, copyStructureFromAbove: !insertAbove } );
 	}
 	}

+ 3 - 4
packages/ckeditor5-table/src/commands/inserttablecommand.js

@@ -43,6 +43,8 @@ export default class InsertTableCommand extends Command {
 	 * @param {Object} options
 	 * @param {Object} options
 	 * @param {Number} [options.rows=2] The number of rows to create in the inserted table.
 	 * @param {Number} [options.rows=2] The number of rows to create in the inserted table.
 	 * @param {Number} [options.columns=2] The number of columns to create in the inserted table.
 	 * @param {Number} [options.columns=2] The number of columns to create in the inserted table.
+	 * @param {Number} [options.headingRows=0] The number of heading rows.
+	 * @param {Number} [options.headingColumns=0] The number of heading columns.
 	 * @fires execute
 	 * @fires execute
 	 */
 	 */
 	execute( options = {} ) {
 	execute( options = {} ) {
@@ -50,13 +52,10 @@ export default class InsertTableCommand extends Command {
 		const selection = model.document.selection;
 		const selection = model.document.selection;
 		const tableUtils = this.editor.plugins.get( 'TableUtils' );
 		const tableUtils = this.editor.plugins.get( 'TableUtils' );
 
 
-		const rows = parseInt( options.rows ) || 2;
-		const columns = parseInt( options.columns ) || 2;
-
 		const insertPosition = findOptimalInsertionPosition( selection, model );
 		const insertPosition = findOptimalInsertionPosition( selection, model );
 
 
 		model.change( writer => {
 		model.change( writer => {
-			const table = tableUtils.createTable( writer, rows, columns );
+			const table = tableUtils.createTable( writer, options );
 
 
 			model.insertContent( table, insertPosition );
 			model.insertContent( table, insertPosition );
 
 

+ 3 - 3
packages/ckeditor5-table/src/commands/mergecellcommand.js

@@ -10,7 +10,7 @@
 import Command from '@ckeditor/ckeditor5-core/src/command';
 import Command from '@ckeditor/ckeditor5-core/src/command';
 import TableWalker from '../tablewalker';
 import TableWalker from '../tablewalker';
 import { getTableCellsContainingSelection } from '../utils/selection';
 import { getTableCellsContainingSelection } from '../utils/selection';
-import { findAncestor, isHeadingColumnCell } from '../utils/common';
+import { isHeadingColumnCell } from '../utils/common';
 import { removeEmptyRowsColumns } from '../utils/structure';
 import { removeEmptyRowsColumns } from '../utils/structure';
 
 
 /**
 /**
@@ -106,10 +106,10 @@ export default class MergeCellCommand extends Command {
 			writer.setSelection( writer.createRangeIn( cellToExpand ) );
 			writer.setSelection( writer.createRangeIn( cellToExpand ) );
 
 
 			const tableUtils = this.editor.plugins.get( 'TableUtils' );
 			const tableUtils = this.editor.plugins.get( 'TableUtils' );
-			const table = findAncestor( 'table', removedTableCellRow );
+			const table = removedTableCellRow.findAncestor( 'table' );
 
 
 			// Remove empty rows and columns after merging.
 			// Remove empty rows and columns after merging.
-			removeEmptyRowsColumns( table, tableUtils, writer.batch );
+			removeEmptyRowsColumns( table, tableUtils );
 		} );
 		} );
 	}
 	}
 
 

+ 3 - 3
packages/ckeditor5-table/src/commands/mergecellscommand.js

@@ -10,7 +10,7 @@
 import Command from '@ckeditor/ckeditor5-core/src/command';
 import Command from '@ckeditor/ckeditor5-core/src/command';
 import TableUtils from '../tableutils';
 import TableUtils from '../tableutils';
 import { getSelectedTableCells, isSelectionRectangular } from '../utils/selection';
 import { getSelectedTableCells, isSelectionRectangular } from '../utils/selection';
-import { findAncestor, updateNumericAttribute } from '../utils/common';
+import { updateNumericAttribute } from '../utils/common';
 import { removeEmptyRowsColumns } from '../utils/structure';
 import { removeEmptyRowsColumns } from '../utils/structure';
 
 
 /**
 /**
@@ -57,10 +57,10 @@ export default class MergeCellsCommand extends Command {
 				mergeTableCells( tableCell, firstTableCell, writer );
 				mergeTableCells( tableCell, firstTableCell, writer );
 			}
 			}
 
 
-			const table = findAncestor( 'table', firstTableCell );
+			const table = firstTableCell.findAncestor( 'table' );
 
 
 			// Remove rows and columns that become empty (have no anchored cells).
 			// Remove rows and columns that become empty (have no anchored cells).
-			removeEmptyRowsColumns( table, tableUtils, writer.batch );
+			removeEmptyRowsColumns( table, tableUtils );
 
 
 			writer.setSelection( firstTableCell, 'in' );
 			writer.setSelection( firstTableCell, 'in' );
 		} );
 		} );

+ 1 - 2
packages/ckeditor5-table/src/commands/removecolumncommand.js

@@ -11,7 +11,6 @@ import Command from '@ckeditor/ckeditor5-core/src/command';
 
 
 import TableWalker from '../tablewalker';
 import TableWalker from '../tablewalker';
 import { getColumnIndexes, getSelectionAffectedTableCells } from '../utils/selection';
 import { getColumnIndexes, getSelectionAffectedTableCells } from '../utils/selection';
-import { findAncestor } from '../utils/common';
 
 
 /**
 /**
  * The remove column command.
  * The remove column command.
@@ -33,7 +32,7 @@ export default class RemoveColumnCommand extends Command {
 		const firstCell = selectedCells[ 0 ];
 		const firstCell = selectedCells[ 0 ];
 
 
 		if ( firstCell ) {
 		if ( firstCell ) {
-			const table = findAncestor( 'table', firstCell );
+			const table = firstCell.findAncestor( 'table' );
 			const tableColumnCount = this.editor.plugins.get( 'TableUtils' ).getColumns( table );
 			const tableColumnCount = this.editor.plugins.get( 'TableUtils' ).getColumns( table );
 
 
 			const { first, last } = getColumnIndexes( selectedCells );
 			const { first, last } = getColumnIndexes( selectedCells );

+ 4 - 11
packages/ckeditor5-table/src/commands/removerowcommand.js

@@ -10,7 +10,6 @@
 import Command from '@ckeditor/ckeditor5-core/src/command';
 import Command from '@ckeditor/ckeditor5-core/src/command';
 
 
 import { getRowIndexes, getSelectionAffectedTableCells } from '../utils/selection';
 import { getRowIndexes, getSelectionAffectedTableCells } from '../utils/selection';
-import { findAncestor } from '../utils/common';
 
 
 /**
 /**
  * The remove row command.
  * The remove row command.
@@ -32,7 +31,7 @@ export default class RemoveRowCommand extends Command {
 		const firstCell = selectedCells[ 0 ];
 		const firstCell = selectedCells[ 0 ];
 
 
 		if ( firstCell ) {
 		if ( firstCell ) {
-			const table = findAncestor( 'table', firstCell );
+			const table = firstCell.findAncestor( 'table' );
 			const tableRowCount = this.editor.plugins.get( 'TableUtils' ).getRows( table );
 			const tableRowCount = this.editor.plugins.get( 'TableUtils' ).getRows( table );
 			const lastRowIndex = tableRowCount - 1;
 			const lastRowIndex = tableRowCount - 1;
 
 
@@ -56,24 +55,18 @@ export default class RemoveRowCommand extends Command {
 		const removedRowIndexes = getRowIndexes( referenceCells );
 		const removedRowIndexes = getRowIndexes( referenceCells );
 
 
 		const firstCell = referenceCells[ 0 ];
 		const firstCell = referenceCells[ 0 ];
-		const table = findAncestor( 'table', firstCell );
+		const table = firstCell.findAncestor( 'table' );
 
 
 		const columnIndexToFocus = this.editor.plugins.get( 'TableUtils' ).getCellLocation( firstCell ).column;
 		const columnIndexToFocus = this.editor.plugins.get( 'TableUtils' ).getCellLocation( firstCell ).column;
 
 
-		// Use single batch to modify table in steps but in one undo step.
-		const batch = model.createBatch();
-
-		model.enqueueChange( batch, () => {
+		model.change( writer => {
 			const rowsToRemove = removedRowIndexes.last - removedRowIndexes.first + 1;
 			const rowsToRemove = removedRowIndexes.last - removedRowIndexes.first + 1;
 
 
 			this.editor.plugins.get( 'TableUtils' ).removeRows( table, {
 			this.editor.plugins.get( 'TableUtils' ).removeRows( table, {
 				at: removedRowIndexes.first,
 				at: removedRowIndexes.first,
-				rows: rowsToRemove,
-				batch
+				rows: rowsToRemove
 			} );
 			} );
-		} );
 
 
-		model.enqueueChange( batch, writer => {
 			const cellToFocus = getCellToFocus( table, removedRowIndexes.first, columnIndexToFocus );
 			const cellToFocus = getCellToFocus( table, removedRowIndexes.first, columnIndexToFocus );
 
 
 			writer.setSelection( writer.createPositionAt( cellToFocus, 0 ) );
 			writer.setSelection( writer.createPositionAt( cellToFocus, 0 ) );

+ 1 - 2
packages/ckeditor5-table/src/commands/selectcolumncommand.js

@@ -11,7 +11,6 @@ import Command from '@ckeditor/ckeditor5-core/src/command';
 
 
 import TableWalker from '../tablewalker';
 import TableWalker from '../tablewalker';
 import { getSelectionAffectedTableCells } from '../utils/selection';
 import { getSelectionAffectedTableCells } from '../utils/selection';
-import { findAncestor } from '../utils/common';
 
 
 /**
 /**
  * The select column command.
  * The select column command.
@@ -42,7 +41,7 @@ export default class SelectColumnCommand extends Command {
 		const referenceCells = getSelectionAffectedTableCells( model.document.selection );
 		const referenceCells = getSelectionAffectedTableCells( model.document.selection );
 		const firstCell = referenceCells[ 0 ];
 		const firstCell = referenceCells[ 0 ];
 		const lastCell = referenceCells.pop();
 		const lastCell = referenceCells.pop();
-		const table = findAncestor( 'table', firstCell );
+		const table = firstCell.findAncestor( 'table' );
 
 
 		const tableUtils = this.editor.plugins.get( 'TableUtils' );
 		const tableUtils = this.editor.plugins.get( 'TableUtils' );
 		const startLocation = tableUtils.getCellLocation( firstCell );
 		const startLocation = tableUtils.getCellLocation( firstCell );

+ 1 - 2
packages/ckeditor5-table/src/commands/selectrowcommand.js

@@ -10,7 +10,6 @@
 import Command from '@ckeditor/ckeditor5-core/src/command';
 import Command from '@ckeditor/ckeditor5-core/src/command';
 
 
 import { getRowIndexes, getSelectionAffectedTableCells } from '../utils/selection';
 import { getRowIndexes, getSelectionAffectedTableCells } from '../utils/selection';
-import { findAncestor } from '../utils/common';
 
 
 /**
 /**
  * The select row command.
  * The select row command.
@@ -41,7 +40,7 @@ export default class SelectRowCommand extends Command {
 		const referenceCells = getSelectionAffectedTableCells( model.document.selection );
 		const referenceCells = getSelectionAffectedTableCells( model.document.selection );
 		const rowIndexes = getRowIndexes( referenceCells );
 		const rowIndexes = getRowIndexes( referenceCells );
 
 
-		const table = findAncestor( 'table', referenceCells[ 0 ] );
+		const table = referenceCells[ 0 ].findAncestor( 'table' );
 		const rangesToSelect = [];
 		const rangesToSelect = [];
 
 
 		for ( let rowIndex = rowIndexes.first; rowIndex <= rowIndexes.last; rowIndex++ ) {
 		for ( let rowIndex = rowIndexes.first; rowIndex <= rowIndexes.last; rowIndex++ ) {

+ 1 - 2
packages/ckeditor5-table/src/commands/setheadercolumncommand.js

@@ -10,7 +10,6 @@
 import Command from '@ckeditor/ckeditor5-core/src/command';
 import Command from '@ckeditor/ckeditor5-core/src/command';
 
 
 import {
 import {
-	findAncestor,
 	isHeadingColumnCell,
 	isHeadingColumnCell,
 	updateNumericAttribute
 	updateNumericAttribute
 } from '../utils/common';
 } from '../utils/common';
@@ -74,7 +73,7 @@ export default class SetHeaderColumnCommand extends Command {
 
 
 		const model = this.editor.model;
 		const model = this.editor.model;
 		const selectedCells = getSelectionAffectedTableCells( model.document.selection );
 		const selectedCells = getSelectionAffectedTableCells( model.document.selection );
-		const table = findAncestor( 'table', selectedCells[ 0 ] );
+		const table = selectedCells[ 0 ].findAncestor( 'table' );
 
 
 		const { first, last } = getColumnIndexes( selectedCells );
 		const { first, last } = getColumnIndexes( selectedCells );
 		const headingColumnsToSet = this.value ? first : last + 1;
 		const headingColumnsToSet = this.value ? first : last + 1;

+ 2 - 2
packages/ckeditor5-table/src/commands/setheaderrowcommand.js

@@ -9,7 +9,7 @@
 
 
 import Command from '@ckeditor/ckeditor5-core/src/command';
 import Command from '@ckeditor/ckeditor5-core/src/command';
 
 
-import { findAncestor, updateNumericAttribute } from '../utils/common';
+import { updateNumericAttribute } from '../utils/common';
 import { getRowIndexes, getSelectionAffectedTableCells } from '../utils/selection';
 import { getRowIndexes, getSelectionAffectedTableCells } from '../utils/selection';
 import { getVerticallyOverlappingCells, splitHorizontally } from '../utils/structure';
 import { getVerticallyOverlappingCells, splitHorizontally } from '../utils/structure';
 
 
@@ -67,7 +67,7 @@ export default class SetHeaderRowCommand extends Command {
 		}
 		}
 		const model = this.editor.model;
 		const model = this.editor.model;
 		const selectedCells = getSelectionAffectedTableCells( model.document.selection );
 		const selectedCells = getSelectionAffectedTableCells( model.document.selection );
-		const table = findAncestor( 'table', selectedCells[ 0 ] );
+		const table = selectedCells[ 0 ].findAncestor( 'table' );
 
 
 		const { first, last } = getRowIndexes( selectedCells );
 		const { first, last } = getRowIndexes( selectedCells );
 		const headingRowsToSet = this.value ? first : last + 1;
 		const headingRowsToSet = this.value ? first : last + 1;

+ 0 - 100
packages/ckeditor5-table/src/converters/downcast.js

@@ -173,79 +173,6 @@ export function downcastInsertCell() {
 	} );
 	} );
 }
 }
 
 
-/**
- * Conversion helper that acts on heading row table attribute change.
- *
- * This converter will:
- *
- * * Rename `<td>` to `<th>` elements or vice versa depending on headings.
- * * Create `<thead>` or `<tbody>` elements if needed.
- * * Remove empty `<thead>` or `<tbody>` if needed.
- *
- * @returns {Function} Conversion helper.
- */
-export function downcastTableHeadingRowsChange() {
-	return dispatcher => dispatcher.on( 'attribute:headingRows:table', ( evt, data, conversionApi ) => {
-		const table = data.item;
-
-		if ( !conversionApi.consumable.consume( data.item, evt.name ) ) {
-			return;
-		}
-
-		const figureElement = conversionApi.mapper.toViewElement( table );
-		const viewTable = getViewTable( figureElement );
-
-		const oldRows = data.attributeOldValue;
-		const newRows = data.attributeNewValue;
-
-		// The head section has grown so move rows from <tbody> to <thead>.
-		if ( newRows > oldRows ) {
-			// Filter out only those rows that are in wrong section.
-			const rowsToMove = Array.from( table.getChildren() ).filter( ( { index } ) => isBetween( index, oldRows - 1, newRows ) );
-
-			const viewTableHead = getOrCreateTableSection( 'thead', viewTable, conversionApi );
-			moveViewRowsToTableSection( rowsToMove, viewTableHead, conversionApi, 'end' );
-
-			// Rename all table cells from moved rows to 'th' as they lands in <thead>.
-			for ( const tableRow of rowsToMove ) {
-				for ( const tableCell of tableRow.getChildren() ) {
-					renameViewTableCell( tableCell, 'th', conversionApi );
-				}
-			}
-		}
-		// The head section has shrunk so move rows from <thead> to <tbody>.
-		else {
-			// Filter out only those rows that are in wrong section.
-			const rowsToMove = Array.from( table.getChildren() )
-				.filter( ( { index } ) => isBetween( index, newRows - 1, oldRows ) )
-				.reverse(); // The rows will be moved from <thead> to <tbody> in reverse order at the beginning of a <tbody>.
-
-			const viewTableBody = getOrCreateTableSection( 'tbody', viewTable, conversionApi );
-			moveViewRowsToTableSection( rowsToMove, viewTableBody, conversionApi, 0 );
-
-			// Check if cells moved from <thead> to <tbody> requires renaming to <td> as this depends on current heading columns attribute.
-			const tableWalker = new TableWalker( table, { startRow: newRows ? newRows - 1 : newRows, endRow: oldRows - 1 } );
-
-			const tableAttributes = {
-				headingRows: table.getAttribute( 'headingRows' ) || 0,
-				headingColumns: table.getAttribute( 'headingColumns' ) || 0
-			};
-
-			for ( const tableSlot of tableWalker ) {
-				renameViewTableCellIfRequired( tableSlot, tableAttributes, conversionApi );
-			}
-		}
-
-		// Cleanup: Ensure that thead & tbody sections are removed if left empty after moving rows. See #6437, #6391.
-		removeTableSectionIfEmpty( 'thead', viewTable, conversionApi );
-		removeTableSectionIfEmpty( 'tbody', viewTable, conversionApi );
-
-		function isBetween( index, lower, upper ) {
-			return index > lower && index < upper;
-		}
-	} );
-}
-
 /**
 /**
  * Conversion helper that acts on heading column table attribute change.
  * Conversion helper that acts on heading column table attribute change.
  *
  *
@@ -333,11 +260,6 @@ function renameViewTableCell( tableCell, desiredCellElementName, conversionApi )
 	const viewWriter = conversionApi.writer;
 	const viewWriter = conversionApi.writer;
 	const viewCell = conversionApi.mapper.toViewElement( tableCell );
 	const viewCell = conversionApi.mapper.toViewElement( tableCell );
 
 
-	// View cell might be not yet converted - skip it as it will be properly created by cell converter later on.
-	if ( !viewCell ) {
-		return;
-	}
-
 	const editable = viewWriter.createEditableElement( desiredCellElementName, viewCell.getAttributes() );
 	const editable = viewWriter.createEditableElement( desiredCellElementName, viewCell.getAttributes() );
 	const renamedCell = toWidgetEditable( editable, viewWriter );
 	const renamedCell = toWidgetEditable( editable, viewWriter );
 
 
@@ -545,28 +467,6 @@ function removeTableSectionIfEmpty( sectionName, tableElement, conversionApi ) {
 	}
 	}
 }
 }
 
 
-// Moves view table rows associated with passed model rows to the provided table section element.
-//
-// **Note**: This method will skip not converted table rows.
-//
-// @param {Array.<module:engine/model/element~Element>} rowsToMove
-// @param {module:engine/view/element~Element} viewTableSection
-// @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi
-// @param {Number|'end'|'before'|'after'} offset Offset or one of the flags.
-function moveViewRowsToTableSection( rowsToMove, viewTableSection, conversionApi, offset ) {
-	for ( const tableRow of rowsToMove ) {
-		const viewTableRow = conversionApi.mapper.toViewElement( tableRow );
-
-		// View table row might be not yet converted - skip it as it will be properly created by cell converter later on.
-		if ( viewTableRow ) {
-			conversionApi.writer.move(
-				conversionApi.writer.createRangeOn( viewTableRow ),
-				conversionApi.writer.createPositionAt( viewTableSection, offset )
-			);
-		}
-	}
-}
-
 // Finds a '<table>' element inside the `<figure>` widget.
 // Finds a '<table>' element inside the `<figure>` widget.
 //
 //
 // @param {module:engine/view/element~Element} viewFigure
 // @param {module:engine/view/element~Element} viewFigure

+ 54 - 0
packages/ckeditor5-table/src/converters/table-heading-rows-refresh-post-fixer.js

@@ -0,0 +1,54 @@
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module table/converters/table-heading-rows-refresh-post-fixer
+ */
+
+/**
+ * Injects a table post-fixer into the model which marks the table in the differ to have it re-rendered.
+ *
+ * Table heading rows are represented in the model by a `headingRows` attribute. However, in the view, it's represented as separate
+ * sections of the table (`<thead>` or `<tbody>`) and changing `headingRows` attribute requires moving table rows between two sections.
+ * This causes problems with structural changes in a table (like adding and removing rows) thus atomic converters cannot be used.
+ *
+ * When table `headingRows` attribute changes, the entire table is re-rendered.
+ *
+ * @param {module:engine/model/model~Model} model
+ */
+export default function injectTableHeadingRowsRefreshPostFixer( model ) {
+	model.document.registerPostFixer( () => tableHeadingRowsRefreshPostFixer( model ) );
+}
+
+function tableHeadingRowsRefreshPostFixer( model ) {
+	const differ = model.document.differ;
+
+	// Stores tables to be refreshed so the table will be refreshed once for multiple changes.
+	const tablesToRefresh = new Set();
+
+	for ( const change of differ.getChanges() ) {
+		if ( change.type != 'attribute' ) {
+			continue;
+		}
+
+		const element = change.range.start.nodeAfter;
+
+		if ( element && element.is( 'table' ) && change.attributeKey == 'headingRows' ) {
+			tablesToRefresh.add( element );
+		}
+	}
+
+	if ( tablesToRefresh.size ) {
+		// @if CK_DEBUG_TABLE // console.log( `Post-fixing table: refreshing heading rows (${ tablesToRefresh.size }).` );
+
+		for ( const table of tablesToRefresh.values() ) {
+			differ.refreshItem( table );
+		}
+
+		return true;
+	}
+
+	return false;
+}

+ 3 - 3
packages/ckeditor5-table/src/converters/table-layout-post-fixer.js

@@ -8,7 +8,7 @@
  */
  */
 
 
 import TableWalker from './../tablewalker';
 import TableWalker from './../tablewalker';
-import { createEmptyTableCell, findAncestor, updateNumericAttribute } from '../utils/common';
+import { createEmptyTableCell, updateNumericAttribute } from '../utils/common';
 
 
 /**
 /**
  * Injects a table layout post-fixer into the model.
  * Injects a table layout post-fixer into the model.
@@ -238,12 +238,12 @@ function tableLayoutPostFixer( writer, model ) {
 
 
 		// Fix table on adding/removing table cells and rows.
 		// Fix table on adding/removing table cells and rows.
 		if ( entry.name == 'tableRow' || entry.name == 'tableCell' ) {
 		if ( entry.name == 'tableRow' || entry.name == 'tableCell' ) {
-			table = findAncestor( 'table', entry.position );
+			table = entry.position.findAncestor( 'table' );
 		}
 		}
 
 
 		// Fix table on any table's attribute change - including attributes of table cells.
 		// Fix table on any table's attribute change - including attributes of table cells.
 		if ( isTableAttributeEntry( entry ) ) {
 		if ( isTableAttributeEntry( entry ) ) {
-			table = findAncestor( 'table', entry.range.start );
+			table = entry.range.start.findAncestor( 'table' );
 		}
 		}
 
 
 		if ( table && !analyzedTables.has( table ) ) {
 		if ( table && !analyzedTables.has( table ) ) {

+ 33 - 17
packages/ckeditor5-table/src/tableclipboard.js

@@ -11,11 +11,8 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 
 
 import TableSelection from './tableselection';
 import TableSelection from './tableselection';
 import TableWalker from './tablewalker';
 import TableWalker from './tablewalker';
-import {
-	findAncestor
-} from './utils/common';
 import TableUtils from './tableutils';
 import TableUtils from './tableutils';
-import { getColumnIndexes, getRowIndexes, getSelectionAffectedTableCells, isSelectionRectangular } from './utils/selection';
+import { getColumnIndexes, getRowIndexes, getSelectionAffectedTableCells, isSelectionRectangular, sortRanges } from './utils/selection';
 import {
 import {
 	cropTableToDimensions,
 	cropTableToDimensions,
 	getHorizontallyOverlappingCells,
 	getHorizontallyOverlappingCells,
@@ -165,7 +162,7 @@ export default class TableClipboard extends Plugin {
 			pastedTable = cropTableToDimensions( pastedTable, cropDimensions, writer );
 			pastedTable = cropTableToDimensions( pastedTable, cropDimensions, writer );
 
 
 			// Content table to which we insert a pasted table.
 			// Content table to which we insert a pasted table.
-			const selectedTable = findAncestor( 'table', selectedTableCells[ 0 ] );
+			const selectedTable = selectedTableCells[ 0 ].findAncestor( 'table' );
 
 
 			replaceSelectedCellsWithPasted( pastedTable, pastedDimensions, selectedTable, selection, writer );
 			replaceSelectedCellsWithPasted( pastedTable, pastedDimensions, selectedTable, selection, writer );
 		} );
 		} );
@@ -186,7 +183,7 @@ export default class TableClipboard extends Plugin {
 // @returns {Number} selection.lastColumn
 // @returns {Number} selection.lastColumn
 // @returns {Number} selection.lastRow
 // @returns {Number} selection.lastRow
 function prepareTableForPasting( selectedTableCells, pastedDimensions, writer, tableUtils ) {
 function prepareTableForPasting( selectedTableCells, pastedDimensions, writer, tableUtils ) {
-	const selectedTable = findAncestor( 'table', selectedTableCells[ 0 ] );
+	const selectedTable = selectedTableCells[ 0 ].findAncestor( 'table' );
 
 
 	const columnIndexes = getColumnIndexes( selectedTableCells );
 	const columnIndexes = getColumnIndexes( selectedTableCells );
 	const rowIndexes = getRowIndexes( selectedTableCells );
 	const rowIndexes = getRowIndexes( selectedTableCells );
@@ -205,7 +202,7 @@ function prepareTableForPasting( selectedTableCells, pastedDimensions, writer, t
 		selection.lastRow += pastedDimensions.height - 1;
 		selection.lastRow += pastedDimensions.height - 1;
 		selection.lastColumn += pastedDimensions.width - 1;
 		selection.lastColumn += pastedDimensions.width - 1;
 
 
-		expandTableSize( selectedTable, selection.lastRow + 1, selection.lastColumn + 1, writer, tableUtils );
+		expandTableSize( selectedTable, selection.lastRow + 1, selection.lastColumn + 1, tableUtils );
 	}
 	}
 
 
 	// In case of expanding selection we do not reset the selection so in this case we will always try to fix selection
 	// In case of expanding selection we do not reset the selection so in this case we will always try to fix selection
@@ -316,17 +313,41 @@ function replaceSelectedCellsWithPasted( pastedTable, pastedDimensions, selected
 		insertPosition = writer.createPositionAfter( cellToInsert );
 		insertPosition = writer.createPositionAfter( cellToInsert );
 	}
 	}
 
 
-	writer.setSelection( cellsToSelect.map( cell => writer.createRangeOn( cell ) ) );
+	// If there are any headings, all the cells that overlap from heading must be splitted.
+	const headingRows = parseInt( selectedTable.getAttribute( 'headingRows' ) || 0 );
+	const headingColumns = parseInt( selectedTable.getAttribute( 'headingColumns' ) || 0 );
+
+	const areHeadingRowsIntersectingSelection = selection.firstRow < headingRows && headingRows <= selection.lastRow;
+	const areHeadingColumnsIntersectingSelection = selection.firstColumn < headingColumns && headingColumns <= selection.lastColumn;
+
+	if ( areHeadingRowsIntersectingSelection ) {
+		const columnsLimit = { first: selection.firstColumn, last: selection.lastColumn };
+		const newCells = doHorizontalSplit( selectedTable, headingRows, columnsLimit, writer, selection.firstRow );
+
+		cellsToSelect.push( ...newCells );
+	}
+
+	if ( areHeadingColumnsIntersectingSelection ) {
+		const rowsLimit = { first: selection.firstRow, last: selection.lastRow };
+		const newCells = doVerticalSplit( selectedTable, headingColumns, rowsLimit, writer );
+
+		cellsToSelect.push( ...newCells );
+	}
+
+	// Selection ranges must be sorted because the first and last selection ranges are considered
+	// as anchor/focus cell ranges for multi-cell selection.
+	const selectionRanges = sortRanges( cellsToSelect.map( cell => writer.createRangeOn( cell ) ) );
+
+	writer.setSelection( selectionRanges );
 }
 }
 
 
 // Expand table (in place) to expected size.
 // Expand table (in place) to expected size.
-function expandTableSize( table, expectedHeight, expectedWidth, writer, tableUtils ) {
+function expandTableSize( table, expectedHeight, expectedWidth, tableUtils ) {
 	const tableWidth = tableUtils.getColumns( table );
 	const tableWidth = tableUtils.getColumns( table );
 	const tableHeight = tableUtils.getRows( table );
 	const tableHeight = tableUtils.getRows( table );
 
 
 	if ( expectedWidth > tableWidth ) {
 	if ( expectedWidth > tableWidth ) {
 		tableUtils.insertColumns( table, {
 		tableUtils.insertColumns( table, {
-			batch: writer.batch,
 			at: tableWidth,
 			at: tableWidth,
 			columns: expectedWidth - tableWidth
 			columns: expectedWidth - tableWidth
 		} );
 		} );
@@ -334,7 +355,6 @@ function expandTableSize( table, expectedHeight, expectedWidth, writer, tableUti
 
 
 	if ( expectedHeight > tableHeight ) {
 	if ( expectedHeight > tableHeight ) {
 		tableUtils.insertRows( table, {
 		tableUtils.insertRows( table, {
-			batch: writer.batch,
 			at: tableHeight,
 			at: tableHeight,
 			rows: expectedHeight - tableHeight
 			rows: expectedHeight - tableHeight
 		} );
 		} );
@@ -489,9 +509,7 @@ function doHorizontalSplit( table, splitRow, limitColumns, writer, startRow = 0
 	// Filter out cells that are not touching insides of the rectangular selection.
 	// Filter out cells that are not touching insides of the rectangular selection.
 	const cellsToSplit = overlappingCells.filter( ( { column, cellWidth } ) => isAffectedBySelection( column, cellWidth, limitColumns ) );
 	const cellsToSplit = overlappingCells.filter( ( { column, cellWidth } ) => isAffectedBySelection( column, cellWidth, limitColumns ) );
 
 
-	for ( const { cell } of cellsToSplit ) {
-		splitHorizontally( cell, splitRow, writer );
-	}
+	return cellsToSplit.map( ( { cell } ) => splitHorizontally( cell, splitRow, writer ) );
 }
 }
 
 
 function doVerticalSplit( table, splitColumn, limitRows, writer ) {
 function doVerticalSplit( table, splitColumn, limitRows, writer ) {
@@ -505,9 +523,7 @@ function doVerticalSplit( table, splitColumn, limitRows, writer ) {
 	// Filter out cells that are not touching insides of the rectangular selection.
 	// Filter out cells that are not touching insides of the rectangular selection.
 	const cellsToSplit = overlappingCells.filter( ( { row, cellHeight } ) => isAffectedBySelection( row, cellHeight, limitRows ) );
 	const cellsToSplit = overlappingCells.filter( ( { row, cellHeight } ) => isAffectedBySelection( row, cellHeight, limitRows ) );
 
 
-	for ( const { cell, column } of cellsToSplit ) {
-		splitVertically( cell, column, splitColumn, writer );
-	}
+	return cellsToSplit.map( ( { cell, column } ) => splitVertically( cell, column, splitColumn, writer ) );
 }
 }
 
 
 // Checks if cell at given row (column) is affected by a rectangular selection defined by first/last column (row).
 // Checks if cell at given row (column) is affected by a rectangular selection defined by first/last column (row).

+ 4 - 4
packages/ckeditor5-table/src/tableediting.js

@@ -15,8 +15,7 @@ import {
 	downcastInsertRow,
 	downcastInsertRow,
 	downcastInsertTable,
 	downcastInsertTable,
 	downcastRemoveRow,
 	downcastRemoveRow,
-	downcastTableHeadingColumnsChange,
-	downcastTableHeadingRowsChange
+	downcastTableHeadingColumnsChange
 } from './converters/downcast';
 } from './converters/downcast';
 
 
 import InsertTableCommand from './commands/inserttablecommand';
 import InsertTableCommand from './commands/inserttablecommand';
@@ -36,6 +35,7 @@ import TableUtils from '../src/tableutils';
 import injectTableLayoutPostFixer from './converters/table-layout-post-fixer';
 import injectTableLayoutPostFixer from './converters/table-layout-post-fixer';
 import injectTableCellParagraphPostFixer from './converters/table-cell-paragraph-post-fixer';
 import injectTableCellParagraphPostFixer from './converters/table-cell-paragraph-post-fixer';
 import injectTableCellRefreshPostFixer from './converters/table-cell-refresh-post-fixer';
 import injectTableCellRefreshPostFixer from './converters/table-cell-refresh-post-fixer';
+import injectTableHeadingRowsRefreshPostFixer from './converters/table-heading-rows-refresh-post-fixer';
 
 
 import '../theme/tableediting.css';
 import '../theme/tableediting.css';
 
 
@@ -113,9 +113,8 @@ export default class TableEditing extends Plugin {
 		conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } );
 		conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } );
 		conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } );
 		conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } );
 
 
-		// Table heading rows and columns conversion.
+		// Table heading columns conversion (change of heading rows requires reconversion of the whole table).
 		conversion.for( 'editingDowncast' ).add( downcastTableHeadingColumnsChange() );
 		conversion.for( 'editingDowncast' ).add( downcastTableHeadingColumnsChange() );
-		conversion.for( 'editingDowncast' ).add( downcastTableHeadingRowsChange() );
 
 
 		// Define all the commands.
 		// Define all the commands.
 		editor.commands.add( 'insertTable', new InsertTableCommand( editor ) );
 		editor.commands.add( 'insertTable', new InsertTableCommand( editor ) );
@@ -143,6 +142,7 @@ export default class TableEditing extends Plugin {
 		editor.commands.add( 'selectTableRow', new SelectRowCommand( editor ) );
 		editor.commands.add( 'selectTableRow', new SelectRowCommand( editor ) );
 		editor.commands.add( 'selectTableColumn', new SelectColumnCommand( editor ) );
 		editor.commands.add( 'selectTableColumn', new SelectColumnCommand( editor ) );
 
 
+		injectTableHeadingRowsRefreshPostFixer( model );
 		injectTableLayoutPostFixer( model );
 		injectTableLayoutPostFixer( model );
 		injectTableCellRefreshPostFixer( model );
 		injectTableCellRefreshPostFixer( model );
 		injectTableCellParagraphPostFixer( model );
 		injectTableCellParagraphPostFixer( model );

+ 2 - 3
packages/ckeditor5-table/src/tablekeyboard.js

@@ -18,7 +18,6 @@ import {
 	getLocalizedArrowKeyCodeDirection
 	getLocalizedArrowKeyCodeDirection
 } from '@ckeditor/ckeditor5-utils/src/keyboard';
 } from '@ckeditor/ckeditor5-utils/src/keyboard';
 import { getSelectedTableCells, getTableCellsContainingSelection } from './utils/selection';
 import { getSelectedTableCells, getTableCellsContainingSelection } from './utils/selection';
-import { findAncestor } from './utils/common';
 
 
 /**
 /**
  * This plugin enables keyboard navigation for tables.
  * This plugin enables keyboard navigation for tables.
@@ -214,7 +213,7 @@ export default class TableKeyboard extends Plugin {
 		}
 		}
 
 
 		// Abort if we're not in a table cell.
 		// Abort if we're not in a table cell.
-		const tableCell = findAncestor( 'tableCell', selection.focus );
+		const tableCell = selection.focus.findAncestor( 'tableCell' );
 
 
 		if ( !tableCell ) {
 		if ( !tableCell ) {
 			return false;
 			return false;
@@ -418,7 +417,7 @@ export default class TableKeyboard extends Plugin {
 	_navigateFromCellInDirection( focusCell, direction, expandSelection = false ) {
 	_navigateFromCellInDirection( focusCell, direction, expandSelection = false ) {
 		const model = this.editor.model;
 		const model = this.editor.model;
 
 
-		const table = findAncestor( 'table', focusCell );
+		const table = focusCell.findAncestor( 'table' );
 		const tableMap = [ ...new TableWalker( table, { includeAllSlots: true } ) ];
 		const tableMap = [ ...new TableWalker( table, { includeAllSlots: true } ) ];
 		const { row: lastRow, column: lastColumn } = tableMap[ tableMap.length - 1 ];
 		const { row: lastRow, column: lastColumn } = tableMap[ tableMap.length - 1 ];
 
 

+ 1 - 6
packages/ckeditor5-table/src/tablemouse.js

@@ -12,7 +12,6 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import TableSelection from './tableselection';
 import TableSelection from './tableselection';
 import MouseEventsObserver from './tablemouse/mouseeventsobserver';
 import MouseEventsObserver from './tablemouse/mouseeventsobserver';
 
 
-import { findAncestor } from './utils/common';
 import { getTableCellsContainingSelection } from './utils/selection';
 import { getTableCellsContainingSelection } from './utils/selection';
 
 
 /**
 /**
@@ -210,11 +209,7 @@ export default class TableMouse extends Plugin {
 		const modelPosition = this.editor.editing.mapper.toModelPosition( viewPosition );
 		const modelPosition = this.editor.editing.mapper.toModelPosition( viewPosition );
 		const modelElement = modelPosition.parent;
 		const modelElement = modelPosition.parent;
 
 
-		if ( modelElement.is( 'tableCell' ) ) {
-			return modelElement;
-		}
-
-		return findAncestor( 'tableCell', modelElement );
+		return modelElement.findAncestor( 'tableCell', { includeSelf: true } );
 	}
 	}
 }
 }
 
 

+ 2 - 4
packages/ckeditor5-table/src/tableproperties/commands/tablepropertycommand.js

@@ -9,8 +9,6 @@
 
 
 import Command from '@ckeditor/ckeditor5-core/src/command';
 import Command from '@ckeditor/ckeditor5-core/src/command';
 
 
-import { findAncestor } from '../../utils/common';
-
 /**
 /**
  * The table cell attribute command.
  * The table cell attribute command.
  *
  *
@@ -38,7 +36,7 @@ export default class TablePropertyCommand extends Command {
 		const editor = this.editor;
 		const editor = this.editor;
 		const selection = editor.model.document.selection;
 		const selection = editor.model.document.selection;
 
 
-		const table = findAncestor( 'table', selection.getFirstPosition() );
+		const table = selection.getFirstPosition().findAncestor( 'table' );
 
 
 		this.isEnabled = !!table;
 		this.isEnabled = !!table;
 		this.value = this._getValue( table );
 		this.value = this._getValue( table );
@@ -60,7 +58,7 @@ export default class TablePropertyCommand extends Command {
 
 
 		const { value, batch } = options;
 		const { value, batch } = options;
 
 
-		const table = findAncestor( 'table', selection.getFirstPosition() );
+		const table = selection.getFirstPosition().findAncestor( 'table' );
 		const valueToSet = this._getValueToSet( value );
 		const valueToSet = this._getValueToSet( value );
 
 
 		model.enqueueChange( batch || 'default', writer => {
 		model.enqueueChange( batch || 'default', writer => {

+ 2 - 3
packages/ckeditor5-table/src/tableselection.js

@@ -13,7 +13,6 @@ import first from '@ckeditor/ckeditor5-utils/src/first';
 import TableWalker from './tablewalker';
 import TableWalker from './tablewalker';
 import TableUtils from './tableutils';
 import TableUtils from './tableutils';
 
 
-import { findAncestor } from './utils/common';
 import { cropTableToDimensions, adjustLastRowIndex, adjustLastColumnIndex } from './utils/structure';
 import { cropTableToDimensions, adjustLastRowIndex, adjustLastColumnIndex } from './utils/structure';
 import { getColumnIndexes, getRowIndexes, getSelectedTableCells, isSelectionRectangular } from './utils/selection';
 import { getColumnIndexes, getRowIndexes, getSelectedTableCells, isSelectionRectangular } from './utils/selection';
 
 
@@ -95,7 +94,7 @@ export default class TableSelection extends Plugin {
 			const { first: firstColumn, last: lastColumn } = getColumnIndexes( selectedCells );
 			const { first: firstColumn, last: lastColumn } = getColumnIndexes( selectedCells );
 			const { first: firstRow, last: lastRow } = getRowIndexes( selectedCells );
 			const { first: firstRow, last: lastRow } = getRowIndexes( selectedCells );
 
 
-			const sourceTable = findAncestor( 'table', selectedCells[ 0 ] );
+			const sourceTable = selectedCells[ 0 ].findAncestor( 'table' );
 
 
 			let adjustedLastRow = lastRow;
 			let adjustedLastRow = lastRow;
 			let adjustedLastColumn = lastColumn;
 			let adjustedLastColumn = lastColumn;
@@ -335,7 +334,7 @@ export default class TableSelection extends Plugin {
 			endColumn
 			endColumn
 		};
 		};
 
 
-		for ( const { row, cell } of new TableWalker( findAncestor( 'table', anchorCell ), walkerOptions ) ) {
+		for ( const { row, cell } of new TableWalker( anchorCell.findAncestor( 'table' ), walkerOptions ) ) {
 			selectionMap[ row - startRow ].push( cell );
 			selectionMap[ row - startRow ].push( cell );
 		}
 		}
 
 

+ 41 - 27
packages/ckeditor5-table/src/tableutils.js

@@ -74,22 +74,36 @@ export default class TableUtils extends Plugin {
 	 *
 	 *
 	 *		model.change( ( writer ) => {
 	 *		model.change( ( writer ) => {
 	 *			// Create a table of 2 rows and 7 columns:
 	 *			// Create a table of 2 rows and 7 columns:
-	 *			const table = tableUtils.createTable( writer, 2, 7);
+	 *			const table = tableUtils.createTable( writer, { rows: 2, columns: 7 } );
 	 *
 	 *
 	 *			// Insert a table to the model at the best position taking the current selection:
 	 *			// Insert a table to the model at the best position taking the current selection:
 	 *			model.insertContent( table );
 	 *			model.insertContent( table );
 	 *		}
 	 *		}
 	 *
 	 *
 	 * @param {module:engine/model/writer~Writer} writer The model writer.
 	 * @param {module:engine/model/writer~Writer} writer The model writer.
-	 * @param {Number} rows The number of rows to create.
-	 * @param {Number} columns The number of columns to create.
+	 * @param {Object} options
+	 * @param {Number} [options.rows=2] The number of rows to create.
+	 * @param {Number} [options.columns=2] The number of columns to create.
+	 * @param {Number} [options.headingRows=0] The number of heading rows.
+	 * @param {Number} [options.headingColumns=0] The number of heading columns.
 	 * @returns {module:engine/model/element~Element} The created table element.
 	 * @returns {module:engine/model/element~Element} The created table element.
 	 */
 	 */
-	createTable( writer, rows, columns ) {
+	createTable( writer, options ) {
 		const table = writer.createElement( 'table' );
 		const table = writer.createElement( 'table' );
 
 
+		const rows = parseInt( options.rows ) || 2;
+		const columns = parseInt( options.columns ) || 2;
+
 		createEmptyRows( writer, table, 0, rows, columns );
 		createEmptyRows( writer, table, 0, rows, columns );
 
 
+		if ( options.headingRows ) {
+			updateNumericAttribute( 'headingRows', options.headingRows, table, writer, 0 );
+		}
+
+		if ( options.headingColumns ) {
+			updateNumericAttribute( 'headingColumns', options.headingColumns, table, writer, 0 );
+		}
+
 		return table;
 		return table;
 	}
 	}
 
 
@@ -136,7 +150,7 @@ export default class TableUtils extends Plugin {
 
 
 			// Inserting rows inside heading section requires to update `headingRows` attribute as the heading section will grow.
 			// Inserting rows inside heading section requires to update `headingRows` attribute as the heading section will grow.
 			if ( headingRows > insertAt ) {
 			if ( headingRows > insertAt ) {
-				writer.setAttribute( 'headingRows', headingRows + rowsToInsert, table );
+				updateNumericAttribute( 'headingRows', headingRows + rowsToInsert, table, writer, 0 );
 			}
 			}
 
 
 			// Inserting at the end or at the beginning of a table doesn't require to calculate anything special.
 			// Inserting at the end or at the beginning of a table doesn't require to calculate anything special.
@@ -309,9 +323,8 @@ export default class TableUtils extends Plugin {
 		const rowsToRemove = options.rows || 1;
 		const rowsToRemove = options.rows || 1;
 		const first = options.at;
 		const first = options.at;
 		const last = first + rowsToRemove - 1;
 		const last = first + rowsToRemove - 1;
-		const batch = options.batch || 'default';
 
 
-		model.enqueueChange( batch, writer => {
+		model.change( writer => {
 			// Removing rows from the table require that most calculations to be done prior to changing table structure.
 			// Removing rows from the table require that most calculations to be done prior to changing table structure.
 			// Preparations must be done in the same enqueueChange callback to use the current table structure.
 			// Preparations must be done in the same enqueueChange callback to use the current table structure.
 
 
@@ -337,11 +350,15 @@ export default class TableUtils extends Plugin {
 				updateNumericAttribute( 'rowspan', rowspan, cell, writer );
 				updateNumericAttribute( 'rowspan', rowspan, cell, writer );
 			}
 			}
 
 
-			// 2d. Remove empty columns (without anchored cells) if there are any.
-			removeEmptyColumns( table, this );
+			// 2d. Adjust heading rows if removed rows were in a heading section.
+			updateHeadingRows( table, first, last, writer );
 
 
-			// 2e. Adjust heading rows if removed rows were in a heading section.
-			updateHeadingRows( table, first, last, model, batch );
+			// 2e. Remove empty columns (without anchored cells) if there are any.
+			if ( !removeEmptyColumns( table, this ) ) {
+				// If there wasn't any empty columns then we still need to check if this wasn't called
+				// because of cleaning empty rows and we only removed one of them.
+				removeEmptyRows( table, this );
+			}
 		} );
 		} );
 	}
 	}
 
 
@@ -396,7 +413,11 @@ export default class TableUtils extends Plugin {
 			}
 			}
 
 
 			// Remove empty rows that could appear after removing columns.
 			// Remove empty rows that could appear after removing columns.
-			removeEmptyRows( table, this, writer.batch );
+			if ( !removeEmptyRows( table, this ) ) {
+				// If there wasn't any empty rows then we still need to check if this wasn't called
+				// because of cleaning empty columns and we only removed one of them.
+				removeEmptyColumns( table, this );
+			}
 		} );
 		} );
 	}
 	}
 
 
@@ -776,21 +797,14 @@ function adjustHeadingColumns( table, removedColumnIndexes, writer ) {
 }
 }
 
 
 // Calculates a new heading rows value for removing rows from heading section.
 // Calculates a new heading rows value for removing rows from heading section.
-function updateHeadingRows( table, first, last, model, batch ) {
-	// Must be done after the changes in table structure (removing rows).
-	// Otherwise the downcast converter for headingRows attribute will fail.
-	// See https://github.com/ckeditor/ckeditor5/issues/6391.
-	//
-	// Must be completely wrapped in enqueueChange to get the current table state (after applying other enqueued changes).
-	model.enqueueChange( batch, writer => {
-		const headingRows = table.getAttribute( 'headingRows' ) || 0;
-
-		if ( first < headingRows ) {
-			const newRows = last < headingRows ? headingRows - ( last - first + 1 ) : first;
-
-			updateNumericAttribute( 'headingRows', newRows, table, writer, 0 );
-		}
-	} );
+function updateHeadingRows( table, first, last, writer ) {
+	const headingRows = table.getAttribute( 'headingRows' ) || 0;
+
+	if ( first < headingRows ) {
+		const newRows = last < headingRows ? headingRows - ( last - first + 1 ) : first;
+
+		updateNumericAttribute( 'headingRows', newRows, table, writer, 0 );
+	}
 }
 }
 
 
 // Finds cells that will be:
 // Finds cells that will be:

+ 4 - 20
packages/ckeditor5-table/src/utils/common.js

@@ -7,26 +7,6 @@
  * @module table/utils/common
  * @module table/utils/common
  */
  */
 
 
-/**
- * Returns the parent element of the given name. Returns undefined if the position or the element is not inside the desired parent.
- *
- * @param {String} parentName The name of the parent element to find.
- * @param {module:engine/model/position~Position|module:engine/model/position~Position} positionOrElement The position or
- * the parentElement to start searching.
- * @returns {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment}
- */
-export function findAncestor( parentName, positionOrElement ) {
-	let parent = positionOrElement.parent;
-
-	while ( parent ) {
-		if ( parent.name === parentName ) {
-			return parent;
-		}
-
-		parent = parent.parent;
-	}
-}
-
 /**
 /**
  * A common method to update the numeric value. If a value is the default one, it will be unset.
  * A common method to update the numeric value. If a value is the default one, it will be unset.
  *
  *
@@ -50,11 +30,15 @@ export function updateNumericAttribute( key, value, item, writer, defaultValue =
  * @param {module:engine/model/writer~Writer} writer The model writer.
  * @param {module:engine/model/writer~Writer} writer The model writer.
  * @param {module:engine/model/position~Position} insertPosition The position at which the table cell should be inserted.
  * @param {module:engine/model/position~Position} insertPosition The position at which the table cell should be inserted.
  * @param {Object} attributes The element attributes.
  * @param {Object} attributes The element attributes.
+ * @returns {module:engine/model/element~Element} Created table cell.
  */
  */
 export function createEmptyTableCell( writer, insertPosition, attributes = {} ) {
 export function createEmptyTableCell( writer, insertPosition, attributes = {} ) {
 	const tableCell = writer.createElement( 'tableCell', attributes );
 	const tableCell = writer.createElement( 'tableCell', attributes );
+
 	writer.insertElement( 'paragraph', tableCell );
 	writer.insertElement( 'paragraph', tableCell );
 	writer.insert( tableCell, insertPosition );
 	writer.insert( tableCell, insertPosition );
+
+	return tableCell;
 }
 }
 
 
 /**
 /**

+ 13 - 8
packages/ckeditor5-table/src/utils/selection.js

@@ -8,7 +8,6 @@
  */
  */
 
 
 import TableWalker from '../tablewalker';
 import TableWalker from '../tablewalker';
-import { findAncestor } from './common';
 
 
 /**
 /**
  * Returns all model table cells that are fully selected (from the outside)
  * Returns all model table cells that are fully selected (from the outside)
@@ -48,7 +47,7 @@ export function getTableCellsContainingSelection( selection ) {
 	const cells = [];
 	const cells = [];
 
 
 	for ( const range of selection.getRanges() ) {
 	for ( const range of selection.getRanges() ) {
-		const cellWithSelection = findAncestor( 'tableCell', range.start );
+		const cellWithSelection = range.start.findAncestor( 'tableCell' );
 
 
 		if ( cellWithSelection ) {
 		if ( cellWithSelection ) {
 			cells.push( cellWithSelection );
 			cells.push( cellWithSelection );
@@ -110,7 +109,7 @@ export function getRowIndexes( tableCells ) {
  * @returns {Object} Returns an object with the `first` and `last` table column indexes.
  * @returns {Object} Returns an object with the `first` and `last` table column indexes.
  */
  */
 export function getColumnIndexes( tableCells ) {
 export function getColumnIndexes( tableCells ) {
-	const table = findAncestor( 'table', tableCells[ 0 ] );
+	const table = tableCells[ 0 ].findAncestor( 'table' );
 	const tableMap = [ ...new TableWalker( table ) ];
 	const tableMap = [ ...new TableWalker( table ) ];
 
 
 	const indexes = tableMap
 	const indexes = tableMap
@@ -188,6 +187,16 @@ export function isSelectionRectangular( selectedTableCells, tableUtils ) {
 	return areaOfValidSelection == areaOfSelectedCells;
 	return areaOfValidSelection == areaOfSelectedCells;
 }
 }
 
 
+/**
+ * Returns array of sorted ranges.
+ *
+ * @param {Iterable.<module:engine/model/range~Range>} ranges
+ * @return {Array.<module:engine/model/range~Range>}
+ */
+export function sortRanges( ranges ) {
+	return Array.from( ranges ).sort( compareRangeOrder );
+}
+
 // Helper method to get an object with `first` and `last` indexes from an unsorted array of indexes.
 // Helper method to get an object with `first` and `last` indexes from an unsorted array of indexes.
 function getFirstLastIndexesObject( indexes ) {
 function getFirstLastIndexesObject( indexes ) {
 	const allIndexesSorted = indexes.sort( ( indexA, indexB ) => indexA - indexB );
 	const allIndexesSorted = indexes.sort( ( indexA, indexB ) => indexA - indexB );
@@ -198,10 +207,6 @@ function getFirstLastIndexesObject( indexes ) {
 	return { first, last };
 	return { first, last };
 }
 }
 
 
-function sortRanges( rangesIterator ) {
-	return Array.from( rangesIterator ).sort( compareRangeOrder );
-}
-
 function compareRangeOrder( rangeA, rangeB ) {
 function compareRangeOrder( rangeA, rangeB ) {
 	// Since table cell ranges are disjoint, it's enough to check their start positions.
 	// Since table cell ranges are disjoint, it's enough to check their start positions.
 	const posA = rangeA.start;
 	const posA = rangeA.start;
@@ -245,7 +250,7 @@ function getBiggestRectangleArea( rows, columns ) {
 //		│ c │ c │ d │ d │
 //		│ c │ c │ d │ d │
 //		└───┴───┴───┴───┘
 //		└───┴───┴───┴───┘
 function areCellInTheSameTableSection( tableCells ) {
 function areCellInTheSameTableSection( tableCells ) {
-	const table = findAncestor( 'table', tableCells[ 0 ] );
+	const table = tableCells[ 0 ].findAncestor( 'table' );
 
 
 	const rowIndexes = getRowIndexes( tableCells );
 	const rowIndexes = getRowIndexes( tableCells );
 	const headingRows = parseInt( table.getAttribute( 'headingRows' ) || 0 );
 	const headingRows = parseInt( table.getAttribute( 'headingRows' ) || 0 );

+ 31 - 17
packages/ckeditor5-table/src/utils/structure.js

@@ -138,6 +138,7 @@ export function getVerticallyOverlappingCells( table, overlapRow, startRow = 0 )
  * @param {module:engine/model/element~Element} tableCell
  * @param {module:engine/model/element~Element} tableCell
  * @param {Number} splitRow
  * @param {Number} splitRow
  * @param {module:engine/model/writer~Writer} writer
  * @param {module:engine/model/writer~Writer} writer
+ * @returns {module:engine/model/element~Element} Created table cell.
  */
  */
 export function splitHorizontally( tableCell, splitRow, writer ) {
 export function splitHorizontally( tableCell, splitRow, writer ) {
 	const tableRow = tableCell.parent;
 	const tableRow = tableCell.parent;
@@ -164,6 +165,7 @@ export function splitHorizontally( tableCell, splitRow, writer ) {
 	const endRow = startRow + newRowspan;
 	const endRow = startRow + newRowspan;
 	const tableMap = [ ...new TableWalker( table, { startRow, endRow, includeAllSlots: true } ) ];
 	const tableMap = [ ...new TableWalker( table, { startRow, endRow, includeAllSlots: true } ) ];
 
 
+	let newCell = null;
 	let columnIndex;
 	let columnIndex;
 
 
 	for ( const tableSlot of tableMap ) {
 	for ( const tableSlot of tableMap ) {
@@ -174,12 +176,14 @@ export function splitHorizontally( tableCell, splitRow, writer ) {
 		}
 		}
 
 
 		if ( columnIndex !== undefined && columnIndex === column && row === endRow ) {
 		if ( columnIndex !== undefined && columnIndex === column && row === endRow ) {
-			createEmptyTableCell( writer, tableSlot.getPositionBefore(), newCellAttributes );
+			newCell = createEmptyTableCell( writer, tableSlot.getPositionBefore(), newCellAttributes );
 		}
 		}
 	}
 	}
 
 
 	// Update the rowspan attribute after updating table.
 	// Update the rowspan attribute after updating table.
 	updateNumericAttribute( 'rowspan', newRowspan, tableCell, writer );
 	updateNumericAttribute( 'rowspan', newRowspan, tableCell, writer );
+
+	return newCell;
 }
 }
 
 
 /**
 /**
@@ -232,6 +236,7 @@ export function getHorizontallyOverlappingCells( table, overlapColumn ) {
  * @param {Number} columnIndex The table cell column index.
  * @param {Number} columnIndex The table cell column index.
  * @param {Number} splitColumn The index of column to split cell on.
  * @param {Number} splitColumn The index of column to split cell on.
  * @param {module:engine/model/writer~Writer} writer
  * @param {module:engine/model/writer~Writer} writer
+ * @returns {module:engine/model/element~Element} Created table cell.
  */
  */
 export function splitVertically( tableCell, columnIndex, splitColumn, writer ) {
 export function splitVertically( tableCell, columnIndex, splitColumn, writer ) {
 	const colspan = parseInt( tableCell.getAttribute( 'colspan' ) );
 	const colspan = parseInt( tableCell.getAttribute( 'colspan' ) );
@@ -250,9 +255,12 @@ export function splitVertically( tableCell, columnIndex, splitColumn, writer ) {
 		newCellAttributes.rowspan = rowspan;
 		newCellAttributes.rowspan = rowspan;
 	}
 	}
 
 
-	createEmptyTableCell( writer, writer.createPositionAfter( tableCell ), newCellAttributes );
+	const newCell = createEmptyTableCell( writer, writer.createPositionAfter( tableCell ), newCellAttributes );
+
 	// Update the colspan attribute after updating table.
 	// Update the colspan attribute after updating table.
 	updateNumericAttribute( 'colspan', newColspan, tableCell, writer );
 	updateNumericAttribute( 'colspan', newColspan, tableCell, writer );
+
+	return newCell;
 }
 }
 
 
 /**
 /**
@@ -342,13 +350,17 @@ export function removeEmptyColumns( table, tableUtils ) {
 		return cellsCount ? result : [ ...result, column ];
 		return cellsCount ? result : [ ...result, column ];
 	}, [] );
 	}, [] );
 
 
-	// @if CK_DEBUG_TABLE // emptyColumns.length > 0 && console.log( `Removing empty columns: ${ emptyColumns.join( ', ' ) }.` );
+	if ( emptyColumns.length > 0 ) {
+		// Remove only last empty column because it will recurrently trigger removing empty rows.
+		const emptyColumn = emptyColumns[ emptyColumns.length - 1 ];
 
 
-	emptyColumns.reverse().forEach( column => {
-		tableUtils.removeColumns( table, { at: column } );
-	} );
+		// @if CK_DEBUG_TABLE // console.log( `Removing empty column: ${ emptyColumn }.` );
+		tableUtils.removeColumns( table, { at: emptyColumn } );
 
 
-	return emptyColumns.length > 0;
+		return true;
+	}
+
+	return false;
 }
 }
 
 
 /**
 /**
@@ -380,10 +392,9 @@ export function removeEmptyColumns( table, tableUtils ) {
  * @protected
  * @protected
  * @param {module:engine/model/element~Element} table
  * @param {module:engine/model/element~Element} table
  * @param {module:table/tableutils~TableUtils} tableUtils
  * @param {module:table/tableutils~TableUtils} tableUtils
- * @param {module:engine/model/batch~Batch|null} [batch] Batch that should be used for removing empty rows.
  * @returns {Boolean} True if removed some rows.
  * @returns {Boolean} True if removed some rows.
  */
  */
-export function removeEmptyRows( table, tableUtils, batch ) {
+export function removeEmptyRows( table, tableUtils ) {
 	const emptyRows = [];
 	const emptyRows = [];
 
 
 	for ( let rowIndex = 0; rowIndex < table.childCount; rowIndex++ ) {
 	for ( let rowIndex = 0; rowIndex < table.childCount; rowIndex++ ) {
@@ -394,13 +405,17 @@ export function removeEmptyRows( table, tableUtils, batch ) {
 		}
 		}
 	}
 	}
 
 
-	// @if CK_DEBUG_TABLE // emptyRows.length > 0 && console.log( `Removing empty rows: ${ emptyRows.join( ', ' ) }.` );
+	if ( emptyRows.length > 0 ) {
+		// Remove only last empty row because it will recurrently trigger removing empty columns.
+		const emptyRow = emptyRows[ emptyRows.length - 1 ];
+
+		// @if CK_DEBUG_TABLE // console.log( `Removing empty row: ${ emptyRow }.` );
+		tableUtils.removeRows( table, { at: emptyRow } );
 
 
-	emptyRows.reverse().forEach( row => {
-		tableUtils.removeRows( table, { at: row, batch } );
-	} );
+		return true;
+	}
 
 
-	return emptyRows.length > 0;
+	return false;
 }
 }
 
 
 /**
 /**
@@ -428,14 +443,13 @@ export function removeEmptyRows( table, tableUtils, batch ) {
  * @protected
  * @protected
  * @param {module:engine/model/element~Element} table
  * @param {module:engine/model/element~Element} table
  * @param {module:table/tableutils~TableUtils} tableUtils
  * @param {module:table/tableutils~TableUtils} tableUtils
- * @param {module:engine/model/batch~Batch|null} [batch] Batch that should be used for removing empty rows.
  */
  */
-export function removeEmptyRowsColumns( table, tableUtils, batch ) {
+export function removeEmptyRowsColumns( table, tableUtils ) {
 	const removedColumns = removeEmptyColumns( table, tableUtils );
 	const removedColumns = removeEmptyColumns( table, tableUtils );
 
 
 	// If there was some columns removed then cleaning empty rows was already triggered.
 	// If there was some columns removed then cleaning empty rows was already triggered.
 	if ( !removedColumns ) {
 	if ( !removedColumns ) {
-		removeEmptyRows( table, tableUtils, batch );
+		removeEmptyRows( table, tableUtils );
 	}
 	}
 }
 }
 
 

+ 2 - 3
packages/ckeditor5-table/src/utils/ui/contextualballoon.js

@@ -10,7 +10,6 @@
 import { centeredBalloonPositionForLongWidgets } from '@ckeditor/ckeditor5-widget/src/utils';
 import { centeredBalloonPositionForLongWidgets } from '@ckeditor/ckeditor5-widget/src/utils';
 import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect';
 import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect';
 import { getTableWidgetAncestor } from './widget';
 import { getTableWidgetAncestor } from './widget';
-import { findAncestor } from '../common';
 import BalloonPanelView from '@ckeditor/ckeditor5-ui/src/panel/balloon/balloonpanelview';
 import BalloonPanelView from '@ckeditor/ckeditor5-ui/src/panel/balloon/balloonpanelview';
 
 
 const DEFAULT_BALLOON_POSITIONS = BalloonPanelView.defaultPositions;
 const DEFAULT_BALLOON_POSITIONS = BalloonPanelView.defaultPositions;
@@ -64,7 +63,7 @@ export function repositionContextualBalloon( editor, target ) {
  */
  */
 export function getBalloonTablePositionData( editor ) {
 export function getBalloonTablePositionData( editor ) {
 	const firstPosition = editor.model.document.selection.getFirstPosition();
 	const firstPosition = editor.model.document.selection.getFirstPosition();
-	const modelTable = findAncestor( 'table', firstPosition );
+	const modelTable = firstPosition.findAncestor( 'table' );
 	const viewTable = editor.editing.mapper.toViewElement( modelTable );
 	const viewTable = editor.editing.mapper.toViewElement( modelTable );
 
 
 	return {
 	return {
@@ -113,7 +112,7 @@ export function getBalloonCellPositionData( editor ) {
 function getTableCellAtPosition( position ) {
 function getTableCellAtPosition( position ) {
 	const isTableCellSelected = position.nodeAfter && position.nodeAfter.is( 'tableCell' );
 	const isTableCellSelected = position.nodeAfter && position.nodeAfter.is( 'tableCell' );
 
 
-	return isTableCellSelected ? position.nodeAfter : findAncestor( 'tableCell', position );
+	return isTableCellSelected ? position.nodeAfter : position.findAncestor( 'tableCell' );
 }
 }
 
 
 // Returns bounding rect for list of rects.
 // Returns bounding rect for list of rects.

+ 12 - 1
packages/ckeditor5-table/src/utils/ui/widget.js

@@ -8,7 +8,6 @@
  */
  */
 
 
 import { isWidget } from '@ckeditor/ckeditor5-widget/src/utils';
 import { isWidget } from '@ckeditor/ckeditor5-widget/src/utils';
-import { findAncestor } from '../common';
 
 
 /**
 /**
  * Returns a table widget editing view element if one is selected.
  * Returns a table widget editing view element if one is selected.
@@ -49,3 +48,15 @@ export function getTableWidgetAncestor( selection ) {
 function isTableWidget( viewElement ) {
 function isTableWidget( viewElement ) {
 	return !!viewElement.getCustomProperty( 'table' ) && isWidget( viewElement );
 	return !!viewElement.getCustomProperty( 'table' ) && isWidget( viewElement );
 }
 }
+
+function findAncestor( parentName, positionOrElement ) {
+	let parent = positionOrElement.parent;
+
+	while ( parent ) {
+		if ( parent.name === parentName ) {
+			return parent;
+		}
+
+		parent = parent.parent;
+	}
+}

+ 15 - 0
packages/ckeditor5-table/tests/commands/inserttablecommand.js

@@ -100,6 +100,21 @@ describe( 'InsertTableCommand', () => {
 				);
 				);
 			} );
 			} );
 
 
+			it( 'should insert table with given heading rows and heading columns after non-empty paragraph', () => {
+				setData( model, '<paragraph>foo[]</paragraph>' );
+
+				command.execute( { rows: 3, columns: 4, headingRows: 1, headingColumns: 2 } );
+
+				assertEqualMarkup( getData( model ),
+					'<paragraph>foo</paragraph>' +
+					modelTable( [
+						[ '[]', '', '', '' ],
+						[ '', '', '', '' ],
+						[ '', '', '', '' ]
+					], { headingRows: 1, headingColumns: 2 } )
+				);
+			} );
+
 			it( 'should insert table before after non-empty paragraph if selection is inside', () => {
 			it( 'should insert table before after non-empty paragraph if selection is inside', () => {
 				setData( model, '<paragraph>f[]oo</paragraph>' );
 				setData( model, '<paragraph>f[]oo</paragraph>' );
 
 

+ 47 - 0
packages/ckeditor5-table/tests/commands/mergecellscommand.js

@@ -706,6 +706,53 @@ describe( 'MergeCellsCommand', () => {
 					]
 					]
 				] ) );
 				] ) );
 			} );
 			} );
+
+			it( 'should remove all empty rows and columns', () => {
+				setData( model, modelTable( [
+					[ '00', '01', '02' ],
+					[ '10', '11', '12' ],
+					[ '20', '21', '22' ]
+				] ) );
+
+				tableSelection.setCellSelection(
+					root.getNodeByPath( [ 0, 0, 0 ] ),
+					root.getNodeByPath( [ 0, 2, 2 ] )
+				);
+
+				command.execute();
+
+				assertEqualMarkup( getData( model ), modelTable( [
+					[
+						'<paragraph>[00</paragraph><paragraph>01</paragraph><paragraph>02</paragraph>' +
+						'<paragraph>10</paragraph><paragraph>11</paragraph><paragraph>12</paragraph>' +
+						'<paragraph>20</paragraph><paragraph>21</paragraph><paragraph>22]</paragraph>'
+					]
+				] ) );
+			} );
+
+			it( 'should remove all empty rows and columns (asymmetrical case)', () => {
+				setData( model, modelTable( [
+					[ '00', '01', { contents: '02', rowspan: 3 } ],
+					[ '10', '11' ],
+					[ '20', '21' ]
+				] ) );
+
+				tableSelection.setCellSelection(
+					root.getNodeByPath( [ 0, 0, 0 ] ),
+					root.getNodeByPath( [ 0, 2, 1 ] )
+				);
+
+				command.execute();
+
+				assertEqualMarkup( getData( model ), modelTable( [
+					[
+						'<paragraph>[00</paragraph><paragraph>01</paragraph>' +
+						'<paragraph>10</paragraph><paragraph>11</paragraph>' +
+						'<paragraph>20</paragraph><paragraph>21]</paragraph>',
+						'02'
+					]
+				] ) );
+			} );
 		} );
 		} );
 	} );
 	} );
 
 

+ 33 - 32
packages/ckeditor5-table/tests/converters/downcast.js

@@ -5,6 +5,7 @@
 
 
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import UndoEditing from '@ckeditor/ckeditor5-undo/src/undoediting';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 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 { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
@@ -19,7 +20,7 @@ describe( 'downcast converters', () => {
 	testUtils.createSinonSandbox();
 	testUtils.createSinonSandbox();
 
 
 	beforeEach( async () => {
 	beforeEach( async () => {
-		editor = await VirtualTestEditor.create( { plugins: [ Paragraph, TableEditing ] } );
+		editor = await VirtualTestEditor.create( { plugins: [ Paragraph, TableEditing, UndoEditing ] } );
 
 
 		model = editor.model;
 		model = editor.model;
 		root = model.document.getRoot( 'main' );
 		root = model.document.getRoot( 'main' );
@@ -840,8 +841,10 @@ describe( 'downcast converters', () => {
 		} );
 		} );
 	} );
 	} );
 
 
-	describe( 'downcastTableHeadingRowsChange()', () => {
+	describe( 'downcastTableHeadingRowsChange', () => {
 		// The heading rows change downcast conversion is not executed in data pipeline.
 		// The heading rows change downcast conversion is not executed in data pipeline.
+		// Note that headingRows table attribute triggers whole table downcast.
+
 		describe( 'editing pipeline', () => {
 		describe( 'editing pipeline', () => {
 			it( 'should work for adding heading rows', () => {
 			it( 'should work for adding heading rows', () => {
 				setModelData( model, modelTable( [
 				setModelData( model, modelTable( [
@@ -941,36 +944,6 @@ describe( 'downcast converters', () => {
 				], { headingRows: 2, asWidget: true } ) );
 				], { headingRows: 2, asWidget: true } ) );
 			} );
 			} );
 
 
-			it( 'should be possible to overwrite', () => {
-				editor.conversion.attributeToAttribute( {
-					model: 'headingRows',
-					view: 'headingRows',
-					converterPriority: 'high'
-				} );
-				setModelData( model, modelTable( [ [ '00' ] ] ) );
-
-				const table = root.getChild( 0 );
-
-				model.change( writer => {
-					writer.setAttribute( 'headingRows', 1, table );
-				} );
-
-				assertEqualMarkup( getViewData( view, { withoutSelection: true } ),
-					'<figure class="ck-widget ck-widget_with-selection-handle table" contenteditable="false" headingRows="1">' +
-						'<div class="ck ck-widget__selection-handle"></div>' +
-						'<table>' +
-							'<tbody>' +
-								'<tr>' +
-									'<td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true">' +
-										'<span style="display:inline-block">00</span>' +
-									'</td>' +
-								'</tr>' +
-							'</tbody>' +
-						'</table>' +
-					'</figure>'
-				);
-			} );
-
 			it( 'should work with adding table rows at the beginning of a table', () => {
 			it( 'should work with adding table rows at the beginning of a table', () => {
 				setModelData( model, modelTable( [
 				setModelData( model, modelTable( [
 					[ '00', '01' ],
 					[ '00', '01' ],
@@ -1047,6 +1020,34 @@ describe( 'downcast converters', () => {
 					'</figure>'
 					'</figure>'
 				);
 				);
 			} );
 			} );
+
+			it( 'should properly integrate with undo', () => {
+				setModelData( model, modelTable( [
+					[ '00', '01' ],
+					[ '10', '11' ],
+					[ '20', '21' ]
+				], { headingRows: 1 } ) );
+
+				const table = root.getChild( 0 );
+
+				model.change( writer => {
+					writer.setAttribute( 'headingRows', 2, table );
+				} );
+
+				assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [
+					[ '00', '01' ],
+					[ '10', '11' ],
+					[ '20', '21' ]
+				], { headingRows: 2, asWidget: true } ) );
+
+				editor.execute( 'undo' );
+
+				assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [
+					[ '00', '01' ],
+					[ '10', '11' ],
+					[ '20', '21' ]
+				], { headingRows: 1, asWidget: true } ) );
+			} );
 		} );
 		} );
 	} );
 	} );
 
 

+ 13 - 2
packages/ckeditor5-table/tests/manual/tablemocking.html

@@ -12,10 +12,16 @@
 		box-sizing: border-box;
 		box-sizing: border-box;
 		margin: 10px 0;
 		margin: 10px 0;
 	}
 	}
-	pre,code {
+	pre, code {
 		font-size: 11px;
 		font-size: 11px;
 		font-family: Menlo, Consolas, Lucida Console, Courier New, dejavu sans mono, monospace;
 		font-family: Menlo, Consolas, Lucida Console, Courier New, dejavu sans mono, monospace;
 	}
 	}
+	#table-tools pre {
+		background: hsl( 0, 0%, 95% );
+		max-height:300px;
+		overflow: auto;
+		padding: 10px;
+	}
 	.diff-add {
 	.diff-add {
 		color: hsl( 120, 70%, 35% );
 		color: hsl( 120, 70%, 35% );
 	}
 	}
@@ -61,4 +67,9 @@
 <div id="editor">
 <div id="editor">
 </div>
 </div>
 
 
-<pre id="ascii-art"></pre>
+<div id="table-tools">
+	<pre id="ascii-art"></pre>
+
+	<h3>Clipboard preview:</h3>
+	<pre id="clipboard"></pre>
+</div>

+ 5 - 2
packages/ckeditor5-table/tests/manual/tablemocking.js

@@ -15,7 +15,6 @@ import { debounce } from 'lodash-es';
 import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
 import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
 import TableWalker from '../../src/tablewalker';
 import TableWalker from '../../src/tablewalker';
 import { getSelectionAffectedTableCells } from '../../src/utils/selection';
 import { getSelectionAffectedTableCells } from '../../src/utils/selection';
-import { findAncestor } from '../../src/utils/common';
 
 
 ClassicEditor
 ClassicEditor
 	.create( document.querySelector( '#editor' ), {
 	.create( document.querySelector( '#editor' ), {
@@ -33,6 +32,10 @@ ClassicEditor
 		const asciiOut = document.getElementById( 'ascii-art' );
 		const asciiOut = document.getElementById( 'ascii-art' );
 		const modelData = document.getElementById( 'model-data' );
 		const modelData = document.getElementById( 'model-data' );
 
 
+		editor.editing.view.document.on( 'paste', ( evt, data ) => {
+			document.getElementById( 'clipboard' ).innerText = data.dataTransfer.getData( 'text/html' ).replace( />(?=<)/g, '>\n' );
+		} );
+
 		document.getElementById( 'clear-content' ).addEventListener( 'click', () => {
 		document.getElementById( 'clear-content' ).addEventListener( 'click', () => {
 			editor.setData( '' );
 			editor.setData( '' );
 		} );
 		} );
@@ -120,7 +123,7 @@ ClassicEditor
 			const tableCells = getSelectionAffectedTableCells( selection );
 			const tableCells = getSelectionAffectedTableCells( selection );
 
 
 			if ( tableCells.length ) {
 			if ( tableCells.length ) {
-				return findAncestor( 'table', tableCells[ 0 ] );
+				return tableCells[ 0 ].findAncestor( 'table' );
 			}
 			}
 
 
 			const element = selection.getSelectedElement();
 			const element = selection.getSelectedElement();

+ 0 - 2
packages/ckeditor5-table/tests/manual/tablemocking.md

@@ -22,5 +22,3 @@ setModelData( model, modelTable( [
     [ '40', '41', '42', '43', '44' ]
     [ '40', '41', '42', '43', '44' ]
 ] ) );
 ] ) );
 ```
 ```
-
-**Note:** Cell content is ignored while generating ASCII-art and `modelTableData`.  

+ 353 - 4
packages/ckeditor5-table/tests/tableclipboard-paste.js

@@ -108,7 +108,7 @@ describe( 'table clipboard', () => {
 					model.createRangeOn( modelRoot.getNodeByPath( [ 0, 1, 1 ] ) )
 					model.createRangeOn( modelRoot.getNodeByPath( [ 0, 1, 1 ] ) )
 				] );
 				] );
 
 
-				const tableToInsert = editor.plugins.get( 'TableUtils' ).createTable( writer, 2, 2 );
+				const tableToInsert = editor.plugins.get( 'TableUtils' ).createTable( writer, { rows: 2, columns: 2 } );
 
 
 				for ( const { cell } of new TableWalker( tableToInsert ) ) {
 				for ( const { cell } of new TableWalker( tableToInsert ) ) {
 					writer.insertText( 'foo', cell.getChild( 0 ), 0 );
 					writer.insertText( 'foo', cell.getChild( 0 ), 0 );
@@ -461,7 +461,7 @@ describe( 'table clipboard', () => {
 			);
 			);
 
 
 			model.change( writer => {
 			model.change( writer => {
-				const tableToInsert = editor.plugins.get( 'TableUtils' ).createTable( writer, 2, 2 );
+				const tableToInsert = editor.plugins.get( 'TableUtils' ).createTable( writer, { rows: 2, columns: 2 } );
 
 
 				for ( const { cell } of new TableWalker( tableToInsert ) ) {
 				for ( const { cell } of new TableWalker( tableToInsert ) ) {
 					writer.insertText( 'foo', cell.getChild( 0 ), 0 );
 					writer.insertText( 'foo', cell.getChild( 0 ), 0 );
@@ -3385,6 +3385,329 @@ describe( 'table clipboard', () => {
 				}
 				}
 			);
 			);
 		} );
 		} );
+
+		describe( 'headings overlapping selected area', () => {
+			beforeEach( () => {
+				setModelData( model, modelTable( [
+					[ '00', '01', '02', '03', '04', '05' ],
+					[ '10', '11', '12', '13', '14', '15' ],
+					[ '20', '21', '22', '23', '24', '25' ],
+					[ '30', '31', '32', '33', '34', '35' ],
+					[ '40', '41', '42', '43', '44', '45' ],
+					[ '50', '51', '52', '53', '54', '55' ]
+				], { headingRows: 3, headingColumns: 3 } ) );
+			} );
+
+			it( 'should not split cells if they are not overlapping from headings', () => {
+				tableSelection.setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
+					modelRoot.getNodeByPath( [ 0, 0, 0 ] )
+				);
+
+				// +----+----+----+----+
+				// | aa           | ad |
+				// +              +----+
+				// |              | bd |
+				// +              +----+
+				// |              | cd |
+				// +----+----+----+----+
+				// | da | db | dc | dd |
+				// +----+----+----+----+
+				pasteTable( [
+					[ { contents: 'aa', colspan: 3, rowspan: 3 }, 'ad' ],
+					[ 'bd' ],
+					[ 'cd' ],
+					[ 'da', 'db', 'dc', 'dd' ]
+				] );
+
+				// +----+----+----+----+----+----+
+				// | aa           | ad | 04 | 05 |
+				// +              +----+----+----+
+				// |              | bd | 14 | 15 |
+				// +              +----+----+----+
+				// |              | cd | 24 | 25 |
+				// +----+----+----+----+----+----+ <-- heading rows
+				// | da | db | dc | dd | 34 | 35 |
+				// +----+----+----+----+----+----+
+				// | 40 | 41 | 42 | 43 | 44 | 45 |
+				// +----+----+----+----+----+----+
+				// | 50 | 51 | 52 | 53 | 54 | 55 |
+				// +----+----+----+----+----+----+
+				//                ^-- heading columns
+				assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+					[ { contents: 'aa', colspan: 3, rowspan: 3 }, 'ad', '04', '05' ],
+					[ 'bd', '14', '15' ],
+					[ 'cd', '24', '25' ],
+					[ 'da', 'db', 'dc', 'dd', '34', '35' ],
+					[ '40', '41', '42', '43', '44', '45' ],
+					[ '50', '51', '52', '53', '54', '55' ]
+				], { headingRows: 3, headingColumns: 3 } ) );
+
+				assertSelectionRangesSorted();
+
+				/* eslint-disable no-multi-spaces */
+				assertSelectedCells( model, [
+					[ 1,       1, 0, 0 ],
+					[          1, 0, 0 ],
+					[          1, 0, 0 ],
+					[ 1, 1, 1, 1, 0, 0 ],
+					[ 0, 0, 0, 0, 0, 0 ],
+					[ 0, 0, 0, 0, 0, 0 ]
+				] );
+				/* eslint-enable no-multi-spaces */
+			} );
+
+			it( 'should split cells that overlap from headings', () => {
+				tableSelection.setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 1, 1 ] ),
+					modelRoot.getNodeByPath( [ 0, 1, 1 ] )
+				);
+
+				// +----+----+----+----+
+				// | aa           | ad |
+				// +              +----+
+				// |              | bd |
+				// +              +----+
+				// |              | cd |
+				// +----+----+----+----+
+				// | da | db | dc | dd |
+				// +----+----+----+----+
+				pasteTable( [
+					[ { contents: 'aa', colspan: 3, rowspan: 3 }, 'ad' ],
+					[ 'bd' ],
+					[ 'cd' ],
+					[ 'da', 'db', 'dc', 'dd' ]
+				] );
+
+				// +----+----+----+----+----+----+
+				// | 00 | 01 | 02 | 03 | 04 | 05 |
+				// +----+----+----+----+----+----+
+				// | 10 | aa      |    | ad | 15 |
+				// +----+         +    +----+----+
+				// | 20 |         |    | bd | 25 |
+				// +----+----+----+----+----+----+ <-- heading rows
+				// | 30 |         |    | cd | 35 |
+				// +----+----+----+----+----+----+
+				// | 40 | da | db | dc | dd | 45 |
+				// +----+----+----+----+----+----+
+				// | 50 | 51 | 52 | 53 | 54 | 55 |
+				// +----+----+----+----+----+----+
+				//                ^-- heading columns
+				assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+					[ '00', '01', '02', '03', '04', '05' ],
+					[ '10', { contents: 'aa', colspan: 2, rowspan: 2 }, { contents: '', rowspan: 2 }, 'ad', '15' ],
+					[ '20', 'bd', '25' ],
+					[ '30', { contents: '', colspan: 2 }, '', 'cd', '35' ],
+					[ '40', 'da', 'db', 'dc', 'dd', '45' ],
+					[ '50', '51', '52', '53', '54', '55' ]
+				], { headingRows: 3, headingColumns: 3 } ) );
+
+				assertSelectionRangesSorted();
+
+				/* eslint-disable no-multi-spaces */
+				assertSelectedCells( model, [
+					[ 0, 0, 0, 0, 0, 0 ],
+					[ 0, 1,    1, 1, 0 ],
+					[ 0,          1, 0 ],
+					[ 0, 1,    1, 1, 0 ],
+					[ 0, 1, 1, 1, 1, 0 ],
+					[ 0, 0, 0, 0, 0, 0 ]
+				] );
+				/* eslint-enable no-multi-spaces */
+			} );
+
+			it( 'should split cells that overlap from heading rows', () => {
+				tableSelection.setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 2, 3 ] ),
+					modelRoot.getNodeByPath( [ 0, 2, 3 ] )
+				);
+
+				// +----+----+----+----+
+				// | aa           | ad |
+				// +              +----+
+				// |              | bd |
+				// +              +----+
+				// |              | cd |
+				// +----+----+----+----+
+				// | da | db | dc | dd |
+				// +----+----+----+----+
+				pasteTable( [
+					[ { contents: 'aa', colspan: 3, rowspan: 3 }, 'ad' ],
+					[ 'bd' ],
+					[ 'cd' ],
+					[ 'da', 'db', 'dc', 'dd' ]
+				] );
+
+				// +----+----+----+----+----+----+----+
+				// | 00 | 01 | 02 | 03 | 04 | 05 |    |
+				// +----+----+----+----+----+----+----+
+				// | 10 | 11 | 12 | 13 | 14 | 15 |    |
+				// +----+----+----+----+----+----+----+
+				// | 20 | 21 | 22 | aa           | ad |
+				// +----+----+----+----+----+----+----+ <-- heading rows
+				// | 30 | 31 | 32 |              | bd |
+				// +----+----+----+              +----+
+				// | 40 | 41 | 42 |              | cd |
+				// +----+----+----+----+----+----+----+
+				// | 50 | 51 | 52 | da | db | dc | dd |
+				// +----+----+----+----+----+----+----+
+				//                ^-- heading columns
+				assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+					[ '00', '01', '02', '03', '04', '05', '' ],
+					[ '10', '11', '12', '13', '14', '15', '' ],
+					[ '20', '21', '22', { contents: 'aa', colspan: 3 }, 'ad' ],
+					[ '30', '31', '32', { contents: '', colspan: 3, rowspan: 2 }, 'bd' ],
+					[ '40', '41', '42', 'cd' ],
+					[ '50', '51', '52', 'da', 'db', 'dc', 'dd' ]
+				], { headingRows: 3, headingColumns: 3 } ) );
+
+				assertSelectionRangesSorted();
+
+				/* eslint-disable no-multi-spaces */
+				assertSelectedCells( model, [
+					[ 0, 0, 0, 0, 0, 0, 0 ],
+					[ 0, 0, 0, 0, 0, 0, 0 ],
+					[ 0, 0, 0, 1,       1 ],
+					[ 0, 0, 0, 1,       1 ],
+					[ 0, 0, 0,          1 ],
+					[ 0, 0, 0, 1, 1, 1, 1 ]
+				] );
+				/* eslint-enable no-multi-spaces */
+			} );
+
+			it( 'should split cells that overlap from heading columns', () => {
+				tableSelection.setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 3, 2 ] ),
+					modelRoot.getNodeByPath( [ 0, 3, 2 ] )
+				);
+
+				// +----+----+----+----+
+				// | aa           | ad |
+				// +              +----+
+				// |              | bd |
+				// +              +----+
+				// |              | cd |
+				// +----+----+----+----+
+				// | da | db | dc | dd |
+				// +----+----+----+----+
+				pasteTable( [
+					[ { contents: 'aa', colspan: 3, rowspan: 3 }, 'ad' ],
+					[ 'bd' ],
+					[ 'cd' ],
+					[ 'da', 'db', 'dc', 'dd' ]
+				] );
+
+				// +----+----+----+----+----+----+
+				// | 00 | 01 | 02 | 03 | 04 | 05 |
+				// +----+----+----+----+----+----+
+				// | 10 | 11 | 12 | 13 | 14 | 15 |
+				// +----+----+----+----+----+----+
+				// | 20 | 21 | 22 | 23 | 24 | 25 |
+				// +----+----+----+----+----+----+ <-- heading rows
+				// | 30 | 31 | aa |         | ad |
+				// +----+----+    +         +----+
+				// | 40 | 41 |    |         | bd |
+				// +----+----+    +         +----+
+				// | 50 | 51 |    |         | cd |
+				// +----+----+----+----+----+----+
+				// |    |    | da | db | dc | dd |
+				// +----+----+----+----+----+----+
+				//                ^-- heading columns
+				assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+					[ '00', '01', '02', '03', '04', '05' ],
+					[ '10', '11', '12', '13', '14', '15' ],
+					[ '20', '21', '22', '23', '24', '25' ],
+					[ '30', '31', { contents: 'aa', rowspan: 3 }, { contents: '', colspan: 2, rowspan: 3 }, 'ad' ],
+					[ '40', '41', 'bd' ],
+					[ '50', '51', 'cd' ],
+					[ '', '', 'da', 'db', 'dc', 'dd' ]
+				], { headingRows: 3, headingColumns: 3 } ) );
+
+				assertSelectionRangesSorted();
+
+				/* eslint-disable no-multi-spaces */
+				assertSelectedCells( model, [
+					[ 0, 0, 0, 0, 0, 0 ],
+					[ 0, 0, 0, 0, 0, 0 ],
+					[ 0, 0, 0, 0, 0, 0 ],
+					[ 0, 0, 1, 1,    1 ],
+					[ 0, 0,          1 ],
+					[ 0, 0,          1 ],
+					[ 0, 0, 1, 1, 1, 1 ]
+				] );
+				/* eslint-enable no-multi-spaces */
+			} );
+
+			it( 'should split cells that overlap from headings (repeated pasted table)', () => {
+				setModelData( model, modelTable( [
+					[ '00', '01', '02', '03', '04' ],
+					[ '10', '11', '12', '13', '14' ],
+					[ '20', '21', '22', '23', '24' ],
+					[ '30', '31', '32', '33', '34' ],
+					[ '40', '41', '42', '43', '44' ]
+				], { headingRows: 1, headingColumns: 1 } ) );
+
+				tableSelection.setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
+					modelRoot.getNodeByPath( [ 0, 4, 4 ] )
+				);
+
+				// +----+----+----+
+				// | aa      | ac |
+				// +         +----+
+				// |         | bc |
+				// +----+----+----+
+				// | ca | cb | cc |
+				// +----+----+----+
+				pasteTable( [
+					[ { contents: 'aa', colspan: 2, rowspan: 2 }, 'ac' ],
+					[ 'bc' ],
+					[ 'ca', 'cb', 'cc' ]
+				] );
+
+				// +----+----+----+----+----+
+				// | aa |    | ac | aa      |
+				// +----+----+----+----+----+ <-- heading rows
+				// |    |    | bc |         |
+				// +----+----+----+----+----+
+				// | ca | cb | cc | ca | cb |
+				// +----+----+----+----+----+
+				// | aa |    | ac | aa      |
+				// +    +    +----+         +
+				// |    |    | bc |         |
+				// +----+----+----+----+----+
+				//      ^-- heading columns
+				assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+					[ 'aa', '', 'ac', { contents: 'aa', colspan: 2 } ],
+					[ '', '', 'bc', { contents: '', colspan: 2 } ],
+					[ 'ca', 'cb', 'cc', 'ca', 'cb' ],
+					[ { contents: 'aa', rowspan: 2 }, { contents: '', rowspan: 2 }, 'ac', { contents: 'aa', colspan: 2, rowspan: 2 } ],
+					[ 'bc' ]
+				], { headingRows: 1, headingColumns: 1 } ) );
+
+				assertSelectionRangesSorted();
+
+				/* eslint-disable no-multi-spaces */
+				assertSelectedCells( model, [
+					[ 1, 1, 1, 1    ],
+					[ 1, 1, 1, 1    ],
+					[ 1, 1, 1, 1, 1 ],
+					[ 1, 1, 1, 1    ],
+					[       1       ]
+				] );
+				/* eslint-enable no-multi-spaces */
+			} );
+
+			function assertSelectionRangesSorted() {
+				const selectionRanges = Array.from( model.document.selection.getRanges() );
+				const selectionRangesSorted = selectionRanges.slice().sort( ( a, b ) => a.start.isBefore( b.start ) ? -1 : 1 );
+
+				const selectionPaths = selectionRanges.map( ( { start } ) => start.path );
+				const sortedPaths = selectionRangesSorted.map( ( { start } ) => start.path );
+
+				expect( selectionPaths ).to.deep.equal( sortedPaths );
+			}
+		} );
 	} );
 	} );
 
 
 	describe( 'Clipboard integration - paste (content scenarios)', () => {
 	describe( 'Clipboard integration - paste (content scenarios)', () => {
@@ -3553,6 +3876,32 @@ describe( 'table clipboard', () => {
 				[ '02', '21', '22' ]
 				[ '02', '21', '22' ]
 			] ) );
 			] ) );
 		} );
 		} );
+
+		it( 'removes block fillers from empty cells (both td and th)', async () => {
+			await createEditor();
+
+			setModelData( model, modelTable( [
+				[ '00', '01', '02' ],
+				[ '01', '11', '12' ],
+				[ '02', '21', '22' ]
+			] ) );
+
+			tableSelection.setCellSelection(
+				modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
+				modelRoot.getNodeByPath( [ 0, 1, 1 ] )
+			);
+
+			pasteTable( [
+				[ '&nbsp;', '&nbsp;' ],
+				[ '&nbsp;', '&nbsp;' ]
+			], { headingRows: 1 } );
+
+			assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+				[ '', '', '02' ],
+				[ '', '', '12' ],
+				[ '02', '21', '22' ]
+			] ) );
+		} );
 	} );
 	} );
 
 
 	async function createEditor( extraPlugins = [] ) {
 	async function createEditor( extraPlugins = [] ) {
@@ -3566,13 +3915,13 @@ describe( 'table clipboard', () => {
 		tableSelection = editor.plugins.get( 'TableSelection' );
 		tableSelection = editor.plugins.get( 'TableSelection' );
 	}
 	}
 
 
-	function pasteTable( tableData ) {
+	function pasteTable( tableData, attributes = {} ) {
 		const data = {
 		const data = {
 			dataTransfer: createDataTransfer(),
 			dataTransfer: createDataTransfer(),
 			preventDefault: sinon.spy(),
 			preventDefault: sinon.spy(),
 			stopPropagation: sinon.spy()
 			stopPropagation: sinon.spy()
 		};
 		};
-		data.dataTransfer.setData( 'text/html', viewTable( tableData ) );
+		data.dataTransfer.setData( 'text/html', viewTable( tableData, attributes ) );
 		viewDocument.fire( 'paste', data );
 		viewDocument.fire( 'paste', data );
 
 
 		return data;
 		return data;

+ 81 - 0
packages/ckeditor5-table/tests/tableutils.js

@@ -959,6 +959,21 @@ describe( 'TableUtils', () => {
 				], { headingRows: 1 } ) );
 				], { headingRows: 1 } ) );
 			} );
 			} );
 
 
+			it( 'should change heading rows if removing a heading row (and cell below is row-spanned)', () => {
+				setData( model, modelTable( [
+					[ '00', '01' ],
+					[ '10', { contents: '11', rowspan: 2 } ],
+					[ '20' ]
+				], { headingRows: 1 } ) );
+
+				tableUtils.removeRows( root.getChild( 0 ), { at: 0 } );
+
+				assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [
+					[ '10', { contents: '11', rowspan: 2 } ],
+					[ '20' ]
+				] ) );
+			} );
+
 			it( 'should decrease rowspan of table cells from previous rows', () => {
 			it( 'should decrease rowspan of table cells from previous rows', () => {
 				// +----+----+----+----+----+
 				// +----+----+----+----+----+
 				// | 00 | 01 | 02 | 03 | 04 |
 				// | 00 | 01 | 02 | 03 | 04 |
@@ -1511,4 +1526,70 @@ describe( 'TableUtils', () => {
 			} );
 			} );
 		} );
 		} );
 	} );
 	} );
+
+	describe( 'createTable()', () => {
+		it( 'should create table', () => {
+			setData( model, '[]' );
+
+			model.change( writer => {
+				const table = tableUtils.createTable( writer, { rows: 3, columns: 2 } );
+
+				model.insertContent( table, model.document.selection.focus );
+			} );
+
+			assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [
+				[ '', '' ],
+				[ '', '' ],
+				[ '', '' ]
+			] ) );
+		} );
+
+		it( 'should create table with heading rows', () => {
+			setData( model, '[]' );
+
+			model.change( writer => {
+				const table = tableUtils.createTable( writer, { rows: 3, columns: 2, headingRows: 1 } );
+
+				model.insertContent( table, model.document.selection.focus );
+			} );
+
+			assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [
+				[ '', '' ],
+				[ '', '' ],
+				[ '', '' ]
+			], { headingRows: 1 } ) );
+		} );
+
+		it( 'should create table with heading columns', () => {
+			setData( model, '[]' );
+
+			model.change( writer => {
+				const table = tableUtils.createTable( writer, { rows: 3, columns: 2, headingColumns: 1 } );
+
+				model.insertContent( table, model.document.selection.focus );
+			} );
+
+			assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [
+				[ '', '' ],
+				[ '', '' ],
+				[ '', '' ]
+			], { headingColumns: 1 } ) );
+		} );
+
+		it( 'should create table with heading rows and columns', () => {
+			setData( model, '[]' );
+
+			model.change( writer => {
+				const table = tableUtils.createTable( writer, { rows: 3, columns: 2, headingRows: 2, headingColumns: 1 } );
+
+				model.insertContent( table, model.document.selection.focus );
+			} );
+
+			assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [
+				[ '', '' ],
+				[ '', '' ],
+				[ '', '' ]
+			], { headingRows: 2, headingColumns: 1 } ) );
+		} );
+	} );
 } );
 } );

+ 1 - 18
packages/ckeditor5-table/tests/utils/common.js

@@ -10,7 +10,7 @@ import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import TableEditing from '../../src/tableediting';
 import TableEditing from '../../src/tableediting';
 import { modelTable } from '../_utils/utils';
 import { modelTable } from '../_utils/utils';
 
 
-import { findAncestor, isHeadingColumnCell } from '../../src/utils/common';
+import { isHeadingColumnCell } from '../../src/utils/common';
 
 
 describe( 'table utils', () => {
 describe( 'table utils', () => {
 	let editor, model, modelRoot, tableUtils;
 	let editor, model, modelRoot, tableUtils;
@@ -32,23 +32,6 @@ describe( 'table utils', () => {
 	} );
 	} );
 
 
 	describe( 'common', () => {
 	describe( 'common', () => {
-		describe( 'findAncestor()', () => {
-			it( 'should return undefined if not in table', () => {
-				setData( model, '<paragraph>foo[]</paragraph>' );
-
-				expect( findAncestor( 'table', model.document.selection.focus ) ).to.be.undefined;
-			} );
-
-			it( 'should return table if position is in tableCell', () => {
-				setData( model, modelTable( [ [ '[]' ] ] ) );
-
-				const parentTable = findAncestor( 'table', model.document.selection.focus );
-
-				expect( parentTable ).to.not.be.undefined;
-				expect( parentTable.is( 'table' ) ).to.be.true;
-			} );
-		} );
-
 		describe( 'isHeadingColumnCell()', () => {
 		describe( 'isHeadingColumnCell()', () => {
 			it( 'should return "true" for a heading column cell', () => {
 			it( 'should return "true" for a heading column cell', () => {
 				setData( model, modelTable( [
 				setData( model, modelTable( [

+ 1 - 2
packages/ckeditor5-table/tests/utils/ui/contextualballoon.js

@@ -16,7 +16,6 @@ import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import { centeredBalloonPositionForLongWidgets } from '@ckeditor/ckeditor5-widget/src/utils';
 import { centeredBalloonPositionForLongWidgets } from '@ckeditor/ckeditor5-widget/src/utils';
 import { modelTable } from '../../_utils/utils';
 import { modelTable } from '../../_utils/utils';
 import { getTableCellsContainingSelection } from '../../../src/utils/selection';
 import { getTableCellsContainingSelection } from '../../../src/utils/selection';
-import { findAncestor } from '../../../src/utils/common';
 import { getBalloonCellPositionData, repositionContextualBalloon } from '../../../src/utils/ui/contextualballoon';
 import { getBalloonCellPositionData, repositionContextualBalloon } from '../../../src/utils/ui/contextualballoon';
 
 
 describe( 'table utils', () => {
 describe( 'table utils', () => {
@@ -117,7 +116,7 @@ describe( 'table utils', () => {
 						'</tableRow></table>' );
 						'</tableRow></table>' );
 					repositionContextualBalloon( editor, 'table' );
 					repositionContextualBalloon( editor, 'table' );
 
 
-					const modelTable = findAncestor( 'table', editor.model.document.selection.getFirstPosition() );
+					const modelTable = editor.model.document.selection.getFirstPosition().findAncestor( 'table' );
 					const viewTable = editor.editing.mapper.toViewElement( modelTable );
 					const viewTable = editor.editing.mapper.toViewElement( modelTable );
 
 
 					sinon.assert.calledWithExactly( spy, {
 					sinon.assert.calledWithExactly( spy, {

BIN
packages/ckeditor5-widget/docs/assets/img/framework-deep-dive-widget-type-around-buttons.gif


+ 67 - 0
packages/ckeditor5-widget/docs/framework/guides/deep-dive/widget-internals.md

@@ -0,0 +1,67 @@
+---
+category: framework-deep-dive-ui
+menu-title: Widget internals
+---
+
+# Deep dive into widget internals
+
+## Disabling the widget type around feature
+
+The {@link module:widget/widgettypearound~WidgetTypeAround `WidgetTypeAround`} plugin allows users to type around widgets where normally it is impossible to place the caret due to limitations of web browsers. Although this feature boosts the productivity, some integrations may not want or need it, for instance:
+
+* when the UI of the feature collides with the integration,
+* when typing outside widgets should be impossible (content made exclusively of widgets),
+* when certain widgets have a fixed location in the document that should never change.
+
+In the sections that follows, you will learn how to configure the editor to address these specific cases.
+
+### Hiding the buttons that insert paragraphs
+
+{@img assets/img/framework-deep-dive-widget-type-around-buttons.gif A screenshot showing the location of the buttons that insert paragraphs around widgets.}
+
+The easiest way to get rid of the type around buttons is to hide them using CSS. Put the following code snippet anywhere in your application and the buttons will no longer bother the users:
+
+```css
+.ck.ck-editor__editable .ck-widget .ck-widget__type-around__button {
+	display: none;
+}
+```
+
+If you only want to customize the type around buttons you can use the same CSS selector to modify their look or the position.
+
+<info-box hint>
+	Hiding the type around buttons does not disable the feature. Users will still be able to activate the caret before or after individual widgets using the keyboard and start typing. [Learn how to disable the entire feature](#disabling-the-entire-feature).
+</info-box>
+
+### Disabling the entire feature
+
+Although the {@link module:widget/widgettypearound~WidgetTypeAround `WidgetTypeAround`} plugin is an integral part of the {@link module:widget/widget~Widget widget} sub–system and loaded by default whenever an editor feature uses widgets (for instance, {@link features/image images} or {@link features/table tables}), you can still disable on the fly. Disabling the feature will both hide the buttons in the widgets and disable other behaviors, for instance:
+
+* the caret will not be rendered before or after a widget when a user navigates the document using arrow keys,
+* the <kbd>Enter</kbd> and <kbd>Shift</kbd>+<kbd>Enter</kbd> keystrokes will no longer insert paragraphs if pressed when a widget is selected.
+
+Use the {@link module:core/plugin~Plugin#forceDisabled `forceDisabled()`} method of the plugin to disable it on the fly like in the snippet below:
+
+```js
+ClassicEditor
+	.create( document.querySelector( '#editor' ), {
+		// ...
+	} )
+	.then( editor => {
+		const widgetTypeAroundPlugin = editor.plugins.get( 'WidgetTypeAround' );
+
+		// Disable the WidgetTypeAround plugin.
+		widgetTypeAroundPlugin.forceDisabled( 'MyApplication' );
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );
+```
+
+<info-box hint>
+	You can always re–enable the plugin using the following snippet
+	```js
+	widgetTypeAroundPlugin.clearForceDisabled( 'MyApplication' );
+	```
+	Please refer to the {@link module:core/plugin~Plugin#clearForceDisabled API documentation} to learn more.
+</info-box>

+ 1 - 0
packages/ckeditor5-widget/package.json

@@ -27,6 +27,7 @@
     "@ckeditor/ckeditor5-heading": "^20.0.0",
     "@ckeditor/ckeditor5-heading": "^20.0.0",
     "@ckeditor/ckeditor5-horizontal-line": "^20.0.0",
     "@ckeditor/ckeditor5-horizontal-line": "^20.0.0",
     "@ckeditor/ckeditor5-image": "^20.0.0",
     "@ckeditor/ckeditor5-image": "^20.0.0",
+    "@ckeditor/ckeditor5-link": "^20.0.0",
     "@ckeditor/ckeditor5-media-embed": "^20.0.0",
     "@ckeditor/ckeditor5-media-embed": "^20.0.0",
     "@ckeditor/ckeditor5-paragraph": "^20.0.0",
     "@ckeditor/ckeditor5-paragraph": "^20.0.0",
     "@ckeditor/ckeditor5-table": "^20.0.0",
     "@ckeditor/ckeditor5-table": "^20.0.0",

+ 6 - 3
packages/ckeditor5-widget/src/widget.js

@@ -146,12 +146,15 @@ export default class Widget extends Plugin {
 			// But at least triple click inside nested editable causes broken selection in Safari.
 			// But at least triple click inside nested editable causes broken selection in Safari.
 			// For such event, we select the entire nested editable element.
 			// For such event, we select the entire nested editable element.
 			// See: https://github.com/ckeditor/ckeditor5/issues/1463.
 			// See: https://github.com/ckeditor/ckeditor5/issues/1463.
-			if ( env.isSafari && domEventData.domEvent.detail >= 3 ) {
+			if ( ( env.isSafari || env.isGecko ) && domEventData.domEvent.detail >= 3 ) {
 				const mapper = editor.editing.mapper;
 				const mapper = editor.editing.mapper;
-				const modelElement = mapper.toModelElement( element );
+				const viewElement = element.is( 'attributeElement' ) ?
+					element.findAncestor( element => !element.is( 'attributeElement' ) ) : element;
+				const modelElement = mapper.toModelElement( viewElement );
+
+				domEventData.preventDefault();
 
 
 				this.editor.model.change( writer => {
 				this.editor.model.change( writer => {
-					domEventData.preventDefault();
 					writer.setSelection( modelElement, 'in' );
 					writer.setSelection( modelElement, 'in' );
 				} );
 				} );
 			}
 			}

+ 57 - 11
packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js

@@ -39,6 +39,8 @@ const POSSIBLE_INSERTION_POSITIONS = [ 'before', 'after' ];
 // Do the SVG parsing once and then clone the result <svg> DOM element for each new button.
 // Do the SVG parsing once and then clone the result <svg> DOM element for each new button.
 const RETURN_ARROW_ICON_ELEMENT = new DOMParser().parseFromString( returnIcon, 'image/svg+xml' ).firstChild;
 const RETURN_ARROW_ICON_ELEMENT = new DOMParser().parseFromString( returnIcon, 'image/svg+xml' ).firstChild;
 
 
+const PLUGIN_DISABLED_EDITING_ROOT_CLASS = 'ck-widget__type-around_disabled';
+
 /**
 /**
  * A plugin that allows users to type around widgets where normally it is impossible to place the caret due
  * A plugin that allows users to type around widgets where normally it is impossible to place the caret due
  * to limitations of web browsers. These "tight spots" occur, for instance, before (or after) a widget being
  * to limitations of web browsers. These "tight spots" occur, for instance, before (or after) a widget being
@@ -50,7 +52,6 @@ const RETURN_ARROW_ICON_ELEMENT = new DOMParser().parseFromString( returnIcon, '
  * in it so that users can type (or insert content, paste, etc.) straight away.
  * in it so that users can type (or insert content, paste, etc.) straight away.
  *
  *
  * @extends module:core/plugin~Plugin
  * @extends module:core/plugin~Plugin
- * @private
  */
  */
 export default class WidgetTypeAround extends Plugin {
 export default class WidgetTypeAround extends Plugin {
 	/**
 	/**
@@ -81,6 +82,29 @@ export default class WidgetTypeAround extends Plugin {
 	 * @inheritDoc
 	 * @inheritDoc
 	 */
 	 */
 	init() {
 	init() {
+		const editor = this.editor;
+		const editingView = editor.editing.view;
+
+		// Set a CSS class on the view editing root when the plugin is disabled so all the buttons
+		// and lines visually disappear. All the interactions are disabled in individual plugin methods.
+		this.on( 'change:isEnabled', ( evt, data, isEnabled ) => {
+			editingView.change( writer => {
+				for ( const root of editingView.document.roots ) {
+					if ( isEnabled ) {
+						writer.removeClass( PLUGIN_DISABLED_EDITING_ROOT_CLASS, root );
+					} else {
+						writer.addClass( PLUGIN_DISABLED_EDITING_ROOT_CLASS, root );
+					}
+				}
+			} );
+
+			if ( !isEnabled ) {
+				editor.model.change( writer => {
+					writer.removeSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE );
+				} );
+			}
+		} );
+
 		this._enableTypeAroundUIInjection();
 		this._enableTypeAroundUIInjection();
 		this._enableInsertingParagraphsOnButtonClick();
 		this._enableInsertingParagraphsOnButtonClick();
 		this._enableInsertingParagraphsOnEnterKeypress();
 		this._enableInsertingParagraphsOnEnterKeypress();
@@ -119,6 +143,28 @@ export default class WidgetTypeAround extends Plugin {
 		editingView.scrollToTheSelection();
 		editingView.scrollToTheSelection();
 	}
 	}
 
 
+	/**
+	 * A wrapper for the {@link module:utils/emittermixin~EmitterMixin#listenTo} method that executes the callbacks only
+	 * when the plugin {@link #isEnabled is enabled}.
+	 *
+	 * @private
+	 * @param {module:utils/emittermixin~Emitter} emitter The object that fires the event.
+	 * @param {String} event The name of the event.
+	 * @param {Function} callback The function to be called on event.
+	 * @param {Object} [options={}] Additional options.
+	 * @param {module:utils/priorities~PriorityString|Number} [options.priority='normal'] The priority of this event callback. The higher
+	 * the priority value the sooner the callback will be fired. Events having the same priority are called in the
+	 * order they were added.
+	 */
+	_listenToIfEnabled( emitter, event, callback, options ) {
+		this.listenTo( emitter, event, ( ...args ) => {
+			// Do not respond if the plugin is disabled.
+			if ( this.isEnabled ) {
+				callback( ...args );
+			}
+		}, options );
+	}
+
 	/**
 	/**
 	 * Similar to {@link #_insertParagraph}, this method inserts a paragraph except that it
 	 * Similar to {@link #_insertParagraph}, this method inserts a paragraph except that it
 	 * does not expect a position but it performs the insertion next to a selected widget
 	 * does not expect a position but it performs the insertion next to a selected widget
@@ -213,7 +259,7 @@ export default class WidgetTypeAround extends Plugin {
 		// This is the main listener responsible for the "fake caret".
 		// This is the main listener responsible for the "fake caret".
 		// Note: The priority must precede the default Widget class keydown handler ("high") and the
 		// Note: The priority must precede the default Widget class keydown handler ("high") and the
 		// TableKeyboard keydown handler ("high-10").
 		// TableKeyboard keydown handler ("high-10").
-		editingView.document.on( 'keydown', ( evt, domEventData ) => {
+		this._listenToIfEnabled( editingView.document, 'keydown', ( evt, domEventData ) => {
 			if ( isArrowKeyCode( domEventData.keyCode ) ) {
 			if ( isArrowKeyCode( domEventData.keyCode ) ) {
 				this._handleArrowKeyPress( evt, domEventData );
 				this._handleArrowKeyPress( evt, domEventData );
 			}
 			}
@@ -223,7 +269,7 @@ export default class WidgetTypeAround extends Plugin {
 		// selection as soon as the model range changes. This attribute only makes sense when a widget is selected
 		// selection as soon as the model range changes. This attribute only makes sense when a widget is selected
 		// (and the "fake horizontal caret" is visible) so whenever the range changes (e.g. selection moved somewhere else),
 		// (and the "fake horizontal caret" is visible) so whenever the range changes (e.g. selection moved somewhere else),
 		// let's get rid of the attribute so that the selection downcast dispatcher isn't even bothered.
 		// let's get rid of the attribute so that the selection downcast dispatcher isn't even bothered.
-		modelSelection.on( 'change:range', ( evt, data ) => {
+		this._listenToIfEnabled( modelSelection, 'change:range', ( evt, data ) => {
 			// Do not reset the selection attribute when the change was indirect.
 			// Do not reset the selection attribute when the change was indirect.
 			if ( !data.directChange ) {
 			if ( !data.directChange ) {
 				return;
 				return;
@@ -238,7 +284,7 @@ export default class WidgetTypeAround extends Plugin {
 
 
 		// Get rid of the widget type around attribute of the selection on every document change
 		// Get rid of the widget type around attribute of the selection on every document change
 		// that makes widget not selected any more (i.e. widget was removed).
 		// that makes widget not selected any more (i.e. widget was removed).
-		model.document.on( 'change:data', () => {
+		this._listenToIfEnabled( model.document, 'change:data', () => {
 			const selectedModelElement = modelSelection.getSelectedElement();
 			const selectedModelElement = modelSelection.getSelectedElement();
 
 
 			if ( selectedModelElement ) {
 			if ( selectedModelElement ) {
@@ -257,7 +303,7 @@ export default class WidgetTypeAround extends Plugin {
 		// React to changes of the model selection attribute made by the arrow keys listener.
 		// React to changes of the model selection attribute made by the arrow keys listener.
 		// If the block widget is selected and the attribute changes, downcast the attribute to special
 		// If the block widget is selected and the attribute changes, downcast the attribute to special
 		// CSS classes associated with the active ("fake horizontal caret") mode of the widget.
 		// CSS classes associated with the active ("fake horizontal caret") mode of the widget.
-		editor.editing.downcastDispatcher.on( 'selection', ( evt, data, conversionApi ) => {
+		this._listenToIfEnabled( editor.editing.downcastDispatcher, 'selection', ( evt, data, conversionApi ) => {
 			const writer = conversionApi.writer;
 			const writer = conversionApi.writer;
 
 
 			if ( this._currentFakeCaretModelElement ) {
 			if ( this._currentFakeCaretModelElement ) {
@@ -296,7 +342,7 @@ export default class WidgetTypeAround extends Plugin {
 			this._currentFakeCaretModelElement = selectedModelElement;
 			this._currentFakeCaretModelElement = selectedModelElement;
 		} );
 		} );
 
 
-		this.listenTo( editor.ui.focusTracker, 'change:isFocused', ( evt, name, isFocused ) => {
+		this._listenToIfEnabled( editor.ui.focusTracker, 'change:isFocused', ( evt, name, isFocused ) => {
 			if ( !isFocused ) {
 			if ( !isFocused ) {
 				editor.model.change( writer => {
 				editor.model.change( writer => {
 					writer.removeSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE );
 					writer.removeSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE );
@@ -450,7 +496,7 @@ export default class WidgetTypeAround extends Plugin {
 		const editor = this.editor;
 		const editor = this.editor;
 		const editingView = editor.editing.view;
 		const editingView = editor.editing.view;
 
 
-		editingView.document.on( 'mousedown', ( evt, domEventData ) => {
+		this._listenToIfEnabled( editingView.document, 'mousedown', ( evt, domEventData ) => {
 			const button = getClosestTypeAroundDomButton( domEventData.domTarget );
 			const button = getClosestTypeAroundDomButton( domEventData.domTarget );
 
 
 			if ( !button ) {
 			if ( !button ) {
@@ -487,7 +533,7 @@ export default class WidgetTypeAround extends Plugin {
 		const editor = this.editor;
 		const editor = this.editor;
 		const editingView = editor.editing.view;
 		const editingView = editor.editing.view;
 
 
-		this.listenTo( editingView.document, 'enter', ( evt, domEventData ) => {
+		this._listenToIfEnabled( editingView.document, 'enter', ( evt, domEventData ) => {
 			const selectedViewElement = editingView.document.selection.getSelectedElement();
 			const selectedViewElement = editingView.document.selection.getSelectedElement();
 			const selectedModelElement = editor.editing.mapper.toModelElement( selectedViewElement );
 			const selectedModelElement = editor.editing.mapper.toModelElement( selectedViewElement );
 			const schema = editor.model.schema;
 			const schema = editor.model.schema;
@@ -543,7 +589,7 @@ export default class WidgetTypeAround extends Plugin {
 
 
 		// Note: The priority must precede the default Widget class keydown handler ("high") and the
 		// Note: The priority must precede the default Widget class keydown handler ("high") and the
 		// TableKeyboard keydown handler ("high + 1").
 		// TableKeyboard keydown handler ("high + 1").
-		editingView.document.on( 'keydown', ( evt, domEventData ) => {
+		this._listenToIfEnabled( editingView.document, 'keydown', ( evt, domEventData ) => {
 			// Don't handle enter/backspace/delete here. They are handled in dedicated listeners.
 			// Don't handle enter/backspace/delete here. They are handled in dedicated listeners.
 			if ( !keyCodesHandledSomewhereElse.includes( domEventData.keyCode ) && !isNonTypingKeystroke( domEventData ) ) {
 			if ( !keyCodesHandledSomewhereElse.includes( domEventData.keyCode ) && !isNonTypingKeystroke( domEventData ) ) {
 				this._insertParagraphAccordingToFakeCaretPosition();
 				this._insertParagraphAccordingToFakeCaretPosition();
@@ -568,7 +614,7 @@ export default class WidgetTypeAround extends Plugin {
 		const schema = model.schema;
 		const schema = model.schema;
 
 
 		// Note: The priority must precede the default Widget class delete handler.
 		// Note: The priority must precede the default Widget class delete handler.
-		this.listenTo( editingView.document, 'delete', ( evt, domEventData ) => {
+		this._listenToIfEnabled( editingView.document, 'delete', ( evt, domEventData ) => {
 			const typeAroundFakeCaretPosition = getTypeAroundFakeCaretPosition( model.document.selection );
 			const typeAroundFakeCaretPosition = getTypeAroundFakeCaretPosition( model.document.selection );
 
 
 			// This listener handles only these cases when the "fake caret" is active.
 			// This listener handles only these cases when the "fake caret" is active.
@@ -647,7 +693,7 @@ export default class WidgetTypeAround extends Plugin {
 		const model = this.editor.model;
 		const model = this.editor.model;
 		const documentSelection = model.document.selection;
 		const documentSelection = model.document.selection;
 
 
-		this.listenTo( editor.model, 'insertContent', ( evt, [ content, selectable ] ) => {
+		this._listenToIfEnabled( editor.model, 'insertContent', ( evt, [ content, selectable ] ) => {
 			if ( selectable && !selectable.is( 'documentSelection' ) ) {
 			if ( selectable && !selectable.is( 'documentSelection' ) ) {
 				return;
 				return;
 			}
 			}

+ 77 - 2
packages/ckeditor5-widget/tests/widget-integration.js

@@ -8,6 +8,7 @@
 import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
 import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import Typing from '@ckeditor/ckeditor5-typing/src/typing';
 import Typing from '@ckeditor/ckeditor5-typing/src/typing';
+import LinkEditing from '@ckeditor/ckeditor5-link/src/linkediting';
 import Widget from '../src/widget';
 import Widget from '../src/widget';
 import DomEventData from '@ckeditor/ckeditor5-engine/src/view/observer/domeventdata';
 import DomEventData from '@ckeditor/ckeditor5-engine/src/view/observer/domeventdata';
 
 
@@ -32,7 +33,7 @@ describe( 'Widget - integration', () => {
 		editorElement = document.createElement( 'div' );
 		editorElement = document.createElement( 'div' );
 		document.body.appendChild( editorElement );
 		document.body.appendChild( editorElement );
 
 
-		return ClassicEditor.create( editorElement, { plugins: [ Paragraph, Widget, Typing ] } )
+		return ClassicEditor.create( editorElement, { plugins: [ Paragraph, Widget, Typing, LinkEditing ] } )
 			.then( newEditor => {
 			.then( newEditor => {
 				editor = newEditor;
 				editor = newEditor;
 				model = editor.model;
 				model = editor.model;
@@ -58,6 +59,9 @@ describe( 'Widget - integration', () => {
 					isObject: true,
 					isObject: true,
 					isInline: true
 					isInline: true
 				} );
 				} );
+				model.schema.extend( '$block', {
+					allowIn: 'nested'
+				} );
 
 
 				editor.conversion.for( 'downcast' )
 				editor.conversion.for( 'downcast' )
 					.elementToElement( { model: 'inline', view: 'figure' } )
 					.elementToElement( { model: 'inline', view: 'figure' } )
@@ -148,6 +152,76 @@ describe( 'Widget - integration', () => {
 		expect( getModelData( model ) ).to.equal( '<widget><nested>[foo bar]</nested></widget>' );
 		expect( getModelData( model ) ).to.equal( '<widget><nested>[foo bar]</nested></widget>' );
 	} );
 	} );
 
 
+	it( 'should select the entire nested editable if triple clicked on link', () => {
+		setModelData( model, '[]<widget><nested>foo <$text linkHref="abc">bar</$text></nested></widget>' );
+
+		const viewDiv = viewDocument.getRoot().getChild( 0 );
+		const viewLink = viewDiv.getChild( 0 ).getChild( 1 );
+		const preventDefault = sinon.spy();
+		const domEventDataMock = new DomEventData( view, {
+			target: view.domConverter.mapViewToDom( viewLink ),
+			preventDefault,
+			detail: 3
+		} );
+
+		viewDocument.fire( 'mousedown', domEventDataMock );
+
+		sinon.assert.called( preventDefault );
+
+		expect( getViewData( view ) ).to.equal(
+			'<div class="ck-widget" contenteditable="false">' +
+			'<figcaption contenteditable="true">{foo <a href="abc">bar</a>]</figcaption>' +
+			'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
+			'</div>'
+		);
+		expect( getModelData( model ) ).to.equal( '<widget><nested>[foo <$text linkHref="abc">bar</$text>]</nested></widget>' );
+	} );
+
+	it( 'should select only clicked paragraph if triple clicked on link', () => {
+		setModelData( model,
+			'[]<widget>' +
+				'<nested>' +
+					'<paragraph>foo</paragraph>' +
+					'<paragraph>foo <$text linkHref="abc">bar</$text></paragraph>' +
+					'<paragraph>bar</paragraph>' +
+				'</nested>' +
+			'</widget>'
+		);
+
+		const viewDiv = viewDocument.getRoot().getChild( 0 );
+		const viewLink = viewDiv.getChild( 0 ).getChild( 1 ).getChild( 1 );
+		const preventDefault = sinon.spy();
+		const domEventDataMock = new DomEventData( view, {
+			target: view.domConverter.mapViewToDom( viewLink ),
+			preventDefault,
+			detail: 3
+		} );
+
+		viewDocument.fire( 'mousedown', domEventDataMock );
+
+		sinon.assert.called( preventDefault );
+
+		expect( getViewData( view ) ).to.equal(
+			'<div class="ck-widget" contenteditable="false">' +
+				'<figcaption contenteditable="true">' +
+					'<p>foo</p>' +
+					'<p>{foo <a href="abc">bar</a>]</p>' +
+					'<p>bar</p>' +
+				'</figcaption>' +
+				'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
+			'</div>'
+		);
+		expect( getModelData( model ) ).to.equal(
+			'<widget>' +
+				'<nested>' +
+					'<paragraph>foo</paragraph>' +
+					'<paragraph>[foo <$text linkHref="abc">bar</$text>]</paragraph>' +
+					'<paragraph>bar</paragraph>' +
+				'</nested>' +
+			'</widget>'
+		);
+	} );
+
 	it( 'should select proper nested editable if triple clicked', () => {
 	it( 'should select proper nested editable if triple clicked', () => {
 		setModelData( model, '[]<widget><nested>foo</nested><nested>bar</nested></widget>' );
 		setModelData( model, '[]<widget><nested>foo</nested><nested>bar</nested></widget>' );
 
 
@@ -224,8 +298,9 @@ describe( 'Widget - integration', () => {
 		expect( getModelData( model ) ).to.equal( '<paragraph>Foo[<inline-widget>foo bar</inline-widget>]Bar</paragraph>' );
 		expect( getModelData( model ) ).to.equal( '<paragraph>Foo[<inline-widget>foo bar</inline-widget>]Bar</paragraph>' );
 	} );
 	} );
 
 
-	it( 'should do nothing for non-Safari browser', () => {
+	it( 'should do nothing for non-Safari and non-Gecko browser', () => {
 		testUtils.sinon.stub( env, 'isSafari' ).get( () => false );
 		testUtils.sinon.stub( env, 'isSafari' ).get( () => false );
+		testUtils.sinon.stub( env, 'isGecko' ).get( () => false );
 
 
 		setModelData( model, '<paragraph>[]</paragraph><widget><nested>foo bar</nested></widget>' );
 		setModelData( model, '<paragraph>[]</paragraph><widget><nested>foo bar</nested></widget>' );
 
 

+ 6 - 0
packages/ckeditor5-widget/tests/widgettypearound/utils.js

@@ -16,6 +16,12 @@ describe( 'widget type around utils', () => {
 		selection = new Selection();
 		selection = new Selection();
 	} );
 	} );
 
 
+	describe( 'TYPE_AROUND_SELECTION_ATTRIBUTE', () => {
+		it( 'should be defined', () => {
+			expect( TYPE_AROUND_SELECTION_ATTRIBUTE ).to.equal( 'widget-type-around' );
+		} );
+	} );
+
 	describe( 'getTypeAroundFakeCaretPosition()', () => {
 	describe( 'getTypeAroundFakeCaretPosition()', () => {
 		it( 'should return "before" if the model selection attribute is "before"', () => {
 		it( 'should return "before" if the model selection attribute is "before"', () => {
 			selection.setAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, 'before' );
 			selection.setAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, 'before' );

+ 216 - 105
packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js

@@ -12,6 +12,7 @@ import ViewText from '@ckeditor/ckeditor5-engine/src/view/text';
 
 
 import Widget from '../../src/widget';
 import Widget from '../../src/widget';
 import WidgetTypeAround from '../../src/widgettypearound/widgettypearound';
 import WidgetTypeAround from '../../src/widgettypearound/widgettypearound';
+import { TYPE_AROUND_SELECTION_ATTRIBUTE } from '../../src/widgettypearound/utils';
 import { toWidget } from '../../src/utils';
 import { toWidget } from '../../src/utils';
 
 
 import { setData as setModelData, getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { setData as setModelData, getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
@@ -56,6 +57,30 @@ describe( 'WidgetTypeAround', () => {
 		it( 'should have a name', () => {
 		it( 'should have a name', () => {
 			expect( WidgetTypeAround.pluginName ).to.equal( 'WidgetTypeAround' );
 			expect( WidgetTypeAround.pluginName ).to.equal( 'WidgetTypeAround' );
 		} );
 		} );
+
+		describe( '#isEnabled support', () => {
+			it( 'should add class to the editing view root when becoming disabled', () => {
+				editor.plugins.get( WidgetTypeAround ).isEnabled = false;
+				expect( viewRoot.hasClass( 'ck-widget__type-around_disabled' ) ).to.be.true;
+
+				editor.plugins.get( WidgetTypeAround ).isEnabled = true;
+				expect( viewRoot.hasClass( 'ck-widget__type-around_disabled' ) ).to.be.false;
+			} );
+
+			it( 'should remove the model selection attribute when becoming disabled', () => {
+				setModelData( editor.model, '<blockWidget></blockWidget>' );
+
+				editor.model.change( writer => {
+					writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, 'foo' );
+				} );
+
+				expect( editor.model.document.selection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.equal( 'foo' );
+
+				editor.plugins.get( WidgetTypeAround ).isEnabled = false;
+
+				expect( editor.model.document.selection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined;
+			} );
+		} );
 	} );
 	} );
 
 
 	describe( '_insertParagraph()', () => {
 	describe( '_insertParagraph()', () => {
@@ -299,7 +324,7 @@ describe( 'WidgetTypeAround', () => {
 				fireKeyboardEvent( 'arrowright' );
 				fireKeyboardEvent( 'arrowright' );
 
 
 				expect( getModelData( model ) ).to.equal( '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]' );
 				expect( getModelData( model ) ).to.equal( '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]' );
-				expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+				expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.equal( 'before' );
 
 
 				const viewWidget = viewRoot.getChild( 1 );
 				const viewWidget = viewRoot.getChild( 1 );
 
 
@@ -316,7 +341,7 @@ describe( 'WidgetTypeAround', () => {
 				fireKeyboardEvent( 'arrowleft' );
 				fireKeyboardEvent( 'arrowleft' );
 
 
 				expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]<paragraph>foo</paragraph>' );
 				expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]<paragraph>foo</paragraph>' );
-				expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' );
+				expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.equal( 'after' );
 
 
 				const viewWidget = viewRoot.getChild( 0 );
 				const viewWidget = viewRoot.getChild( 0 );
 
 
@@ -333,7 +358,7 @@ describe( 'WidgetTypeAround', () => {
 				fireKeyboardEvent( 'arrowright' );
 				fireKeyboardEvent( 'arrowright' );
 
 
 				expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
 				expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
-				expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' );
+				expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.equal( 'after' );
 
 
 				const viewWidget = viewRoot.getChild( 0 );
 				const viewWidget = viewRoot.getChild( 0 );
 
 
@@ -350,7 +375,7 @@ describe( 'WidgetTypeAround', () => {
 				fireKeyboardEvent( 'arrowleft' );
 				fireKeyboardEvent( 'arrowleft' );
 
 
 				expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
 				expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
-				expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+				expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.equal( 'before' );
 
 
 				const viewWidget = viewRoot.getChild( 0 );
 				const viewWidget = viewRoot.getChild( 0 );
 
 
@@ -367,7 +392,7 @@ describe( 'WidgetTypeAround', () => {
 				fireKeyboardEvent( 'arrowright', { shiftKey: true } );
 				fireKeyboardEvent( 'arrowright', { shiftKey: true } );
 
 
 				expect( getModelData( model ) ).to.equal( '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]' );
 				expect( getModelData( model ) ).to.equal( '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]' );
-				expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+				expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.equal( 'before' );
 
 
 				sinon.assert.calledOnce( eventInfoStub.stop );
 				sinon.assert.calledOnce( eventInfoStub.stop );
 				sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
 				sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
@@ -379,7 +404,7 @@ describe( 'WidgetTypeAround', () => {
 				fireKeyboardEvent( 'a' );
 				fireKeyboardEvent( 'a' );
 				fireMutation( 'a' );
 				fireMutation( 'a' );
 
 
-				expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+				expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined;
 				expect( getModelData( model ) ).to.equal( '<paragraph>fooa[]</paragraph><blockWidget></blockWidget>' );
 				expect( getModelData( model ) ).to.equal( '<paragraph>fooa[]</paragraph><blockWidget></blockWidget>' );
 
 
 				const viewWidget = viewRoot.getChild( 1 );
 				const viewWidget = viewRoot.getChild( 1 );
@@ -396,7 +421,7 @@ describe( 'WidgetTypeAround', () => {
 
 
 				fireKeyboardEvent( 'arrowright' );
 				fireKeyboardEvent( 'arrowright' );
 
 
-				expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+				expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined;
 
 
 				const viewWidget = viewRoot.getChild( 1 );
 				const viewWidget = viewRoot.getChild( 1 );
 
 
@@ -412,7 +437,7 @@ describe( 'WidgetTypeAround', () => {
 
 
 				fireKeyboardEvent( 'arrowleft' );
 				fireKeyboardEvent( 'arrowleft' );
 
 
-				expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+				expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined;
 
 
 				const viewWidget = viewRoot.getChild( 0 );
 				const viewWidget = viewRoot.getChild( 0 );
 
 
@@ -428,7 +453,7 @@ describe( 'WidgetTypeAround', () => {
 
 
 				fireKeyboardEvent( 'arrowright' );
 				fireKeyboardEvent( 'arrowright' );
 
 
-				expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+				expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined;
 
 
 				const viewWidget = viewRoot.getChild( 1 );
 				const viewWidget = viewRoot.getChild( 1 );
 
 
@@ -444,7 +469,7 @@ describe( 'WidgetTypeAround', () => {
 
 
 				fireKeyboardEvent( 'arrowleft' );
 				fireKeyboardEvent( 'arrowleft' );
 
 
-				expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+				expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined;
 
 
 				const viewWidget = viewRoot.getChild( 0 );
 				const viewWidget = viewRoot.getChild( 0 );
 
 
@@ -480,7 +505,7 @@ describe( 'WidgetTypeAround', () => {
 				fireKeyboardEvent( 'arrowleft' );
 				fireKeyboardEvent( 'arrowleft' );
 
 
 				expect( getModelData( model ) ).to.equal( '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]' );
 				expect( getModelData( model ) ).to.equal( '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]' );
-				expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+				expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.equal( 'before' );
 
 
 				sinon.assert.calledOnce( eventInfoStub.stop );
 				sinon.assert.calledOnce( eventInfoStub.stop );
 				sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
 				sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
@@ -488,7 +513,7 @@ describe( 'WidgetTypeAround', () => {
 				fireKeyboardEvent( 'arrowleft' );
 				fireKeyboardEvent( 'arrowleft' );
 
 
 				expect( getModelData( model ) ).to.equal( '<paragraph>foo[]</paragraph><blockWidget></blockWidget>' );
 				expect( getModelData( model ) ).to.equal( '<paragraph>foo[]</paragraph><blockWidget></blockWidget>' );
-				expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+				expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined;
 
 
 				const viewWidget = viewRoot.getChild( 1 );
 				const viewWidget = viewRoot.getChild( 1 );
 
 
@@ -505,7 +530,7 @@ describe( 'WidgetTypeAround', () => {
 				fireKeyboardEvent( 'arrowright' );
 				fireKeyboardEvent( 'arrowright' );
 
 
 				expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]<paragraph>foo</paragraph>' );
 				expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]<paragraph>foo</paragraph>' );
-				expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' );
+				expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.equal( 'after' );
 
 
 				sinon.assert.calledOnce( eventInfoStub.stop );
 				sinon.assert.calledOnce( eventInfoStub.stop );
 				sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
 				sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
@@ -513,7 +538,7 @@ describe( 'WidgetTypeAround', () => {
 				fireKeyboardEvent( 'arrowright' );
 				fireKeyboardEvent( 'arrowright' );
 
 
 				expect( getModelData( model ) ).to.equal( '<blockWidget></blockWidget><paragraph>[]foo</paragraph>' );
 				expect( getModelData( model ) ).to.equal( '<blockWidget></blockWidget><paragraph>[]foo</paragraph>' );
-				expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+				expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined;
 
 
 				const viewWidget = viewRoot.getChild( 0 );
 				const viewWidget = viewRoot.getChild( 0 );
 
 
@@ -530,7 +555,7 @@ describe( 'WidgetTypeAround', () => {
 				fireKeyboardEvent( 'arrowleft', { shiftKey: true } );
 				fireKeyboardEvent( 'arrowleft', { shiftKey: true } );
 
 
 				expect( getModelData( model ) ).to.equal( '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]' );
 				expect( getModelData( model ) ).to.equal( '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]' );
-				expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+				expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.equal( 'before' );
 
 
 				sinon.assert.calledOnce( eventInfoStub.stop );
 				sinon.assert.calledOnce( eventInfoStub.stop );
 				sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
 				sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
@@ -538,7 +563,7 @@ describe( 'WidgetTypeAround', () => {
 				fireKeyboardEvent( 'arrowleft', { shiftKey: true } );
 				fireKeyboardEvent( 'arrowleft', { shiftKey: true } );
 
 
 				expect( getModelData( model ) ).to.equal( '<paragraph>foo[]</paragraph><blockWidget></blockWidget>' );
 				expect( getModelData( model ) ).to.equal( '<paragraph>foo[]</paragraph><blockWidget></blockWidget>' );
-				expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+				expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined;
 
 
 				sinon.assert.calledOnce( eventInfoStub.stop );
 				sinon.assert.calledOnce( eventInfoStub.stop );
 				sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
 				sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
@@ -550,7 +575,7 @@ describe( 'WidgetTypeAround', () => {
 				fireKeyboardEvent( 'arrowleft' );
 				fireKeyboardEvent( 'arrowleft' );
 
 
 				expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
 				expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
-				expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+				expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.equal( 'before' );
 
 
 				sinon.assert.calledOnce( eventInfoStub.stop );
 				sinon.assert.calledOnce( eventInfoStub.stop );
 				sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
 				sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
@@ -558,7 +583,7 @@ describe( 'WidgetTypeAround', () => {
 				fireKeyboardEvent( 'arrowleft' );
 				fireKeyboardEvent( 'arrowleft' );
 
 
 				expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
 				expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
-				expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+				expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.equal( 'before' );
 
 
 				sinon.assert.calledOnce( eventInfoStub.stop );
 				sinon.assert.calledOnce( eventInfoStub.stop );
 				sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
 				sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
@@ -578,7 +603,7 @@ describe( 'WidgetTypeAround', () => {
 				fireKeyboardEvent( 'arrowright' );
 				fireKeyboardEvent( 'arrowright' );
 
 
 				expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
 				expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
-				expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' );
+				expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.equal( 'after' );
 
 
 				sinon.assert.calledOnce( eventInfoStub.stop );
 				sinon.assert.calledOnce( eventInfoStub.stop );
 				sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
 				sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
@@ -586,7 +611,7 @@ describe( 'WidgetTypeAround', () => {
 				fireKeyboardEvent( 'arrowright' );
 				fireKeyboardEvent( 'arrowright' );
 
 
 				expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
 				expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
-				expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' );
+				expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.equal( 'after' );
 
 
 				sinon.assert.calledOnce( eventInfoStub.stop );
 				sinon.assert.calledOnce( eventInfoStub.stop );
 				sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
 				sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
@@ -606,7 +631,7 @@ describe( 'WidgetTypeAround', () => {
 				fireKeyboardEvent( 'arrowleft' );
 				fireKeyboardEvent( 'arrowleft' );
 
 
 				expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
 				expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
-				expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+				expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.equal( 'before' );
 
 
 				sinon.assert.calledOnce( eventInfoStub.stop );
 				sinon.assert.calledOnce( eventInfoStub.stop );
 				sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
 				sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
@@ -614,7 +639,7 @@ describe( 'WidgetTypeAround', () => {
 				fireKeyboardEvent( 'arrowright' );
 				fireKeyboardEvent( 'arrowright' );
 
 
 				expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
 				expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
-				expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+				expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined;
 
 
 				const viewWidget = viewRoot.getChild( 0 );
 				const viewWidget = viewRoot.getChild( 0 );
 
 
@@ -631,7 +656,7 @@ describe( 'WidgetTypeAround', () => {
 				fireKeyboardEvent( 'arrowright' );
 				fireKeyboardEvent( 'arrowright' );
 
 
 				expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
 				expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
-				expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' );
+				expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.equal( 'after' );
 
 
 				sinon.assert.calledOnce( eventInfoStub.stop );
 				sinon.assert.calledOnce( eventInfoStub.stop );
 				sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
 				sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
@@ -639,7 +664,7 @@ describe( 'WidgetTypeAround', () => {
 				fireKeyboardEvent( 'arrowleft' );
 				fireKeyboardEvent( 'arrowleft' );
 
 
 				expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
 				expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
-				expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+				expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined;
 
 
 				const viewWidget = viewRoot.getChild( 0 );
 				const viewWidget = viewRoot.getChild( 0 );
 
 
@@ -651,28 +676,44 @@ describe( 'WidgetTypeAround', () => {
 			} );
 			} );
 		} );
 		} );
 
 
+		it( 'should not work when the plugin is disabled', () => {
+			editor.plugins.get( WidgetTypeAround ).isEnabled = false;
+
+			setModelData( editor.model, '<paragraph>foo[]</paragraph><blockWidget></blockWidget><paragraph>bar</paragraph>' );
+
+			fireKeyboardEvent( 'arrowright' );
+
+			expect( getModelData( model ) ).to.equal( '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]<paragraph>bar</paragraph>' );
+			expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined;
+
+			fireKeyboardEvent( 'arrowdown' );
+
+			expect( getModelData( model ) ).to.equal( '<paragraph>foo</paragraph><blockWidget></blockWidget><paragraph>[]bar</paragraph>' );
+			expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined;
+		} );
+
 		it( 'should activate and deactivate the "fake caret" using all 4 arrow keys', () => {
 		it( 'should activate and deactivate the "fake caret" using all 4 arrow keys', () => {
 			setModelData( editor.model, '<paragraph>foo[]</paragraph><blockWidget></blockWidget>' );
 			setModelData( editor.model, '<paragraph>foo[]</paragraph><blockWidget></blockWidget>' );
 
 
 			fireKeyboardEvent( 'arrowright' );
 			fireKeyboardEvent( 'arrowright' );
 
 
 			expect( getModelData( model ) ).to.equal( '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]' );
 			expect( getModelData( model ) ).to.equal( '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]' );
-			expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+			expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.equal( 'before' );
 
 
 			fireKeyboardEvent( 'arrowdown' );
 			fireKeyboardEvent( 'arrowdown' );
 
 
 			expect( getModelData( model ) ).to.equal( '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]' );
 			expect( getModelData( model ) ).to.equal( '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]' );
-			expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+			expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined;
 
 
 			fireKeyboardEvent( 'arrowup' );
 			fireKeyboardEvent( 'arrowup' );
 
 
 			expect( getModelData( model ) ).to.equal( '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]' );
 			expect( getModelData( model ) ).to.equal( '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]' );
-			expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+			expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.equal( 'before' );
 
 
 			fireKeyboardEvent( 'arrowleft' );
 			fireKeyboardEvent( 'arrowleft' );
 
 
 			expect( getModelData( model ) ).to.equal( '<paragraph>foo[]</paragraph><blockWidget></blockWidget>' );
 			expect( getModelData( model ) ).to.equal( '<paragraph>foo[]</paragraph><blockWidget></blockWidget>' );
-			expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+			expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined;
 		} );
 		} );
 
 
 		it( 'should quit the "fake caret" mode when the editor loses focus', () => {
 		it( 'should quit the "fake caret" mode when the editor loses focus', () => {
@@ -683,13 +724,13 @@ describe( 'WidgetTypeAround', () => {
 			fireKeyboardEvent( 'arrowright' );
 			fireKeyboardEvent( 'arrowright' );
 
 
 			expect( getModelData( model ) ).to.equal( '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]' );
 			expect( getModelData( model ) ).to.equal( '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]' );
-			expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+			expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.equal( 'before' );
 
 
 			editor.ui.focusTracker.isFocused = false;
 			editor.ui.focusTracker.isFocused = false;
 
 
 			const viewWidget = viewRoot.getChild( 1 );
 			const viewWidget = viewRoot.getChild( 1 );
 
 
-			expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+			expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined;
 			expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_before' ) ).to.be.false;
 			expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_before' ) ).to.be.false;
 			expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_after' ) ).to.be.false;
 			expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_after' ) ).to.be.false;
 		} );
 		} );
@@ -700,7 +741,7 @@ describe( 'WidgetTypeAround', () => {
 			fireKeyboardEvent( 'arrowright' );
 			fireKeyboardEvent( 'arrowright' );
 
 
 			expect( getModelData( model ) ).to.equal( '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]' );
 			expect( getModelData( model ) ).to.equal( '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]' );
-			expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+			expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.equal( 'before' );
 
 
 			model.change( writer => {
 			model.change( writer => {
 				writer.setSelection( model.document.getRoot().getChild( 0 ), 'in' );
 				writer.setSelection( model.document.getRoot().getChild( 0 ), 'in' );
@@ -708,7 +749,7 @@ describe( 'WidgetTypeAround', () => {
 
 
 			const viewWidget = viewRoot.getChild( 1 );
 			const viewWidget = viewRoot.getChild( 1 );
 
 
-			expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+			expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined;
 			expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_before' ) ).to.be.false;
 			expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_before' ) ).to.be.false;
 			expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_after' ) ).to.be.false;
 			expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_after' ) ).to.be.false;
 		} );
 		} );
@@ -719,14 +760,14 @@ describe( 'WidgetTypeAround', () => {
 			fireKeyboardEvent( 'arrowright' );
 			fireKeyboardEvent( 'arrowright' );
 
 
 			expect( getModelData( model ) ).to.equal( '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]' );
 			expect( getModelData( model ) ).to.equal( '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]' );
-			expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+			expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.equal( 'before' );
 
 
 			// This could happen in collaboration.
 			// This could happen in collaboration.
 			model.document.selection.fire( 'change:range', {
 			model.document.selection.fire( 'change:range', {
 				directChange: false
 				directChange: false
 			} );
 			} );
 
 
-			expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+			expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.equal( 'before' );
 
 
 			const viewWidget = viewRoot.getChild( 1 );
 			const viewWidget = viewRoot.getChild( 1 );
 
 
@@ -740,14 +781,14 @@ describe( 'WidgetTypeAround', () => {
 			const selection = model.createSelection( modelSelection );
 			const selection = model.createSelection( modelSelection );
 
 
 			model.change( writer => {
 			model.change( writer => {
-				writer.setSelectionAttribute( 'widget-type-around', 'before' );
+				writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, 'before' );
 				model.deleteContent( selection );
 				model.deleteContent( selection );
 			} );
 			} );
 
 
 			const viewWidget = viewRoot.getChild( 1 );
 			const viewWidget = viewRoot.getChild( 1 );
 
 
 			expect( getModelData( model ) ).to.equal( '<paragraph>foo[]</paragraph><paragraph></paragraph><paragraph>baz</paragraph>' );
 			expect( getModelData( model ) ).to.equal( '<paragraph>foo[]</paragraph><paragraph></paragraph><paragraph>baz</paragraph>' );
-			expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+			expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined;
 			expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_before' ) ).to.be.false;
 			expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_before' ) ).to.be.false;
 			expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_after' ) ).to.be.false;
 			expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_after' ) ).to.be.false;
 		} );
 		} );
@@ -756,12 +797,12 @@ describe( 'WidgetTypeAround', () => {
 			setModelData( editor.model, '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]<paragraph>baz</paragraph>' );
 			setModelData( editor.model, '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]<paragraph>baz</paragraph>' );
 
 
 			model.change( writer => {
 			model.change( writer => {
-				writer.setSelectionAttribute( 'widget-type-around', 'before' );
+				writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, 'before' );
 				writer.remove( editor.model.document.getRoot().getChild( 1 ) );
 				writer.remove( editor.model.document.getRoot().getChild( 1 ) );
 			} );
 			} );
 
 
 			expect( getModelData( model ) ).to.equal( '<paragraph>foo[]</paragraph><paragraph>baz</paragraph>' );
 			expect( getModelData( model ) ).to.equal( '<paragraph>foo[]</paragraph><paragraph>baz</paragraph>' );
-			expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+			expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined;
 		} );
 		} );
 
 
 		describe( 'inserting a new paragraph', () => {
 		describe( 'inserting a new paragraph', () => {
@@ -770,7 +811,7 @@ describe( 'WidgetTypeAround', () => {
 					setModelData( editor.model, '[<blockWidget></blockWidget>]' );
 					setModelData( editor.model, '[<blockWidget></blockWidget>]' );
 
 
 					fireKeyboardEvent( 'arrowleft' );
 					fireKeyboardEvent( 'arrowleft' );
-					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.equal( 'before' );
 
 
 					fireKeyboardEvent( 'enter' );
 					fireKeyboardEvent( 'enter' );
 					expect( getModelData( model ) ).to.equal( '<paragraph>[]</paragraph><blockWidget></blockWidget>' );
 					expect( getModelData( model ) ).to.equal( '<paragraph>[]</paragraph><blockWidget></blockWidget>' );
@@ -780,7 +821,7 @@ describe( 'WidgetTypeAround', () => {
 					setModelData( editor.model, '[<blockWidget></blockWidget>]' );
 					setModelData( editor.model, '[<blockWidget></blockWidget>]' );
 
 
 					fireKeyboardEvent( 'arrowright' );
 					fireKeyboardEvent( 'arrowright' );
-					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' );
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.equal( 'after' );
 
 
 					fireKeyboardEvent( 'enter' );
 					fireKeyboardEvent( 'enter' );
 					expect( getModelData( model ) ).to.equal( '<blockWidget></blockWidget><paragraph>[]</paragraph>' );
 					expect( getModelData( model ) ).to.equal( '<blockWidget></blockWidget><paragraph>[]</paragraph>' );
@@ -797,49 +838,62 @@ describe( 'WidgetTypeAround', () => {
 					editor.execute( 'undo' );
 					editor.execute( 'undo' );
 
 
 					expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
 					expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
-					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined;
+				} );
+
+				it( 'should not work when the plugin is disabled', () => {
+					setModelData( editor.model, '[<blockWidget></blockWidget>]' );
+
+					editor.plugins.get( WidgetTypeAround ).isEnabled = false;
+
+					model.change( writer => {
+						writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, 'after' );
+					} );
+
+					fireKeyboardEvent( 'enter' );
+					expect( getModelData( model ) ).to.equal( '<paragraph>[]</paragraph>' );
 				} );
 				} );
 			} );
 			} );
 
 
 			describe( 'on Enter key press when the widget is selected (no "fake caret", though)', () => {
 			describe( 'on Enter key press when the widget is selected (no "fake caret", though)', () => {
 				it( 'should insert a new paragraph after the widget if Enter was pressed', () => {
 				it( 'should insert a new paragraph after the widget if Enter was pressed', () => {
 					setModelData( editor.model, '[<blockWidget></blockWidget>]' );
 					setModelData( editor.model, '[<blockWidget></blockWidget>]' );
-					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined;
 
 
 					fireKeyboardEvent( 'enter' );
 					fireKeyboardEvent( 'enter' );
 
 
 					expect( getModelData( model ) ).to.equal( '<blockWidget></blockWidget><paragraph>[]</paragraph>' );
 					expect( getModelData( model ) ).to.equal( '<blockWidget></blockWidget><paragraph>[]</paragraph>' );
-					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined;
 				} );
 				} );
 
 
 				it( 'should insert a new paragraph before the widget if Shift+Enter was pressed', () => {
 				it( 'should insert a new paragraph before the widget if Shift+Enter was pressed', () => {
 					setModelData( editor.model, '[<blockWidget></blockWidget>]' );
 					setModelData( editor.model, '[<blockWidget></blockWidget>]' );
-					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined;
 
 
 					fireKeyboardEvent( 'enter', { shiftKey: true } );
 					fireKeyboardEvent( 'enter', { shiftKey: true } );
 
 
 					expect( getModelData( model ) ).to.equal( '<paragraph>[]</paragraph><blockWidget></blockWidget>' );
 					expect( getModelData( model ) ).to.equal( '<paragraph>[]</paragraph><blockWidget></blockWidget>' );
-					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined;
 				} );
 				} );
 
 
 				it( 'should insert a new paragraph only if an entire widget is selected (selected nested editable content)', () => {
 				it( 'should insert a new paragraph only if an entire widget is selected (selected nested editable content)', () => {
 					setModelData( editor.model, '<blockWidget><nested>[foo] bar</nested></blockWidget>' );
 					setModelData( editor.model, '<blockWidget><nested>[foo] bar</nested></blockWidget>' );
-					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined;
 
 
 					fireKeyboardEvent( 'enter' );
 					fireKeyboardEvent( 'enter' );
 
 
 					expect( getModelData( model ) ).to.equal( '<blockWidget><nested>[] bar</nested></blockWidget>' );
 					expect( getModelData( model ) ).to.equal( '<blockWidget><nested>[] bar</nested></blockWidget>' );
-					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined;
 				} );
 				} );
 
 
 				it( 'should insert a new paragraph only if an entire widget is selected (selected widget siblings)', () => {
 				it( 'should insert a new paragraph only if an entire widget is selected (selected widget siblings)', () => {
 					setModelData( editor.model, '<paragraph>f[oo</paragraph><blockWidget></blockWidget><paragraph>o]o</paragraph>' );
 					setModelData( editor.model, '<paragraph>f[oo</paragraph><blockWidget></blockWidget><paragraph>o]o</paragraph>' );
-					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined;
 
 
 					fireKeyboardEvent( 'enter' );
 					fireKeyboardEvent( 'enter' );
 
 
 					expect( getModelData( model ) ).to.equal( '<paragraph>f</paragraph><paragraph>[]o</paragraph>' );
 					expect( getModelData( model ) ).to.equal( '<paragraph>f</paragraph><paragraph>[]o</paragraph>' );
-					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined;
 				} );
 				} );
 
 
 				it( 'should split ancestors to find a place that allows a widget', () => {
 				it( 'should split ancestors to find a place that allows a widget', () => {
@@ -879,27 +933,37 @@ describe( 'WidgetTypeAround', () => {
 
 
 				it( 'should integrate with the undo feature', () => {
 				it( 'should integrate with the undo feature', () => {
 					setModelData( editor.model, '[<blockWidget></blockWidget>]' );
 					setModelData( editor.model, '[<blockWidget></blockWidget>]' );
-					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined;
 
 
 					fireKeyboardEvent( 'enter' );
 					fireKeyboardEvent( 'enter' );
 
 
 					expect( getModelData( model ) ).to.equal( '<blockWidget></blockWidget><paragraph>[]</paragraph>' );
 					expect( getModelData( model ) ).to.equal( '<blockWidget></blockWidget><paragraph>[]</paragraph>' );
-					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined;
 
 
 					editor.execute( 'undo' );
 					editor.execute( 'undo' );
 
 
 					expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
 					expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
-					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined;
 				} );
 				} );
 
 
 				it( 'should do nothing if a non-type-around-friendly content is selected', () => {
 				it( 'should do nothing if a non-type-around-friendly content is selected', () => {
 					setModelData( editor.model, '<paragraph>foo[<inlineWidget></inlineWidget>]</paragraph>' );
 					setModelData( editor.model, '<paragraph>foo[<inlineWidget></inlineWidget>]</paragraph>' );
-					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined;
 
 
 					fireKeyboardEvent( 'enter' );
 					fireKeyboardEvent( 'enter' );
 
 
 					expect( getModelData( model ) ).to.equal( '<paragraph>foo</paragraph><paragraph>[]</paragraph>' );
 					expect( getModelData( model ) ).to.equal( '<paragraph>foo</paragraph><paragraph>[]</paragraph>' );
-					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined;
+				} );
+
+				it( 'should not work when the plugin is disabled', () => {
+					editor.plugins.get( WidgetTypeAround ).isEnabled = false;
+
+					setModelData( editor.model, '[<blockWidget></blockWidget>]' );
+
+					fireKeyboardEvent( 'enter' );
+
+					expect( getModelData( model ) ).to.equal( '<paragraph>[]</paragraph>' );
 				} );
 				} );
 			} );
 			} );
 
 
@@ -908,40 +972,40 @@ describe( 'WidgetTypeAround', () => {
 					setModelData( editor.model, '[<blockWidget></blockWidget>]' );
 					setModelData( editor.model, '[<blockWidget></blockWidget>]' );
 
 
 					fireKeyboardEvent( 'arrowleft' );
 					fireKeyboardEvent( 'arrowleft' );
-					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.equal( 'before' );
 
 
 					fireKeyboardEvent( 'a' );
 					fireKeyboardEvent( 'a' );
 					fireMutation( 'a' );
 					fireMutation( 'a' );
 
 
 					expect( getModelData( model ) ).to.equal( '<paragraph>a[]</paragraph><blockWidget></blockWidget>' );
 					expect( getModelData( model ) ).to.equal( '<paragraph>a[]</paragraph><blockWidget></blockWidget>' );
-					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined;
 				} );
 				} );
 
 
 				it( 'should insert a character inside a new paragraph after a widget if the caret was "after" it', () => {
 				it( 'should insert a character inside a new paragraph after a widget if the caret was "after" it', () => {
 					setModelData( editor.model, '[<blockWidget></blockWidget>]' );
 					setModelData( editor.model, '[<blockWidget></blockWidget>]' );
 
 
 					fireKeyboardEvent( 'arrowright' );
 					fireKeyboardEvent( 'arrowright' );
-					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' );
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.equal( 'after' );
 
 
 					fireKeyboardEvent( 'a' );
 					fireKeyboardEvent( 'a' );
 					fireMutation( 'a' );
 					fireMutation( 'a' );
 
 
 					expect( getModelData( model ) ).to.equal( '<blockWidget></blockWidget><paragraph>a[]</paragraph>' );
 					expect( getModelData( model ) ).to.equal( '<blockWidget></blockWidget><paragraph>a[]</paragraph>' );
-					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined;
 				} );
 				} );
 
 
 				it( 'should do nothing if a "safe" keystroke was pressed', () => {
 				it( 'should do nothing if a "safe" keystroke was pressed', () => {
 					setModelData( editor.model, '[<blockWidget></blockWidget>]' );
 					setModelData( editor.model, '[<blockWidget></blockWidget>]' );
 
 
 					fireKeyboardEvent( 'arrowright' );
 					fireKeyboardEvent( 'arrowright' );
-					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' );
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.equal( 'after' );
 
 
 					fireKeyboardEvent( 'esc' );
 					fireKeyboardEvent( 'esc' );
 					fireKeyboardEvent( 'tab' );
 					fireKeyboardEvent( 'tab' );
 					fireKeyboardEvent( 'd', { ctrlKey: true } );
 					fireKeyboardEvent( 'd', { ctrlKey: true } );
 
 
 					expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
 					expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
-					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' );
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.equal( 'after' );
 				} );
 				} );
 
 
 				it( 'should integrate with the undo feature', () => {
 				it( 'should integrate with the undo feature', () => {
@@ -955,11 +1019,27 @@ describe( 'WidgetTypeAround', () => {
 
 
 					editor.execute( 'undo' );
 					editor.execute( 'undo' );
 					expect( getModelData( model ) ).to.equal( '<paragraph>[]</paragraph><blockWidget></blockWidget>' );
 					expect( getModelData( model ) ).to.equal( '<paragraph>[]</paragraph><blockWidget></blockWidget>' );
-					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined;
 
 
 					editor.execute( 'undo' );
 					editor.execute( 'undo' );
 					expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
 					expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
-					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined;
+				} );
+
+				it( 'should not work when the plugin is disabled', () => {
+					setModelData( editor.model, '[<blockWidget></blockWidget>]' );
+
+					editor.plugins.get( WidgetTypeAround ).isEnabled = false;
+
+					model.change( writer => {
+						writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, 'before' );
+					} );
+
+					fireKeyboardEvent( 'a' );
+					fireMutation( 'a' );
+
+					expect( getModelData( model ) ).to.equal( '<paragraph>a[]</paragraph>' );
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined;
 				} );
 				} );
 			} );
 			} );
 		} );
 		} );
@@ -974,11 +1054,11 @@ describe( 'WidgetTypeAround', () => {
 					fireKeyboardEvent( 'arrowleft' );
 					fireKeyboardEvent( 'arrowleft' );
 
 
 					expect( getModelData( model ) ).to.equal( '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]' );
 					expect( getModelData( model ) ).to.equal( '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]' );
-					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.equal( 'before' );
 
 
 					fireDeleteEvent();
 					fireDeleteEvent();
 					expect( getModelData( model ) ).to.equal( '<paragraph>fo[]</paragraph><blockWidget></blockWidget>' );
 					expect( getModelData( model ) ).to.equal( '<paragraph>fo[]</paragraph><blockWidget></blockWidget>' );
-					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined;
 
 
 					sinon.assert.calledOnce( eventInfoStub.stop );
 					sinon.assert.calledOnce( eventInfoStub.stop );
 					sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
 					sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
@@ -990,11 +1070,11 @@ describe( 'WidgetTypeAround', () => {
 					fireKeyboardEvent( 'arrowleft' );
 					fireKeyboardEvent( 'arrowleft' );
 
 
 					expect( getModelData( model ) ).to.equal( '<paragraph></paragraph>[<blockWidget></blockWidget>]' );
 					expect( getModelData( model ) ).to.equal( '<paragraph></paragraph>[<blockWidget></blockWidget>]' );
-					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.equal( 'before' );
 
 
 					fireDeleteEvent();
 					fireDeleteEvent();
 					expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
 					expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
-					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.equal( 'before' );
 
 
 					sinon.assert.calledOnce( eventInfoStub.stop );
 					sinon.assert.calledOnce( eventInfoStub.stop );
 					sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
 					sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
@@ -1011,11 +1091,11 @@ describe( 'WidgetTypeAround', () => {
 						'</blockQuote>' +
 						'</blockQuote>' +
 						'[<blockWidget></blockWidget>]'
 						'[<blockWidget></blockWidget>]'
 					);
 					);
-					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.equal( 'before' );
 
 
 					fireDeleteEvent();
 					fireDeleteEvent();
 					expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
 					expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
-					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.equal( 'before' );
 
 
 					sinon.assert.calledOnce( eventInfoStub.stop );
 					sinon.assert.calledOnce( eventInfoStub.stop );
 					sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
 					sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
@@ -1041,7 +1121,7 @@ describe( 'WidgetTypeAround', () => {
 						'</blockQuote>' +
 						'</blockQuote>' +
 						'[<blockWidget></blockWidget>]'
 						'[<blockWidget></blockWidget>]'
 					);
 					);
-					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.equal( 'before' );
 
 
 					// Assert that the paragraph is merged rather than deleted because
 					// Assert that the paragraph is merged rather than deleted because
 					// it is safer for collaboration.
 					// it is safer for collaboration.
@@ -1056,7 +1136,7 @@ describe( 'WidgetTypeAround', () => {
 						'</blockQuote>' +
 						'</blockQuote>' +
 						'<blockWidget></blockWidget>'
 						'<blockWidget></blockWidget>'
 					);
 					);
-					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined;
 					expect( operationType ).to.equal( 'merge' );
 					expect( operationType ).to.equal( 'merge' );
 
 
 					sinon.assert.calledOnce( eventInfoStub.stop );
 					sinon.assert.calledOnce( eventInfoStub.stop );
@@ -1069,11 +1149,11 @@ describe( 'WidgetTypeAround', () => {
 					fireKeyboardEvent( 'arrowleft' );
 					fireKeyboardEvent( 'arrowleft' );
 
 
 					expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
 					expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
-					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.equal( 'before' );
 
 
 					fireDeleteEvent();
 					fireDeleteEvent();
 					expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
 					expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
-					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.equal( 'before' );
 
 
 					sinon.assert.calledOnce( eventInfoStub.stop );
 					sinon.assert.calledOnce( eventInfoStub.stop );
 					sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
 					sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
@@ -1085,11 +1165,11 @@ describe( 'WidgetTypeAround', () => {
 					fireKeyboardEvent( 'arrowright' );
 					fireKeyboardEvent( 'arrowright' );
 
 
 					expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
 					expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
-					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' );
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.equal( 'after' );
 
 
 					fireDeleteEvent();
 					fireDeleteEvent();
 					expect( getModelData( model ) ).to.equal( '<paragraph>[]</paragraph>' );
 					expect( getModelData( model ) ).to.equal( '<paragraph>[]</paragraph>' );
-					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined;
 
 
 					sinon.assert.calledOnce( eventInfoStub.stop );
 					sinon.assert.calledOnce( eventInfoStub.stop );
 					sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
 					sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
@@ -1101,11 +1181,11 @@ describe( 'WidgetTypeAround', () => {
 					fireKeyboardEvent( 'arrowright' );
 					fireKeyboardEvent( 'arrowright' );
 
 
 					expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]<paragraph>foo</paragraph>' );
 					expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]<paragraph>foo</paragraph>' );
-					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' );
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.equal( 'after' );
 
 
 					fireDeleteEvent();
 					fireDeleteEvent();
 					expect( getModelData( model ) ).to.equal( '<paragraph>[]</paragraph><paragraph>foo</paragraph>' );
 					expect( getModelData( model ) ).to.equal( '<paragraph>[]</paragraph><paragraph>foo</paragraph>' );
-					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined;
 
 
 					sinon.assert.calledOnce( eventInfoStub.stop );
 					sinon.assert.calledOnce( eventInfoStub.stop );
 					sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
 					sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
@@ -1120,11 +1200,11 @@ describe( 'WidgetTypeAround', () => {
 						'<blockWidget><nested>foo</nested></blockWidget>' +
 						'<blockWidget><nested>foo</nested></blockWidget>' +
 						'[<blockWidget></blockWidget>]'
 						'[<blockWidget></blockWidget>]'
 					);
 					);
-					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.equal( 'before' );
 
 
 					fireDeleteEvent();
 					fireDeleteEvent();
 					expect( getModelData( model ) ).to.equal( '<paragraph>[]</paragraph><blockWidget></blockWidget>' );
 					expect( getModelData( model ) ).to.equal( '<paragraph>[]</paragraph><blockWidget></blockWidget>' );
-					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined;
 
 
 					sinon.assert.calledOnce( eventInfoStub.stop );
 					sinon.assert.calledOnce( eventInfoStub.stop );
 					sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
 					sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
@@ -1138,11 +1218,11 @@ describe( 'WidgetTypeAround', () => {
 					fireKeyboardEvent( 'arrowright' );
 					fireKeyboardEvent( 'arrowright' );
 
 
 					expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]<paragraph>foo</paragraph>' );
 					expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]<paragraph>foo</paragraph>' );
-					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' );
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.equal( 'after' );
 
 
 					fireDeleteEvent( true );
 					fireDeleteEvent( true );
 					expect( getModelData( model ) ).to.equal( '<blockWidget></blockWidget><paragraph>[]oo</paragraph>' );
 					expect( getModelData( model ) ).to.equal( '<blockWidget></blockWidget><paragraph>[]oo</paragraph>' );
-					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined;
 
 
 					sinon.assert.calledOnce( eventInfoStub.stop );
 					sinon.assert.calledOnce( eventInfoStub.stop );
 					sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
 					sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
@@ -1154,11 +1234,11 @@ describe( 'WidgetTypeAround', () => {
 					fireKeyboardEvent( 'arrowright' );
 					fireKeyboardEvent( 'arrowright' );
 
 
 					expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]<paragraph></paragraph>' );
 					expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]<paragraph></paragraph>' );
-					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' );
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.equal( 'after' );
 
 
 					fireDeleteEvent( true );
 					fireDeleteEvent( true );
 					expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
 					expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
-					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' );
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.equal( 'after' );
 
 
 					sinon.assert.calledOnce( eventInfoStub.stop );
 					sinon.assert.calledOnce( eventInfoStub.stop );
 					sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
 					sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
@@ -1173,11 +1253,11 @@ describe( 'WidgetTypeAround', () => {
 						'[<blockWidget></blockWidget>]' +
 						'[<blockWidget></blockWidget>]' +
 						'<blockQuote><paragraph></paragraph></blockQuote>'
 						'<blockQuote><paragraph></paragraph></blockQuote>'
 					);
 					);
-					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' );
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.equal( 'after' );
 
 
 					fireDeleteEvent( true );
 					fireDeleteEvent( true );
 					expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
 					expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
-					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' );
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.equal( 'after' );
 
 
 					sinon.assert.calledOnce( eventInfoStub.stop );
 					sinon.assert.calledOnce( eventInfoStub.stop );
 					sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
 					sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
@@ -1203,7 +1283,7 @@ describe( 'WidgetTypeAround', () => {
 							'<paragraph>foo</paragraph>' +
 							'<paragraph>foo</paragraph>' +
 						'</blockQuote>'
 						'</blockQuote>'
 					);
 					);
-					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' );
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.equal( 'after' );
 
 
 					// Assert that the paragraph is merged rather than deleted because
 					// Assert that the paragraph is merged rather than deleted because
 					// it is safer for collaboration.
 					// it is safer for collaboration.
@@ -1218,7 +1298,7 @@ describe( 'WidgetTypeAround', () => {
 							'<paragraph>[]foo</paragraph>' +
 							'<paragraph>[]foo</paragraph>' +
 						'</blockQuote>'
 						'</blockQuote>'
 					);
 					);
-					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined;
 					expect( operationType ).to.equal( 'merge' );
 					expect( operationType ).to.equal( 'merge' );
 
 
 					sinon.assert.calledOnce( eventInfoStub.stop );
 					sinon.assert.calledOnce( eventInfoStub.stop );
@@ -1231,11 +1311,11 @@ describe( 'WidgetTypeAround', () => {
 					fireKeyboardEvent( 'arrowright' );
 					fireKeyboardEvent( 'arrowright' );
 
 
 					expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
 					expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
-					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' );
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.equal( 'after' );
 
 
 					fireDeleteEvent( true );
 					fireDeleteEvent( true );
 					expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
 					expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
-					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' );
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.equal( 'after' );
 
 
 					sinon.assert.calledOnce( eventInfoStub.stop );
 					sinon.assert.calledOnce( eventInfoStub.stop );
 					sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
 					sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
@@ -1247,11 +1327,11 @@ describe( 'WidgetTypeAround', () => {
 					fireKeyboardEvent( 'arrowleft' );
 					fireKeyboardEvent( 'arrowleft' );
 
 
 					expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
 					expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
-					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.equal( 'before' );
 
 
 					fireDeleteEvent( true );
 					fireDeleteEvent( true );
 					expect( getModelData( model ) ).to.equal( '<paragraph>[]</paragraph>' );
 					expect( getModelData( model ) ).to.equal( '<paragraph>[]</paragraph>' );
-					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined;
 
 
 					sinon.assert.calledOnce( eventInfoStub.stop );
 					sinon.assert.calledOnce( eventInfoStub.stop );
 					sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
 					sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
@@ -1263,11 +1343,11 @@ describe( 'WidgetTypeAround', () => {
 					fireKeyboardEvent( 'arrowleft' );
 					fireKeyboardEvent( 'arrowleft' );
 
 
 					expect( getModelData( model ) ).to.equal( '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]' );
 					expect( getModelData( model ) ).to.equal( '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]' );
-					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.equal( 'before' );
 
 
 					fireDeleteEvent( true );
 					fireDeleteEvent( true );
 					expect( getModelData( model ) ).to.equal( '<paragraph>foo</paragraph><paragraph>[]</paragraph>' );
 					expect( getModelData( model ) ).to.equal( '<paragraph>foo</paragraph><paragraph>[]</paragraph>' );
-					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined;
 
 
 					sinon.assert.calledOnce( eventInfoStub.stop );
 					sinon.assert.calledOnce( eventInfoStub.stop );
 					sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
 					sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
@@ -1282,17 +1362,34 @@ describe( 'WidgetTypeAround', () => {
 						'[<blockWidget></blockWidget>]' +
 						'[<blockWidget></blockWidget>]' +
 						'<blockWidget><nested>foo</nested></blockWidget>'
 						'<blockWidget><nested>foo</nested></blockWidget>'
 					);
 					);
-					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' );
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.equal( 'after' );
 
 
 					fireDeleteEvent( true );
 					fireDeleteEvent( true );
 					expect( getModelData( model ) ).to.equal( '<blockWidget></blockWidget><paragraph>[]</paragraph>' );
 					expect( getModelData( model ) ).to.equal( '<blockWidget></blockWidget><paragraph>[]</paragraph>' );
-					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+					expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined;
 
 
 					sinon.assert.calledOnce( eventInfoStub.stop );
 					sinon.assert.calledOnce( eventInfoStub.stop );
 					sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
 					sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
 				} );
 				} );
 			} );
 			} );
 
 
+			it( 'should not work when the plugin is disabled', () => {
+				setModelData( editor.model, '[<blockWidget></blockWidget>]' );
+				fireKeyboardEvent( 'arrowleft' );
+
+				expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
+				expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.equal( 'before' );
+
+				editor.plugins.get( WidgetTypeAround ).isEnabled = false;
+
+				model.change( writer => {
+					writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, 'before' );
+				} );
+
+				fireDeleteEvent();
+				expect( getModelData( model ) ).to.equal( '<paragraph>[]</paragraph>' );
+			} );
+
 			function fireDeleteEvent( isForward = false ) {
 			function fireDeleteEvent( isForward = false ) {
 				eventInfoStub = new EventInfo( viewDocument, 'delete' );
 				eventInfoStub = new EventInfo( viewDocument, 'delete' );
 				sinon.spy( eventInfoStub, 'stop' );
 				sinon.spy( eventInfoStub, 'stop' );
@@ -1362,7 +1459,7 @@ describe( 'WidgetTypeAround', () => {
 			const selection = model.createSelection( modelSelection );
 			const selection = model.createSelection( modelSelection );
 
 
 			model.change( writer => {
 			model.change( writer => {
-				writer.setSelectionAttribute( 'widget-type-around', 'before' );
+				writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, 'before' );
 				model.insertContent( createParagraph( 'bar' ), selection );
 				model.insertContent( createParagraph( 'bar' ), selection );
 			} );
 			} );
 
 
@@ -1375,12 +1472,12 @@ describe( 'WidgetTypeAround', () => {
 
 
 			const batchSet = setupBatchWatch();
 			const batchSet = setupBatchWatch();
 
 
-			expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+			expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined;
 
 
 			model.insertContent( createParagraph( 'bar' ) );
 			model.insertContent( createParagraph( 'bar' ) );
 
 
 			expect( getModelData( model ) ).to.equal( '<paragraph>foo</paragraph><paragraph>bar[]</paragraph><paragraph>baz</paragraph>' );
 			expect( getModelData( model ) ).to.equal( '<paragraph>foo</paragraph><paragraph>bar[]</paragraph><paragraph>baz</paragraph>' );
-			expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+			expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined;
 			expect( batchSet.size ).to.be.equal( 1 );
 			expect( batchSet.size ).to.be.equal( 1 );
 		} );
 		} );
 
 
@@ -1390,13 +1487,13 @@ describe( 'WidgetTypeAround', () => {
 			const batchSet = setupBatchWatch();
 			const batchSet = setupBatchWatch();
 
 
 			model.change( writer => {
 			model.change( writer => {
-				writer.setSelectionAttribute( 'widget-type-around', 'before' );
+				writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, 'before' );
 			} );
 			} );
 
 
 			model.insertContent( createParagraph( 'bar' ) );
 			model.insertContent( createParagraph( 'bar' ) );
 
 
 			expect( getModelData( model ) ).to.equal( '<paragraph>bar[]</paragraph><blockWidget></blockWidget>' );
 			expect( getModelData( model ) ).to.equal( '<paragraph>bar[]</paragraph><blockWidget></blockWidget>' );
-			expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+			expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined;
 			expect( batchSet.size ).to.be.equal( 1 );
 			expect( batchSet.size ).to.be.equal( 1 );
 		} );
 		} );
 
 
@@ -1406,13 +1503,13 @@ describe( 'WidgetTypeAround', () => {
 			const batchSet = setupBatchWatch();
 			const batchSet = setupBatchWatch();
 
 
 			model.change( writer => {
 			model.change( writer => {
-				writer.setSelectionAttribute( 'widget-type-around', 'after' );
+				writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, 'after' );
 			} );
 			} );
 
 
 			model.insertContent( createParagraph( 'bar' ) );
 			model.insertContent( createParagraph( 'bar' ) );
 
 
 			expect( getModelData( model ) ).to.equal( '<blockWidget></blockWidget><paragraph>bar[]</paragraph>' );
 			expect( getModelData( model ) ).to.equal( '<blockWidget></blockWidget><paragraph>bar[]</paragraph>' );
-			expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+			expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined;
 			expect( batchSet.size ).to.be.equal( 1 );
 			expect( batchSet.size ).to.be.equal( 1 );
 		} );
 		} );
 
 
@@ -1422,13 +1519,13 @@ describe( 'WidgetTypeAround', () => {
 			const batchSet = setupBatchWatch();
 			const batchSet = setupBatchWatch();
 
 
 			model.change( writer => {
 			model.change( writer => {
-				writer.setSelectionAttribute( 'widget-type-around', 'before' );
+				writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, 'before' );
 			} );
 			} );
 
 
 			model.insertContent( createParagraph( 'bar' ) );
 			model.insertContent( createParagraph( 'bar' ) );
 
 
 			expect( getModelData( model ) ).to.equal( '<paragraph>foo</paragraph><paragraph>bar[]</paragraph><blockWidget></blockWidget>' );
 			expect( getModelData( model ) ).to.equal( '<paragraph>foo</paragraph><paragraph>bar[]</paragraph><blockWidget></blockWidget>' );
-			expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+			expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined;
 			expect( batchSet.size ).to.be.equal( 1 );
 			expect( batchSet.size ).to.be.equal( 1 );
 		} );
 		} );
 
 
@@ -1438,16 +1535,30 @@ describe( 'WidgetTypeAround', () => {
 			const batchSet = setupBatchWatch();
 			const batchSet = setupBatchWatch();
 
 
 			model.change( writer => {
 			model.change( writer => {
-				writer.setSelectionAttribute( 'widget-type-around', 'after' );
+				writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, 'after' );
 			} );
 			} );
 
 
 			model.insertContent( createParagraph( 'bar' ) );
 			model.insertContent( createParagraph( 'bar' ) );
 
 
 			expect( getModelData( model ) ).to.equal( '<blockWidget></blockWidget><paragraph>bar[]</paragraph><paragraph>foo</paragraph>' );
 			expect( getModelData( model ) ).to.equal( '<blockWidget></blockWidget><paragraph>bar[]</paragraph><paragraph>foo</paragraph>' );
-			expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+			expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined;
 			expect( batchSet.size ).to.be.equal( 1 );
 			expect( batchSet.size ).to.be.equal( 1 );
 		} );
 		} );
 
 
+		it( 'should not work when the plugin is disabled', () => {
+			setModelData( editor.model, '[<blockWidget></blockWidget>]' );
+
+			editor.plugins.get( WidgetTypeAround ).isEnabled = false;
+
+			model.change( writer => {
+				writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, 'before' );
+			} );
+
+			model.insertContent( createParagraph( 'bar' ) );
+
+			expect( getModelData( model ) ).to.equal( '<paragraph>bar[]</paragraph>' );
+		} );
+
 		function createParagraph( text ) {
 		function createParagraph( text ) {
 			return model.change( writer => {
 			return model.change( writer => {
 				const paragraph = writer.createElement( 'paragraph' );
 				const paragraph = writer.createElement( 'paragraph' );

+ 7 - 0
packages/ckeditor5-widget/theme/widgettypearound.css

@@ -104,3 +104,10 @@
 .ck.ck-editor__editable.ck-restricted-editing_mode_restricted .ck-widget__type-around {
 .ck.ck-editor__editable.ck-restricted-editing_mode_restricted .ck-widget__type-around {
 	display: none;
 	display: none;
 }
 }
+
+/*
+ * Integration with the #isEnabled property of the WidgetTypeAround plugin.
+ */
+.ck.ck-editor__editable.ck-widget__type-around_disabled .ck-widget__type-around {
+	display: none;
+}