8
0
Pārlūkot izejas kodu

Merge branch 'master' into i/4469-view-raw-element

Aleksander Nowodzinski 5 gadi atpakaļ
vecāks
revīzija
5aaffb6414
100 mainītis faili ar 3520 papildinājumiem un 886 dzēšanām
  1. 1 1
      CHANGELOG.md
  2. 10 2
      docs/features/keyboard-support.md
  3. 3 3
      docs/framework/guides/contributing/git-commit-message-convention.md
  4. 1 0
      package.json
  5. 1 1
      packages/ckeditor5-core/tests/_utils-tests/assertions/attribute.js
  6. 2 2
      packages/ckeditor5-core/tests/_utils/assertions/attribute.js
  7. 1 0
      packages/ckeditor5-core/theme/icons/object-size-full.svg
  8. 1 0
      packages/ckeditor5-core/theme/icons/object-size-large.svg
  9. 1 0
      packages/ckeditor5-core/theme/icons/object-size-medium.svg
  10. 1 0
      packages/ckeditor5-core/theme/icons/object-size-small.svg
  11. 22 0
      packages/ckeditor5-engine/src/model/element.js
  12. 16 0
      packages/ckeditor5-engine/src/model/position.js
  13. 1 1
      packages/ckeditor5-engine/src/view/domconverter.js
  14. 27 0
      packages/ckeditor5-engine/tests/model/element.js
  15. 20 0
      packages/ckeditor5-engine/tests/model/position.js
  16. 1 1
      packages/ckeditor5-highlight/package.json
  17. 9 6
      packages/ckeditor5-image/docs/_snippets/features/image-resizeui.js
  18. 1 1
      packages/ckeditor5-image/docs/_snippets/features/image-resizeuidropdown.js
  19. 5 3
      packages/ckeditor5-image/lang/contexts.json
  20. 66 22
      packages/ckeditor5-image/src/imageresize.js
  21. 1 1
      packages/ckeditor5-image/src/imageresize/imageresizeediting.js
  22. 156 65
      packages/ckeditor5-image/src/imageresize/imageresizeui.js
  23. 136 68
      packages/ckeditor5-image/tests/imageresize/imageresizeui.js
  24. 10 13
      packages/ckeditor5-image/tests/manual/imageresizeui.js
  25. 18 9
      packages/ckeditor5-image/tests/manual/imageresizeui.md
  26. 12 0
      packages/ckeditor5-image/theme/imageresize.css
  27. 1 1
      packages/ckeditor5-link/package.json
  28. 135 1
      packages/ckeditor5-link/src/linkediting.js
  29. 2 0
      packages/ckeditor5-link/src/linkimage.js
  30. 13 0
      packages/ckeditor5-link/src/linkimageediting.js
  31. 2 2
      packages/ckeditor5-link/src/linkui.js
  32. 351 11
      packages/ckeditor5-link/tests/linkediting.js
  33. 20 3
      packages/ckeditor5-link/tests/linkimageediting.js
  34. 8 0
      packages/ckeditor5-link/tests/linkui.js
  35. 3 1
      packages/ckeditor5-link/tests/manual/linkimage.md
  36. 1 1
      packages/ckeditor5-link/theme/icons/link.svg
  37. 18 0
      packages/ckeditor5-link/theme/linkimage.css
  38. 24 11
      packages/ckeditor5-page-break/src/pagebreakediting.js
  39. 30 2
      packages/ckeditor5-page-break/tests/pagebreakediting.js
  40. 1 0
      packages/ckeditor5-paste-from-office/package.json
  41. 16 0
      packages/ckeditor5-paste-from-office/tests/_data/page-break/index.js
  42. 738 0
      packages/ckeditor5-paste-from-office/tests/_data/page-break/page-break/input.html
  43. 3 0
      packages/ckeditor5-paste-from-office/tests/_data/page-break/page-break/model.html
  44. BIN
      packages/ckeditor5-paste-from-office/tests/_data/page-break/page-break/page-break.docx
  45. 3 1
      packages/ckeditor5-paste-from-office/tests/_utils/fixtures.js
  46. 10 0
      packages/ckeditor5-paste-from-office/tests/data/integration.js
  47. 3 2
      packages/ckeditor5-paste-from-office/tests/manual/integration.js
  48. 2 3
      packages/ckeditor5-table/src/commands/insertcolumncommand.js
  49. 2 3
      packages/ckeditor5-table/src/commands/insertrowcommand.js
  50. 3 4
      packages/ckeditor5-table/src/commands/inserttablecommand.js
  51. 3 3
      packages/ckeditor5-table/src/commands/mergecellcommand.js
  52. 3 3
      packages/ckeditor5-table/src/commands/mergecellscommand.js
  53. 1 2
      packages/ckeditor5-table/src/commands/removecolumncommand.js
  54. 4 11
      packages/ckeditor5-table/src/commands/removerowcommand.js
  55. 1 2
      packages/ckeditor5-table/src/commands/selectcolumncommand.js
  56. 1 2
      packages/ckeditor5-table/src/commands/selectrowcommand.js
  57. 1 2
      packages/ckeditor5-table/src/commands/setheadercolumncommand.js
  58. 2 2
      packages/ckeditor5-table/src/commands/setheaderrowcommand.js
  59. 0 100
      packages/ckeditor5-table/src/converters/downcast.js
  60. 54 0
      packages/ckeditor5-table/src/converters/table-heading-rows-refresh-post-fixer.js
  61. 3 3
      packages/ckeditor5-table/src/converters/table-layout-post-fixer.js
  62. 42 18
      packages/ckeditor5-table/src/tableclipboard.js
  63. 4 4
      packages/ckeditor5-table/src/tableediting.js
  64. 2 3
      packages/ckeditor5-table/src/tablekeyboard.js
  65. 1 6
      packages/ckeditor5-table/src/tablemouse.js
  66. 2 4
      packages/ckeditor5-table/src/tableproperties/commands/tablepropertycommand.js
  67. 2 3
      packages/ckeditor5-table/src/tableselection.js
  68. 41 27
      packages/ckeditor5-table/src/tableutils.js
  69. 4 20
      packages/ckeditor5-table/src/utils/common.js
  70. 13 8
      packages/ckeditor5-table/src/utils/selection.js
  71. 31 17
      packages/ckeditor5-table/src/utils/structure.js
  72. 2 3
      packages/ckeditor5-table/src/utils/ui/contextualballoon.js
  73. 12 1
      packages/ckeditor5-table/src/utils/ui/widget.js
  74. 15 0
      packages/ckeditor5-table/tests/commands/inserttablecommand.js
  75. 47 0
      packages/ckeditor5-table/tests/commands/mergecellscommand.js
  76. 33 32
      packages/ckeditor5-table/tests/converters/downcast.js
  77. 13 2
      packages/ckeditor5-table/tests/manual/tablemocking.html
  78. 5 2
      packages/ckeditor5-table/tests/manual/tablemocking.js
  79. 0 2
      packages/ckeditor5-table/tests/manual/tablemocking.md
  80. 368 4
      packages/ckeditor5-table/tests/tableclipboard-paste.js
  81. 81 0
      packages/ckeditor5-table/tests/tableutils.js
  82. 1 18
      packages/ckeditor5-table/tests/utils/common.js
  83. 1 2
      packages/ckeditor5-table/tests/utils/ui/contextualballoon.js
  84. 9 0
      packages/ckeditor5-theme-lark/theme/ckeditor5-link/linkimage.css
  85. 3 3
      packages/ckeditor5-typing/src/inputcommand.js
  86. 4 1
      packages/ckeditor5-typing/src/utils/injectunsafekeystrokeshandling.js
  87. 24 2
      packages/ckeditor5-typing/tests/inputcommand.js
  88. 46 3
      packages/ckeditor5-typing/tests/utils/injectunsafekeystrokeshandling.js
  89. BIN
      packages/ckeditor5-widget/docs/assets/img/framework-deep-dive-widget-type-around-buttons.gif
  90. 67 0
      packages/ckeditor5-widget/docs/framework/guides/deep-dive/widget-internals.md
  91. 1 0
      packages/ckeditor5-widget/package.json
  92. 6 3
      packages/ckeditor5-widget/src/widget.js
  93. 57 11
      packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js
  94. 77 2
      packages/ckeditor5-widget/tests/widget-integration.js
  95. 6 0
      packages/ckeditor5-widget/tests/widgettypearound/utils.js
  96. 216 105
      packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js
  97. 7 0
      packages/ckeditor5-widget/theme/widgettypearound.css
  98. 292 18
      scripts/docs/build-content-styles.js
  99. 0 96
      scripts/docs/content-styles/ckeditor.js
  100. 0 91
      scripts/docs/content-styles/list-content-styles.js

+ 1 - 1
CHANGELOG.md

@@ -19,7 +19,7 @@ New features were also accompanied by a set of bug fixes, to name a few:
 * [Autoformatting will no longer change formatting when typing in an inline code](https://github.com/ckeditor/ckeditor5/issues/1239).
 * [Autoformatting will no longer change formatting when typing in an inline code](https://github.com/ckeditor/ckeditor5/issues/1239).
 * Editor will no longer [crash if there's a HTML comment in the source data](https://github.com/ckeditor/ckeditor5/issues/5734).
 * Editor will no longer [crash if there's a HTML comment in the source data](https://github.com/ckeditor/ckeditor5/issues/5734).
 
 
-TODO: Add a link to the blog post.
+Read more in the blog post: https://ckeditor.com/blog/CKEditor-5-v20.0.0-with-linking-images-and-multi-cell-comments-released/
 
 
 ### Collaboration features
 ### Collaboration features
 
 

+ 10 - 2
docs/features/keyboard-support.md

@@ -67,13 +67,21 @@ Below is a list of the most important keystrokes supported by CKEditor 5 and its
 			<th colspan="3">When a widget is selected (for example: image, table, horizontal line, etc.)</th>
 			<th colspan="3">When a widget is selected (for example: image, table, horizontal line, etc.)</th>
 		</tr>
 		</tr>
 		<tr>
 		<tr>
-            <td>Insert a new paragraph directly after a widget</td>
+			<td>Insert a new paragraph directly after a widget</td>
 			<td colspan="2"><kbd>Enter</kbd></td>
 			<td colspan="2"><kbd>Enter</kbd></td>
-		</tr> 
+		</tr>
 		<tr>
 		<tr>
 			<td>Insert a new paragraph directly before a widget</td>
 			<td>Insert a new paragraph directly before a widget</td>
 			<td colspan="2"><kbd>Shift</kbd> + <kbd>Enter</kbd></td>
 			<td colspan="2"><kbd>Shift</kbd> + <kbd>Enter</kbd></td>
 		</tr>
 		</tr>
+		<tr>
+			<td>Display the caret to allow typing directly before a widget</td>
+			<td colspan="2"><kbd>↑</kbd> / <kbd>←</kbd></td>
+		</tr>
+		<tr>
+			<td>Display the caret to allow typing directly after a widget</td>
+			<td colspan="2"><kbd>↓</kbd> / <kbd>→</kbd></td>
+		</tr>
 		<tr>
 		<tr>
 			<th colspan="3">In a table cell</th>
 			<th colspan="3">In a table cell</th>
 		</tr>
 		</tr>

+ 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 - 0
package.json

@@ -98,6 +98,7 @@
     "lint-staged": "^10.2.6",
     "lint-staged": "^10.2.6",
     "mini-css-extract-plugin": "^0.9.0",
     "mini-css-extract-plugin": "^0.9.0",
     "minimatch": "^3.0.4",
     "minimatch": "^3.0.4",
+    "mkdirp": "^1.0.4",
     "nyc": "^15.0.1",
     "nyc": "^15.0.1",
     "postcss-loader": "^3.0.0",
     "postcss-loader": "^3.0.0",
     "progress-bar-webpack-plugin": "^2.1.0",
     "progress-bar-webpack-plugin": "^2.1.0",

+ 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
 		);
 		);
 	}
 	}
 } );
 } );

+ 1 - 0
packages/ckeditor5-core/theme/icons/object-size-full.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M2.5 17v1h-1v-1h1zm2 0v1h-1v-1h1zm2 0v1h-1v-1h1zm2 0v1h-1v-1h1zm2 0v1h-1v-1h1zm2 0v1h-1v-1h1zm2 0v1h-1v-1h1zm2 0v1h-1v-1h1zm2 0v1h-1v-1h1zM1 15.5v1H0v-1h1zm19 0v1h-1v-1h1zm-19-2v1H0v-1h1zm19 0v1h-1v-1h1zm-19-2v1H0v-1h1zm19 0v1h-1v-1h1zm-19-2v1H0v-1h1zm19 0v1h-1v-1h1zm-19-2v1H0v-1h1zm19 0v1h-1v-1h1zm-19-2v1H0v-1h1zm19 0v1h-1v-1h1zm0-2v1h-1v-1h1zm-19 0v1H0v-1h1zM14.5 2v1h-1V2h1zm2 0v1h-1V2h1zm2 0v1h-1V2h1zm-8 0v1h-1V2h1zm-2 0v1h-1V2h1zm-2 0v1h-1V2h1zm-2 0v1h-1V2h1zm8 0v1h-1V2h1zm-10 0v1h-1V2h1z"/><path d="M18.095 2H1.905C.853 2 0 2.895 0 4v12c0 1.105.853 2 1.905 2h16.19C19.147 18 20 17.105 20 16V4c0-1.105-.853-2-1.905-2zm0 1.5c.263 0 .476.224.476.5v12c0 .276-.213.5-.476.5H1.905a.489.489 0 0 1-.476-.5V4c0-.276.213-.5.476-.5h16.19z"/></svg>

+ 1 - 0
packages/ckeditor5-core/theme/icons/object-size-large.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M2.5 17v1h-1v-1h1zm2 0v1h-1v-1h1zm2 0v1h-1v-1h1zm2 0v1h-1v-1h1zm2 0v1h-1v-1h1zm2 0v1h-1v-1h1zm2 0v1h-1v-1h1zm2 0v1h-1v-1h1zm2 0v1h-1v-1h1zM1 15.5v1H0v-1h1zm19 0v1h-1v-1h1zm-19-2v1H0v-1h1zm19 0v1h-1v-1h1zm-19-2v1H0v-1h1zm19 0v1h-1v-1h1zm-19-2v1H0v-1h1zm19 0v1h-1v-1h1zm-19-2v1H0v-1h1zm19 0v1h-1v-1h1zm-19-2v1H0v-1h1zm19 0v1h-1v-1h1zm0-2v1h-1v-1h1zm-19 0v1H0v-1h1zM14.5 2v1h-1V2h1zm2 0v1h-1V2h1zm2 0v1h-1V2h1zm-8 0v1h-1V2h1zm-2 0v1h-1V2h1zm-2 0v1h-1V2h1zm-2 0v1h-1V2h1zm8 0v1h-1V2h1zm-10 0v1h-1V2h1z"/><path d="M13 6H2a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2zm0 1.5a.5.5 0 0 1 .5.5v8a.5.5 0 0 1-.5.5H2a.5.5 0 0 1-.5-.5V8a.5.5 0 0 1 .5-.5h11z"/></svg>

+ 1 - 0
packages/ckeditor5-core/theme/icons/object-size-medium.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M2.5 17v1h-1v-1h1zm2 0v1h-1v-1h1zm2 0v1h-1v-1h1zm2 0v1h-1v-1h1zm2 0v1h-1v-1h1zm2 0v1h-1v-1h1zm2 0v1h-1v-1h1zm2 0v1h-1v-1h1zm2 0v1h-1v-1h1zM1 15.5v1H0v-1h1zm19 0v1h-1v-1h1zm-19-2v1H0v-1h1zm19 0v1h-1v-1h1zm-19-2v1H0v-1h1zm19 0v1h-1v-1h1zm-19-2v1H0v-1h1zm19 0v1h-1v-1h1zm-19-2v1H0v-1h1zm19 0v1h-1v-1h1zm-19-2v1H0v-1h1zm19 0v1h-1v-1h1zm0-2v1h-1v-1h1zm-19 0v1H0v-1h1zM14.5 2v1h-1V2h1zm2 0v1h-1V2h1zm2 0v1h-1V2h1zm-8 0v1h-1V2h1zm-2 0v1h-1V2h1zm-2 0v1h-1V2h1zm-2 0v1h-1V2h1zm8 0v1h-1V2h1zm-10 0v1h-1V2h1z"/><path d="M10 8H2a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2v-6a2 2 0 0 0-2-2zm0 1.5a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-.5.5H2a.5.5 0 0 1-.5-.5v-6a.5.5 0 0 1 .5-.5h8z"/></svg>

+ 1 - 0
packages/ckeditor5-core/theme/icons/object-size-small.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M2.5 17v1h-1v-1h1zm2 0v1h-1v-1h1zm2 0v1h-1v-1h1zm2 0v1h-1v-1h1zm2 0v1h-1v-1h1zm2 0v1h-1v-1h1zm2 0v1h-1v-1h1zm2 0v1h-1v-1h1zm2 0v1h-1v-1h1zM1 15.5v1H0v-1h1zm19 0v1h-1v-1h1zm-19-2v1H0v-1h1zm19 0v1h-1v-1h1zm-19-2v1H0v-1h1zm19 0v1h-1v-1h1zm-19-2v1H0v-1h1zm19 0v1h-1v-1h1zm-19-2v1H0v-1h1zm19 0v1h-1v-1h1zm-19-2v1H0v-1h1zm19 0v1h-1v-1h1zm0-2v1h-1v-1h1zm-19 0v1H0v-1h1zM14.5 2v1h-1V2h1zm2 0v1h-1V2h1zm2 0v1h-1V2h1zm-8 0v1h-1V2h1zm-2 0v1h-1V2h1zm-2 0v1h-1V2h1zm-2 0v1h-1V2h1zm8 0v1h-1V2h1zm-10 0v1h-1V2h1z"/><path d="M7 10H2a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h5a2 2 0 0 0 2-2v-4a2 2 0 0 0-2-2zm0 1.5a.5.5 0 0 1 .5.5v4a.5.5 0 0 1-.5.5H2a.5.5 0 0 1-.5-.5v-4a.5.5 0 0 1 .5-.5h5z"/></svg>

+ 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 ] );

+ 1 - 1
packages/ckeditor5-highlight/package.json

@@ -1,7 +1,7 @@
 {
 {
   "name": "@ckeditor/ckeditor5-highlight",
   "name": "@ckeditor/ckeditor5-highlight",
   "version": "20.0.0",
   "version": "20.0.0",
-  "description": "Highilght feature for CKEditor 5.",
+  "description": "Highlight feature for CKEditor 5.",
   "keywords": [
   "keywords": [
     "ckeditor",
     "ckeditor",
     "ckeditor5",
     "ckeditor5",

+ 9 - 6
packages/ckeditor5-image/docs/_snippets/features/image-resizeui.js

@@ -17,26 +17,29 @@ ClassicEditor
 			resizeOptions: [
 			resizeOptions: [
 				{
 				{
 					name: 'imageResize:original',
 					name: 'imageResize:original',
-					label: 'Original size',
-					value: null
+					label: 'Original',
+					value: null,
+					icon: 'original'
 				},
 				},
 				{
 				{
 					name: 'imageResize:50',
 					name: 'imageResize:50',
 					label: '50%',
 					label: '50%',
-					value: '50'
+					value: '50',
+					icon: 'medium'
 				},
 				},
 				{
 				{
 					name: 'imageResize:75',
 					name: 'imageResize:75',
 					label: '75%',
 					label: '75%',
-					value: '75'
+					value: '75',
+					icon: 'large'
 				}
 				}
 			],
 			],
 			toolbar: [
 			toolbar: [
 				'imageStyle:full',
 				'imageStyle:full',
 				'imageStyle:side', '|',
 				'imageStyle:side', '|',
-				'imageResize:original',
 				'imageResize:50',
 				'imageResize:50',
-				'imageResize:75'
+				'imageResize:75',
+				'imageResize:original'
 			]
 			]
 		},
 		},
 		cloudServices: CS_CONFIG
 		cloudServices: CS_CONFIG

+ 1 - 1
packages/ckeditor5-image/docs/_snippets/features/image-resizeuidropdown.js

@@ -17,7 +17,7 @@ ClassicEditor
 			resizeOptions: [
 			resizeOptions: [
 				{
 				{
 					name: 'imageResize:original',
 					name: 'imageResize:original',
-					label: 'Original size',
+					label: 'Original',
 					value: null
 					value: null
 				},
 				},
 				{
 				{

+ 5 - 3
packages/ckeditor5-image/lang/contexts.json

@@ -11,7 +11,9 @@
 	"Insert image": "Label for the insert image toolbar button.",
 	"Insert image": "Label for the insert image toolbar button.",
 	"Upload failed": "Title of the notification displayed when upload fails.",
 	"Upload failed": "Title of the notification displayed when upload fails.",
 	"Image toolbar": "The label used by assistive technologies describing an image toolbar attached to an image widget.",
 	"Image toolbar": "The label used by assistive technologies describing an image toolbar attached to an image widget.",
-	"Resize image to": "The label used for the standalone resize option buttons in the image toolbar",
-	"Resize image": "The label used for the dropdown in the image toolbar, containing defined resize options",
-	"Resize image to the original size": "The label used for the standalone resize reset option button in the image toolbar, when user set the value to `null`"
+	"Resize image": "The label used for the dropdown in the image toolbar containing defined resize options",
+	"Resize image to %0": "The label used for the standalone resize options buttons in the image toolbar",
+	"Resize image to the original size": "The accessibility label of the standalone image resize reset option button in the image toolbar for the screen readers",
+	"Original": "Default label for the resize option that resets the size of the image.",
+	"Image resize list": "The accessibility label of the image resize dropdown list for the screen readers."
 }
 }

+ 66 - 22
packages/ckeditor5-image/src/imageresize.js

@@ -58,16 +58,21 @@ export default class ImageResize extends Plugin {
  */
  */
 
 
 /**
 /**
- * The resize options.
+ * The image resize options.
  *
  *
- * Each option should have its `name`, which is a component name definition that will be
- * used in the {@link module:image/imageresize/imageresizeui~ImageResizeUI} plugin.
- * Other properties like `label` and `value` define the following:
- * a text label for the option button and the value that will be applied to the image's width.
+ * Each option should have at least these two properties:
  *
  *
- * The value property is combined with the `resizeUnit` (`%` by default), eg: `value: '50'` and `resizeUnit: '%'` is `50%`.
+ * * name: The name of the UI component registered in the global
+ * {@link module:core/editor/editorui~EditorUI#componentFactory component factory} of the editor,
+ * representing the button a user can click to change the size of an image,
+ * * value: An actual image width applied when a user clicks the mentioned button
+ * ({@link module:image/imageresize/imageresizecommand~ImageResizeCommand} gets executed).
+ * The value property is combined with the {@link module:image/image~ImageConfig#resizeUnit `config.image.resizeUnit`} (`%` by default).
+ * For instance: `value: '50'` and `resizeUnit: '%'` will render as `'50%'` in the UI.
  *
  *
- * **NOTE:** If you want to set an option that will reset image to its original size, you need to pass a `null` value
+ * **Resetting the image size**
+ *
+ * If you want to set an option that will reset image to its original size, you need to pass a `null` value
  * to one of the options. The `:original` token is not mandatory, you can call it anything you wish, but it must reflect
  * to one of the options. The `:original` token is not mandatory, you can call it anything you wish, but it must reflect
  * in the standalone buttons configuration for the image toolbar.
  * in the standalone buttons configuration for the image toolbar.
  *
  *
@@ -77,17 +82,14 @@ export default class ImageResize extends Plugin {
  *					resizeUnit: "%",
  *					resizeUnit: "%",
  *					resizeOptions: [ {
  *					resizeOptions: [ {
  *						name: 'imageResize:original',
  *						name: 'imageResize:original',
- *						label: 'Original size',
  *						value: null
  *						value: null
  *					},
  *					},
  *					{
  *					{
  *						name: 'imageResize:50',
  *						name: 'imageResize:50',
- *						label: '50%',
  *						value: '50'
  *						value: '50'
  *					},
  *					},
  *					{
  *					{
  *						name: 'imageResize:75',
  *						name: 'imageResize:75',
- *						label: '75%',
  *						value: '75'
  *						value: '75'
  *					} ]
  *					} ]
  *				}
  *				}
@@ -95,27 +97,26 @@ export default class ImageResize extends Plugin {
  *			.then( ... )
  *			.then( ... )
  *			.catch( ... );
  *			.catch( ... );
  *
  *
+ * **Resizing images using a dropdown**
+ *
  * With resize options defined, you can decide whether you want to display them as a dropdown or as standalone buttons.
  * With resize options defined, you can decide whether you want to display them as a dropdown or as standalone buttons.
- * For the dropdown, you need to pass only the `imageResize` token to the `image.toolbar`.
- * The dropdown contains all defined options by default:
+ * For the dropdown, you need to pass only the `imageResize` token to the
+{@link module:image/image~ImageConfig#toolbar `config.image.toolbar`}. The dropdown contains all defined options by default:
  *
  *
- *			ClassicEditor
+ *		ClassicEditor
  *			.create( editorElement, {
  *			.create( editorElement, {
  *				image: {
  *				image: {
  *					resizeUnit: "%",
  *					resizeUnit: "%",
  *					resizeOptions: [ {
  *					resizeOptions: [ {
  *						name: 'imageResize:original',
  *						name: 'imageResize:original',
- *						label: 'Original size',
  *						value: null
  *						value: null
  *					},
  *					},
  *					{
  *					{
  *						name: 'imageResize:50',
  *						name: 'imageResize:50',
- *						label: '50%',
  *						value: '50'
  *						value: '50'
  *					},
  *					},
  *					{
  *					{
  *						name: 'imageResize:75',
  *						name: 'imageResize:75',
- *						label: '75%',
  *						value: '75'
  *						value: '75'
  *					} ],
  *					} ],
  *					toolbar: [ 'imageResize', ... ],
  *					toolbar: [ 'imageResize', ... ],
@@ -124,33 +125,76 @@ export default class ImageResize extends Plugin {
  *			.then( ... )
  *			.then( ... )
  *			.catch( ... );
  *			.catch( ... );
  *
  *
- * If you want to have separate buttons for each option, pass their names instead:
+ * **Resizing images using individual buttons**
+ *
+ * If you want to have separate buttons for {@link module:image/imageresize/imageresizeui~ImageResizeOption each option},
+ * pass their names to the {@link module:image/image~ImageConfig#toolbar `config.image.toolbar`} instead. Please keep in mind
+ * that this time **you must define the additional
+ * {@link module:image/imageresize/imageresizeui~ImageResizeOption `icon` property}**:
  *
  *
- *			ClassicEditor
+ *		ClassicEditor
  *			.create( editorElement, {
  *			.create( editorElement, {
  *				image: {
  *				image: {
  *					resizeUnit: "%",
  *					resizeUnit: "%",
  *					resizeOptions: [ {
  *					resizeOptions: [ {
  *						name: 'imageResize:original',
  *						name: 'imageResize:original',
- *						label: 'Original size',
  *						value: null
  *						value: null
+ *						icon: 'original'
+ *					},
+ *					{
+ *						name: 'imageResize:25',
+ *						value: '25'
+ *						icon: 'small'
  *					},
  *					},
  *					{
  *					{
  *						name: 'imageResize:50',
  *						name: 'imageResize:50',
- *						label: '50%',
  *						value: '50'
  *						value: '50'
+ *						icon: 'medium'
  *					},
  *					},
  *					{
  *					{
  *						name: 'imageResize:75',
  *						name: 'imageResize:75',
- *						label: '75%',
  *						value: '75'
  *						value: '75'
+ *						icon: 'large'
  *					} ],
  *					} ],
- *					toolbar: [ 'imageResize:original', 'imageResize:50', 'imageResize:75', ... ],
+ *					toolbar: [ 'imageResize:25', 'imageResize:50', 'imageResize:75', 'imageResize:original', ... ],
  *				}
  *				}
  *			} )
  *			} )
  *			.then( ... )
  *			.then( ... )
  *			.catch( ... );
  *			.catch( ... );
  *
  *
+ * **Customizing resize button labels**
+ *
+ * You can set your own label for each resize button. To do that, add the `label` property like in the example below.
+ *
+ * * When using the **dropdown**, the labels are displayed on the list of all options when you open the dropdown.
+ * * When using **standalone buttons**, the labels will are displayed as tooltips when a user hovers over the button.
+ *
+ *		ClassicEditor
+ *			.create( editorElement, {
+ *				image: {
+ *					resizeUnit: "%",
+ *					resizeOptions: [ {
+ *						name: 'imageResize:original',
+ *						value: null,
+ *						label: 'Original size'
+ *						// Note: add the "icon" property if you're configuring a standalone button.
+ *					},
+ *					{
+ *						name: 'imageResize:50',
+ *						value: '50',
+ *						label: 'Medium size'
+ *						// Note: add the "icon" property if you're configuring a standalone button.
+ *					},
+ *					{
+ *						name: 'imageResize:75',
+ *						value: '75',
+ *						label: 'Large size'
+ *						// Note: add the "icon" property if you're configuring a standalone button.
+ *					} ]
+ *				}
+ *			} )
+ *			.then( ... )
+ *			.catch( ... );
  *
  *
  * @member {Array.<module:image/imageresize/imageresizeui~ImageResizeOption>} module:image/image~ImageConfig#resizeOptions
  * @member {Array.<module:image/imageresize/imageresizeui~ImageResizeOption>} module:image/image~ImageConfig#resizeOptions
  */
  */

+ 1 - 1
packages/ckeditor5-image/src/imageresize/imageresizeediting.js

@@ -4,7 +4,7 @@
  */
  */
 
 
 /**
 /**
- * @module image/imageresizeediting
+ * @module image/imageresize/imageresizeediting
  */
  */
 
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';

+ 156 - 65
packages/ckeditor5-image/src/imageresize/imageresizeui.js

@@ -16,10 +16,24 @@ import DropdownButtonView from '@ckeditor/ckeditor5-ui/src/dropdown/button/dropd
 import Model from '@ckeditor/ckeditor5-ui/src/model';
 import Model from '@ckeditor/ckeditor5-ui/src/model';
 import Collection from '@ckeditor/ckeditor5-utils/src/collection';
 import Collection from '@ckeditor/ckeditor5-utils/src/collection';
 
 
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+
+import iconSmall from '@ckeditor/ckeditor5-core/theme/icons/object-size-small.svg';
+import iconMedium from '@ckeditor/ckeditor5-core/theme/icons/object-size-medium.svg';
+import iconLarge from '@ckeditor/ckeditor5-core/theme/icons/object-size-large.svg';
+import iconFull from '@ckeditor/ckeditor5-core/theme/icons/object-size-full.svg';
+
+const RESIZE_ICONS = {
+	small: iconSmall,
+	medium: iconMedium,
+	large: iconLarge,
+	original: iconFull
+};
+
 /**
 /**
  * The `ImageResizeUI` plugin.
  * The `ImageResizeUI` plugin.
  *
  *
- * It adds a possibility to resize each image using toolbar dropdown or separate buttons, depends on the plugin configuration.
+ * It adds a possibility to resize images using the toolbar dropdown or individual buttons, depending on the plugin configuration.
  *
  *
  * @extends module:core/plugin~Plugin
  * @extends module:core/plugin~Plugin
  */
  */
@@ -38,6 +52,23 @@ export default class ImageResizeUI extends Plugin {
 		return 'ImageResizeUI';
 		return 'ImageResizeUI';
 	}
 	}
 
 
+	/**
+	 * @inheritDoc
+	 */
+	constructor( editor ) {
+		super( editor );
+
+		/**
+		 * The resize unit.
+		 *
+		 * @readonly
+		 * @private
+		 * @type {module:image/image~ImageConfig#resizeUnit}
+		 * @default '%'
+		 */
+		this._resizeUnit = editor.config.get( 'image.resizeUnit' ) || '%';
+	}
+
 	/**
 	/**
 	 * @inheritDoc
 	 * @inheritDoc
 	 */
 	 */
@@ -45,7 +76,6 @@ export default class ImageResizeUI extends Plugin {
 		const editor = this.editor;
 		const editor = this.editor;
 		const options = editor.config.get( 'image.resizeOptions' );
 		const options = editor.config.get( 'image.resizeOptions' );
 		const command = editor.commands.get( 'imageResize' );
 		const command = editor.commands.get( 'imageResize' );
-		const resizeUnit = editor.config.get( 'image.resizeUnit' ) || '%';
 
 
 		if ( !options ) {
 		if ( !options ) {
 			return;
 			return;
@@ -54,44 +84,62 @@ export default class ImageResizeUI extends Plugin {
 		this.bind( 'isEnabled' ).to( command );
 		this.bind( 'isEnabled' ).to( command );
 
 
 		for ( const option of options ) {
 		for ( const option of options ) {
-			this._addButton( option, resizeUnit );
+			this._registerImageResizeButton( option );
 		}
 		}
 
 
-		this._addDropdown( options, resizeUnit );
+		this._registerImageResizeDropdown( options );
 	}
 	}
 
 
 	/**
 	/**
 	 * A helper function that creates a standalone button component for the plugin.
 	 * A helper function that creates a standalone button component for the plugin.
 	 *
 	 *
 	 * @private
 	 * @private
-	 *
 	 * @param {module:image/imageresize/imageresizeui~ImageResizeOption} resizeOption A model of resize option.
 	 * @param {module:image/imageresize/imageresizeui~ImageResizeOption} resizeOption A model of resize option.
-	 * @param {String} unit A resize unit.
 	 */
 	 */
-	_addButton( { name, label, value }, unit ) {
+	_registerImageResizeButton( option ) {
 		const editor = this.editor;
 		const editor = this.editor;
-		const t = editor.t;
-		const parsedValue = value ? value + unit : null;
+		const { name, value, icon } = option;
+		const optionValueWithUnit = value ? value + this._resizeUnit : null;
 
 
 		editor.ui.componentFactory.add( name, locale => {
 		editor.ui.componentFactory.add( name, locale => {
 			const button = new ButtonView( locale );
 			const button = new ButtonView( locale );
 			const command = editor.commands.get( 'imageResize' );
 			const command = editor.commands.get( 'imageResize' );
-			const commandCallback = setOptionOn( parsedValue );
+			const labelText = this._getOptionLabelValue( option, true );
+
+			if ( !RESIZE_ICONS[ icon ] ) {
+				/**
+				 * When configuring {@link module:image/image~ImageConfig#resizeOptions `config.image.resizeOptions`} for standalone
+				 * buttons, a valid `icon` token must be set for each option.
+				 *
+				 * See all valid options described in the
+				 * {@link module:image/imageresize/imageresizeui~ImageResizeOption plugin configuration}.
+				 *
+				 * @error imageresizeui-missing-icon
+				 * @param {module:image/imageresize/imageresizeui~ImageResizeOption} option Invalid image resize option.
+				*/
+				throw new CKEditorError(
+					'imageresizeui-missing-icon: ' +
+					'The resize option "' + name + '" misses the "icon" property ' +
+					'or the property value doesn\'t match any of available icons.',
+					editor,
+					option
+				);
+			}
 
 
 			button.set( {
 			button.set( {
-				label: t( label ),
-				withText: true,
-				tooltip: parsedValue ? t( 'Resize image to' ) + ' ' + parsedValue : t( 'Resize image to the original size' ),
-				isToggleable: true,
-				commandValue: parsedValue
+				// Use the `label` property for a verbose description (because of ARIA).
+				label: labelText,
+				icon: RESIZE_ICONS[ icon ],
+				tooltip: labelText,
+				isToggleable: true
 			} );
 			} );
 
 
 			// Bind button to the command.
 			// Bind button to the command.
 			button.bind( 'isEnabled' ).to( this );
 			button.bind( 'isEnabled' ).to( this );
-			button.bind( 'isOn' ).to( command, 'value', commandCallback );
+			button.bind( 'isOn' ).to( command, 'value', getIsOnButtonCallback( optionValueWithUnit ) );
 
 
-			this.listenTo( button, 'execute', evt => {
-				editor.execute( 'imageResize', { width: evt.source.commandValue } );
+			this.listenTo( button, 'execute', () => {
+				editor.execute( 'imageResize', { width: optionValueWithUnit } );
 			} );
 			} );
 
 
 			return button;
 			return button;
@@ -99,19 +147,16 @@ export default class ImageResizeUI extends Plugin {
 	}
 	}
 
 
 	/**
 	/**
-	 * A helper function that creates a dropdown component for the plugin containing all resize options created through the
-	 * plugin configuration settings.
+	 * A helper function that creates a dropdown component for the plugin containing all resize options defined in
+	 * the editor configuration.
 	 *
 	 *
 	 * @private
 	 * @private
-	 *
-	 * @param {Array.<module:image/imageresize/imageresizeui~ImageResizeOption>} options An array of the configured options.
-	 * @param {String} unit A resize unit.
+	 * @param {Array.<module:image/imageresize/imageresizeui~ImageResizeOption>} options An array of configured options.
 	 */
 	 */
-	_addDropdown( options, unit ) {
+	_registerImageResizeDropdown( options ) {
 		const editor = this.editor;
 		const editor = this.editor;
 		const t = editor.t;
 		const t = editor.t;
-		const firstOption = options[ 0 ];
-		const resetOption = options.find( option => option.value === null );
+		const originalSizeOption = options.find( option => !option.value );
 
 
 		// Register dropdown.
 		// Register dropdown.
 		editor.ui.componentFactory.add( 'imageResize', locale => {
 		editor.ui.componentFactory.add( 'imageResize', locale => {
@@ -121,19 +166,25 @@ export default class ImageResizeUI extends Plugin {
 
 
 			dropdownButton.set( {
 			dropdownButton.set( {
 				tooltip: t( 'Resize image' ),
 				tooltip: t( 'Resize image' ),
-				commandValue: firstOption.value,
+				commandValue: originalSizeOption.value,
+				icon: iconMedium,
 				isToggleable: true,
 				isToggleable: true,
-				label: firstOption.label,
-				withText: true
+				label: this._getOptionLabelValue( originalSizeOption ),
+				withText: true,
+				class: 'ck-resize-image-button'
 			} );
 			} );
 
 
 			dropdownButton.bind( 'label' ).to( command, 'value', commandValue => {
 			dropdownButton.bind( 'label' ).to( command, 'value', commandValue => {
-				return commandValue && commandValue.width || resetOption.label;
+				if ( commandValue && commandValue.width ) {
+					return commandValue.width;
+				} else {
+					return this._getOptionLabelValue( originalSizeOption );
+				}
 			} );
 			} );
 			dropdownView.bind( 'isOn' ).to( command );
 			dropdownView.bind( 'isOn' ).to( command );
 			dropdownView.bind( 'isEnabled' ).to( this );
 			dropdownView.bind( 'isEnabled' ).to( this );
 
 
-			addListToDropdown( dropdownView, prepareListDefinitions( options, command, unit ) );
+			addListToDropdown( dropdownView, this._getResizeDropdownListItemDefinitions( options, command ) );
 
 
 			dropdownView.listView.ariaLabel = t( 'Image resize list' );
 			dropdownView.listView.ariaLabel = t( 'Image resize list' );
 
 
@@ -146,40 +197,73 @@ export default class ImageResizeUI extends Plugin {
 			return dropdownView;
 			return dropdownView;
 		} );
 		} );
 	}
 	}
-}
 
 
-// A helper function for parsing resize options definitions.
-function prepareListDefinitions( definitions, command, resizeUnit ) {
-	const itemDefinitions = new Collection();
-
-	definitions.map( itemDefinition => {
-		const parsedValue = itemDefinition.value ? itemDefinition.value + resizeUnit : null;
-		const definition = {
-			type: 'button',
-			model: new Model( {
-				commandName: 'imageResize',
-				commandValue: parsedValue,
-				label: itemDefinition.label,
-				withText: true,
-				icon: null
-			} )
-		};
-
-		const commandCallback = setOptionOn( parsedValue );
-
-		definition.model.bind( 'isOn' ).to( command, 'value', commandCallback );
+	/**
+	 * A helper function for creating an option label value string.
+	 *
+	 * @private
+	 * @param {module:image/imageresize/imageresizeui~ImageResizeOption} option A resize option object.
+	 * @param {Boolean} [forTooltip] An optional flag for creating a tooltip label.
+	 * @returns {String} A user-defined label, a label combined from the value and resize unit or the default label
+	 * for reset options (`Original`).
+	 */
+	_getOptionLabelValue( option, forTooltip ) {
+		const t = this.editor.t;
+
+		if ( option.label ) {
+			return option.label;
+		} else if ( forTooltip ) {
+			if ( option.value ) {
+				return t( 'Resize image to %0', option.value + this._resizeUnit );
+			} else {
+				return t( 'Resize image to the original size' );
+			}
+		} else {
+			if ( option.value ) {
+				return option.value + this._resizeUnit;
+			} else {
+				return t( 'Original' );
+			}
+		}
+	}
 
 
-		itemDefinitions.add( definition );
-	} );
+	/**
+	 * A helper function that parses resize options and returns list item definitions ready for use in a dropdown.
+	 *
+	 * @private
+	 * @param {Array.<module:image/imageresize/imageresizeui~ImageResizeOption>} options The resize options.
+	 * @param {module:image/imageresize/imageresizecommand~ImageResizeCommand} command A resize image command.
+	 * @returns {Iterable.<module:ui/dropdown/utils~ListDropdownItemDefinition>} Dropdown item definitions.
+	 */
+	_getResizeDropdownListItemDefinitions( options, command ) {
+		const itemDefinitions = new Collection();
+
+		options.map( option => {
+			const optionValueWithUnit = option.value ? option.value + this._resizeUnit : null;
+			const definition = {
+				type: 'button',
+				model: new Model( {
+					commandName: 'imageResize',
+					commandValue: optionValueWithUnit,
+					label: this._getOptionLabelValue( option ),
+					withText: true,
+					icon: null
+				} )
+			};
+
+			definition.model.bind( 'isOn' ).to( command, 'value', getIsOnButtonCallback( optionValueWithUnit ) );
+
+			itemDefinitions.add( definition );
+		} );
 
 
-	return itemDefinitions;
+		return itemDefinitions;
+	}
 }
 }
 
 
-// A helper function for setting the `isOn` state used for creating a callback function in a value binding.
-function setOptionOn( value ) {
+// A helper function for setting the `isOn` state of buttons in value bindings.
+function getIsOnButtonCallback( value ) {
 	return commandValue => {
 	return commandValue => {
-		// Set reseting option on when command equals `null`.
-		if ( commandValue === value ) {
+		if ( value === null && commandValue === value ) {
 			return true;
 			return true;
 		}
 		}
 
 
@@ -188,12 +272,19 @@ function setOptionOn( value ) {
 }
 }
 
 
 /**
 /**
- * A resize option type.
+ * An image resize option used in the {@link module:image/image~ImageConfig#resizeOptions image resize configuration}.
  *
  *
  * @typedef {Object} module:image/imageresize/imageresizeui~ImageResizeOption
  * @typedef {Object} module:image/imageresize/imageresizeui~ImageResizeOption
- *
- * @property {String} resizeOption.name A name of the option used for creating a component.
- * You refer to that name later in the {@link module:image/image~ImageConfig#toolbar}.
- * @property {String} resizeOption.label A label to be displayed with a button.
- * @property {String} resizeOption.value A value of a resize option. `null` value is for resetting an image to its original size.
+ * @property {String} name A name of the UI component that changes the image size.
+ * * If you configure the feature using individual resize buttons, you can refer to this name in the
+ * {@link module:image/image~ImageConfig#toolbar image toolbar configuration}.
+ * * If you configure the feature using the resize dropdown, this name will be used for a list item in the dropdown.
+ * @property {String} value A value of a resize option without the unit
+ * ({@link module:image/image~ImageConfig#resizeUnit configured separately}). `null` resets an image to its original size.
+ * @property {String} [resizeOptions.icon] An icon used by an individual resize button (see the `name` property to learn more).
+ * Available icons are: `'small'`, `'medium'`, `'large'`, `'original'`.
+ * @property {String} [label] A label of the option displayed in the dropdown or, if the feature is configured using
+ * individual buttons, a {@link module:ui/button/buttonview~ButtonView#tooltip} and an ARIA attribute of a button.
+ * If not specified, the label is generated automatically based on the option `value` and the
+ * {@link module:image/image~ImageConfig#resizeUnit `config.image.resizeUnit`}.
  */
  */

+ 136 - 68
packages/ckeditor5-image/tests/imageresize/imageresizeui.js

@@ -17,23 +17,30 @@ import Undo from '@ckeditor/ckeditor5-undo/src/undo';
 import Table from '@ckeditor/ckeditor5-table/src/table';
 import Table from '@ckeditor/ckeditor5-table/src/table';
 
 
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
+
+import iconSmall from '@ckeditor/ckeditor5-core/theme/icons/object-size-small.svg';
+import iconMedium from '@ckeditor/ckeditor5-core/theme/icons/object-size-medium.svg';
+import iconLarge from '@ckeditor/ckeditor5-core/theme/icons/object-size-large.svg';
+import iconFull from '@ckeditor/ckeditor5-core/theme/icons/object-size-full.svg';
 
 
 describe( 'ImageResizeUI', () => {
 describe( 'ImageResizeUI', () => {
 	let plugin, command, editor, editorElement;
 	let plugin, command, editor, editorElement;
 
 
 	const resizeOptions = [ {
 	const resizeOptions = [ {
 		name: 'imageResize:original',
 		name: 'imageResize:original',
-		label: 'Original size',
 		value: null
 		value: null
 	},
 	},
+	{
+		name: 'imageResize:25',
+		value: '25'
+	},
 	{
 	{
 		name: 'imageResize:50',
 		name: 'imageResize:50',
-		label: '50%',
 		value: '50'
 		value: '50'
 	},
 	},
 	{
 	{
 		name: 'imageResize:75',
 		name: 'imageResize:75',
-		label: '75%',
 		value: '75'
 		value: '75'
 	} ];
 	} ];
 
 
@@ -47,7 +54,6 @@ describe( 'ImageResizeUI', () => {
 			.create( editorElement, {
 			.create( editorElement, {
 				plugins: [ Image, ImageStyle, Paragraph, Undo, Table, ImageResizeUI ],
 				plugins: [ Image, ImageStyle, Paragraph, Undo, Table, ImageResizeUI ],
 				image: {
 				image: {
-					resizeUnit: '%',
 					resizeOptions
 					resizeOptions
 				}
 				}
 			} );
 			} );
@@ -70,66 +76,25 @@ describe( 'ImageResizeUI', () => {
 		it( 'should be named', () => {
 		it( 'should be named', () => {
 			expect( ImageResizeUI.pluginName ).to.equal( 'ImageResizeUI' );
 			expect( ImageResizeUI.pluginName ).to.equal( 'ImageResizeUI' );
 		} );
 		} );
-
-		it( 'should be disabled when command is disabled', () => {
-			command.isEnabled = true;
-
-			expect( plugin.isEnabled ).to.be.true;
-
-			command.isEnabled = false;
-
-			expect( plugin.isEnabled ).to.be.false;
-		} );
 	} );
 	} );
 
 
-	describe( 'init()', () => {
-		it( 'should have set "%" resize unit', () => {
-			const unit = editor.config.get( 'image.resizeUnit' );
+	describe( 'constructor()', () => {
+		it( 'should create `_resizeUnit` with default value of `%`', () => {
+			const unit = plugin._resizeUnit;
 
 
 			expect( unit ).to.equal( '%' );
 			expect( unit ).to.equal( '%' );
 		} );
 		} );
+	} );
 
 
-		it( 'should have set "%" resize unit if not defined', async () => {
-			const editor = await ClassicTestEditor
-				.create( editorElement, {
-					plugins: [ Image, ImageStyle, Paragraph, Undo, Table, ImageResizeUI ],
-					image: {
-						resizeOptions
-					}
-				} );
-
-			const button = editor.ui.componentFactory.create( 'imageResize:50' );
-			const command = editor.commands.get( 'imageResize' );
-
-			command.isEnabled = true;
-
-			button.fire( 'execute' );
-
-			expect( command.value.width.includes( '%' ) ).to.be.true;
-
-			await editor.destroy();
-		} );
-
-		it( 'should have set "px" resize unit', async () => {
-			const editor = await ClassicTestEditor
-				.create( editorElement, {
-					plugins: [ Image, ImageStyle, Paragraph, Undo, Table, ImageResizeUI ],
-					image: {
-						resizeUnit: 'px',
-						resizeOptions
-					}
-				} );
-
-			const button = editor.ui.componentFactory.create( 'imageResize:50' );
-			const command = editor.commands.get( 'imageResize' );
-
+	describe( 'init()', () => {
+		it( 'should be disabled when command is disabled', () => {
 			command.isEnabled = true;
 			command.isEnabled = true;
 
 
-			button.fire( 'execute' );
+			expect( plugin.isEnabled ).to.be.true;
 
 
-			expect( command.value.width.includes( 'px' ) ).to.be.true;
+			command.isEnabled = false;
 
 
-			await editor.destroy();
+			expect( plugin.isEnabled ).to.be.false;
 		} );
 		} );
 
 
 		it( 'should not register a dropdown or buttons if no resize options passed', async () => {
 		it( 'should not register a dropdown or buttons if no resize options passed', async () => {
@@ -167,12 +132,12 @@ describe( 'ImageResizeUI', () => {
 			expect( editor.ui.componentFactory.create( 'imageResize' ) ).to.be.instanceof( DropdownView );
 			expect( editor.ui.componentFactory.create( 'imageResize' ) ).to.be.instanceof( DropdownView );
 		} );
 		} );
 
 
-		it( 'should have 3 resize options in the `imageResize` dropdown', () => {
+		it( 'should have 4 resize options in the `imageResize` dropdown', () => {
 			const dropdownView = editor.ui.componentFactory.create( 'imageResize' );
 			const dropdownView = editor.ui.componentFactory.create( 'imageResize' );
 
 
-			expect( dropdownView.listView.items.length ).to.equal( 3 );
-			expect( dropdownView.listView.items.first.element.textContent ).to.equal( 'Original size' );
-			expect( dropdownView.listView.items._items[ 1 ].element.textContent ).to.equal( '50%' );
+			expect( dropdownView.listView.items.length ).to.equal( 4 );
+			expect( dropdownView.listView.items.first.element.textContent ).to.equal( 'Original' );
+			expect( dropdownView.listView.items._items[ 1 ].element.textContent ).to.equal( '25%' );
 			expect( dropdownView.listView.items.last.element.textContent ).to.equal( '75%' );
 			expect( dropdownView.listView.items.last.element.textContent ).to.equal( '75%' );
 		} );
 		} );
 
 
@@ -192,11 +157,56 @@ describe( 'ImageResizeUI', () => {
 			resizeBy50Percent.fire( 'execute' );
 			resizeBy50Percent.fire( 'execute' );
 
 
 			sinon.assert.calledOnce( commandSpy );
 			sinon.assert.calledOnce( commandSpy );
-			expect( command.value.width ).to.equal( '50%' );
+			expect( command.value.width ).to.equal( '25%' );
 		} );
 		} );
 	} );
 	} );
 
 
 	describe( 'resize option button', () => {
 	describe( 'resize option button', () => {
+		let editor, plugin;
+
+		beforeEach( async () => {
+			editor = await ClassicTestEditor
+				.create( editorElement, {
+					plugins: [ Image, ImageStyle, Paragraph, Undo, Table, ImageResizeUI ],
+					image: {
+						resizeUnit: '%',
+						resizeOptions: [ {
+							name: 'imageResize:original',
+							value: null,
+							icon: 'original'
+						},
+						{
+							name: 'imageResize:25',
+							value: '25',
+							icon: 'small'
+						},
+						{
+							name: 'imageResize:50',
+							value: '50',
+							icon: 'medium'
+						},
+						{
+							name: 'imageResize:75',
+							value: '75',
+							icon: 'large'
+						} ],
+						toolbar: [ 'imageResize:original', 'imageResize:25', 'imageResize:50', 'imageResize:75' ]
+					}
+				} );
+
+			plugin = editor.plugins.get( 'ImageResizeUI' );
+		} );
+
+		afterEach( async () => {
+			if ( editorElement ) {
+				editorElement.remove();
+			}
+
+			if ( editor ) {
+				await editor.destroy();
+			}
+		} );
+
 		it( 'should be bound to `#isEnabled`', () => {
 		it( 'should be bound to `#isEnabled`', () => {
 			const buttonView = editor.ui.componentFactory.create( 'imageResize:50' );
 			const buttonView = editor.ui.componentFactory.create( 'imageResize:50' );
 
 
@@ -213,13 +223,41 @@ describe( 'ImageResizeUI', () => {
 			expect( editor.ui.componentFactory.create( 'imageResize:50' ) ).to.be.instanceof( ButtonView );
 			expect( editor.ui.componentFactory.create( 'imageResize:50' ) ).to.be.instanceof( ButtonView );
 		} );
 		} );
 
 
-		it( 'should be created with visible "50%" label', () => {
+		it( 'should be created with invisible "Resize image: 30%" label when is provided', async () => {
+			const editor = await ClassicTestEditor
+				.create( editorElement, {
+					plugins: [ Image, ImageStyle, Paragraph, Undo, Table, ImageResizeUI ],
+					image: {
+						resizeUnit: '%',
+						resizeOptions: [ {
+							name: 'imageResize:30',
+							value: '30',
+							label: 'Resize image: 30%',
+							icon: 'small'
+						} ],
+						toolbar: [ 'imageResize:30' ]
+					}
+				} );
+
+			const buttonView = editor.ui.componentFactory.create( 'imageResize:30' );
+			buttonView.render();
+
+			expect( buttonView.withText ).to.be.false;
+			expect( buttonView.label ).to.equal( 'Resize image: 30%' );
+			expect( buttonView.labelView ).to.be.instanceOf( View );
+
+			editor.destroy();
+		} );
+
+		it( 'should be created with invisible "Resize image to 50%" label when is not provided', async () => {
 			const buttonView = editor.ui.componentFactory.create( 'imageResize:50' );
 			const buttonView = editor.ui.componentFactory.create( 'imageResize:50' );
 			buttonView.render();
 			buttonView.render();
 
 
-			expect( buttonView.withText ).to.be.true;
-			expect( buttonView.label ).to.equal( '50%' );
+			expect( buttonView.withText ).to.be.false;
+			expect( buttonView.label ).to.equal( 'Resize image to 50%' );
 			expect( buttonView.labelView ).to.be.instanceOf( View );
 			expect( buttonView.labelView ).to.be.instanceOf( View );
+
+			editor.destroy();
 		} );
 		} );
 
 
 		it( 'should be created with a proper tooltip, depends on the set value', () => {
 		it( 'should be created with a proper tooltip, depends on the set value', () => {
@@ -233,12 +271,6 @@ describe( 'ImageResizeUI', () => {
 			expect( buttonView50.tooltip ).to.equal( 'Resize image to 50%' );
 			expect( buttonView50.tooltip ).to.equal( 'Resize image to 50%' );
 		} );
 		} );
 
 
-		it( 'should have `commandValue` equal "50%"', () => {
-			const buttonView = editor.ui.componentFactory.create( 'imageResize:50' );
-
-			expect( buttonView.commandValue ).to.equal( '50%' );
-		} );
-
 		it( 'should execute `imageResize` command with "50%" value', () => {
 		it( 'should execute `imageResize` command with "50%" value', () => {
 			const buttonView = editor.ui.componentFactory.create( 'imageResize:50' );
 			const buttonView = editor.ui.componentFactory.create( 'imageResize:50' );
 			const command = editor.commands.get( 'imageResize' );
 			const command = editor.commands.get( 'imageResize' );
@@ -251,5 +283,41 @@ describe( 'ImageResizeUI', () => {
 			sinon.assert.calledOnce( commandSpy );
 			sinon.assert.calledOnce( commandSpy );
 			expect( command.value.width ).to.equal( '50%' );
 			expect( command.value.width ).to.equal( '50%' );
 		} );
 		} );
+
+		it( 'should have set a proper icon', () => {
+			const buttonOriginal = editor.ui.componentFactory.create( 'imageResize:original' );
+			const button25 = editor.ui.componentFactory.create( 'imageResize:25' );
+			const button50 = editor.ui.componentFactory.create( 'imageResize:50' );
+			const button75 = editor.ui.componentFactory.create( 'imageResize:75' );
+
+			expect( buttonOriginal.icon ).to.deep.equal( iconFull );
+			expect( button25.icon ).to.deep.equal( iconSmall );
+			expect( button50.icon ).to.deep.equal( iconMedium );
+			expect( button75.icon ).to.deep.equal( iconLarge );
+		} );
+
+		it( 'should throw the CKEditorError if no `icon` is provided', async () => {
+			const editor = await ClassicTestEditor
+				.create( editorElement, {
+					plugins: [ Image, ImageStyle, Paragraph, Undo, Table, ImageResizeUI ],
+					image: {
+						resizeUnit: '%',
+						resizeOptions: [ {
+							name: 'imageResize:noicon',
+							value: '100'
+						} ],
+						toolbar: [ 'imageResize:noicon' ]
+					}
+				} );
+
+			const errMsg = 'The resize option "imageResize:noicon" misses the "icon" property ' +
+				'or the property value doesn\'t match any of available icons.';
+
+			expectToThrowCKEditorError( () => {
+				editor.ui.componentFactory.create( 'imageResize:noicon' );
+			}, errMsg, editor );
+
+			editor.destroy();
+		} );
 	} );
 	} );
 } );
 } );

+ 10 - 13
packages/ckeditor5-image/tests/manual/imageresizeui.js

@@ -36,17 +36,14 @@ const imageConfig1 = {
 	resizeOptions: [
 	resizeOptions: [
 		{
 		{
 			name: 'imageResize:original',
 			name: 'imageResize:original',
-			label: 'Original size',
 			value: null
 			value: null
 		},
 		},
 		{
 		{
 			name: 'imageResize:50',
 			name: 'imageResize:50',
-			label: '50%',
 			value: '50'
 			value: '50'
 		},
 		},
 		{
 		{
 			name: 'imageResize:75',
 			name: 'imageResize:75',
-			label: '75%',
 			value: '75'
 			value: '75'
 		}
 		}
 	],
 	],
@@ -66,7 +63,7 @@ const config1 = {
 ClassicEditor
 ClassicEditor
 	.create( document.querySelector( '#editor1' ), config1 )
 	.create( document.querySelector( '#editor1' ), config1 )
 	.then( editor => {
 	.then( editor => {
-		window.editor = editor;
+		window.editor1 = editor;
 	} )
 	} )
 	.catch( err => {
 	.catch( err => {
 		console.error( err.stack );
 		console.error( err.stack );
@@ -77,27 +74,27 @@ const imageConfig2 = {
 	resizeOptions: [
 	resizeOptions: [
 		{
 		{
 			name: 'imageResize:original',
 			name: 'imageResize:original',
-			label: 'Original size',
-			value: null
+			value: null,
+			icon: 'original'
 		},
 		},
 		{
 		{
 			name: 'imageResize:50',
 			name: 'imageResize:50',
-			label: '50%',
-			value: '50'
+			value: '50',
+			icon: 'medium'
 		},
 		},
 		{
 		{
 			name: 'imageResize:75',
 			name: 'imageResize:75',
-			label: '75%',
-			value: '75'
+			value: '75',
+			icon: 'large'
 		}
 		}
 	],
 	],
 	toolbar: [
 	toolbar: [
 		'imageStyle:alignLeft',
 		'imageStyle:alignLeft',
 		'imageStyle:full',
 		'imageStyle:full',
 		'imageStyle:side', '|',
 		'imageStyle:side', '|',
-		'imageResize:original',
 		'imageResize:50',
 		'imageResize:50',
-		'imageResize:75'
+		'imageResize:75',
+		'imageResize:original'
 	],
 	],
 	styles: [
 	styles: [
 		'full',
 		'full',
@@ -114,7 +111,7 @@ const config2 = {
 ClassicEditor
 ClassicEditor
 	.create( document.querySelector( '#editor2' ), config2 )
 	.create( document.querySelector( '#editor2' ), config2 )
 	.then( editor => {
 	.then( editor => {
-		window.editor = editor;
+		window.editor2 = editor;
 	} )
 	} )
 	.catch( err => {
 	.catch( err => {
 		console.error( err.stack );
 		console.error( err.stack );

+ 18 - 9
packages/ckeditor5-image/tests/manual/imageresizeui.md

@@ -1,11 +1,20 @@
 ## Image Resize UI
 ## Image Resize UI
 
 
-The tests for manual image resizing.
-- The first test should have the dropdown with configured options in the image toolbar (using `imageResize`).
-	- Plugin icon should appear only in the dropbdown button.
-	- Each option should have a label text represented an option value defined in the plugin configuration.
-	- Selected options should be set "on" when dropdown is open.
-- The second one should have the standalone buttons instead of dropdown (from the first test) in the image toolbar (using
-`imageResize:option`).
-	- Each option should have the plugin icon "stretched" over the label text which represents an option value defined in the plugin configuration.
-	- Selected option should be set "on".
+Tests for manual image resizing using the UI in the image toolbar.
+
+## Dropdown
+
+1. Select an image in the editor.
+2. A dropdown with configured options (`config.image.resizeOptions`) should be visible in the toolbar.
+	- The plugin icon should appear only next to the dropdown button.
+	- Each option should have a label text corresponding to an option value defined in the configuration.
+	- The selected option should be "on" when the dropdown is open.
+
+## Buttons
+
+1. Select an image in the editor.
+2. Standalone buttons should be displayed in the image toolbar (corresponding to `config.image.resizeOptions`).
+	- Each button should have an icon as in the configuration (`small`, `medium`, `large` or `original`).
+	- No label should be rendered,
+	- The tooltip text and the `aria-label` attribute should be the same (and more verbose).
+	- The selected option button should be "on".

+ 12 - 0
packages/ckeditor5-image/theme/imageresize.css

@@ -23,3 +23,15 @@
 		display: block;
 		display: block;
 	}
 	}
 }
 }
+
+[dir="ltr"] .ck.ck-button.ck-button_with-text.ck-resize-image-button .ck-button__icon {
+	margin-right: var(--ck-spacing-standard);
+}
+
+[dir="rtl"] .ck.ck-button.ck-button_with-text.ck-resize-image-button .ck-button__icon {
+	margin-left: var(--ck-spacing-standard);
+}
+
+.ck.ck-dropdown .ck-button.ck-resize-image-button .ck-button__label {
+	width: 4em;
+}

+ 1 - 1
packages/ckeditor5-link/package.json

@@ -10,6 +10,7 @@
     "ckeditor5-plugin"
     "ckeditor5-plugin"
   ],
   ],
   "dependencies": {
   "dependencies": {
+    "@ckeditor/ckeditor5-clipboard": "^20.0.0",
     "@ckeditor/ckeditor5-core": "^20.0.0",
     "@ckeditor/ckeditor5-core": "^20.0.0",
     "@ckeditor/ckeditor5-engine": "^20.0.0",
     "@ckeditor/ckeditor5-engine": "^20.0.0",
     "@ckeditor/ckeditor5-image": "^20.0.0",
     "@ckeditor/ckeditor5-image": "^20.0.0",
@@ -21,7 +22,6 @@
   "devDependencies": {
   "devDependencies": {
     "@ckeditor/ckeditor5-basic-styles": "^20.0.0",
     "@ckeditor/ckeditor5-basic-styles": "^20.0.0",
     "@ckeditor/ckeditor5-block-quote": "^20.0.0",
     "@ckeditor/ckeditor5-block-quote": "^20.0.0",
-    "@ckeditor/ckeditor5-clipboard": "^20.0.0",
     "@ckeditor/ckeditor5-code-block": "^20.0.0",
     "@ckeditor/ckeditor5-code-block": "^20.0.0",
     "@ckeditor/ckeditor5-editor-classic": "^20.0.0",
     "@ckeditor/ckeditor5-editor-classic": "^20.0.0",
     "@ckeditor/ckeditor5-enter": "^20.0.0",
     "@ckeditor/ckeditor5-enter": "^20.0.0",

+ 135 - 1
packages/ckeditor5-link/src/linkediting.js

@@ -10,6 +10,8 @@
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import MouseObserver from '@ckeditor/ckeditor5-engine/src/view/observer/mouseobserver';
 import MouseObserver from '@ckeditor/ckeditor5-engine/src/view/observer/mouseobserver';
 import TwoStepCaretMovement from '@ckeditor/ckeditor5-typing/src/twostepcaretmovement';
 import TwoStepCaretMovement from '@ckeditor/ckeditor5-typing/src/twostepcaretmovement';
+import Input from '@ckeditor/ckeditor5-typing/src/input';
+import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard';
 import LinkCommand from './linkcommand';
 import LinkCommand from './linkcommand';
 import UnlinkCommand from './unlinkcommand';
 import UnlinkCommand from './unlinkcommand';
 import AutomaticDecorators from './utils/automaticdecorators';
 import AutomaticDecorators from './utils/automaticdecorators';
@@ -44,7 +46,8 @@ export default class LinkEditing extends Plugin {
 	 * @inheritDoc
 	 * @inheritDoc
 	 */
 	 */
 	static get requires() {
 	static get requires() {
-		return [ TwoStepCaretMovement ];
+		// Clipboard is required for handling cut and paste events while typing over the link.
+		return [ TwoStepCaretMovement, Input, Clipboard ];
 	}
 	}
 
 
 	/**
 	/**
@@ -110,6 +113,9 @@ export default class LinkEditing extends Plugin {
 
 
 		// Handle a click at the beginning/end of a link element.
 		// Handle a click at the beginning/end of a link element.
 		this._enableClickingAfterLink();
 		this._enableClickingAfterLink();
+
+		// Handle typing over the link.
+		this._enableTypingOverLink();
 	}
 	}
 
 
 	/**
 	/**
@@ -414,4 +420,132 @@ export default class LinkEditing extends Plugin {
 			}
 			}
 		} );
 		} );
 	}
 	}
+
+	/**
+	 * Starts listening to {@link module:engine/model/model~Model#deleteContent} and {@link module:engine/model/model~Model#insertContent}
+	 * and checks whether typing over the link. If so, attributes of removed text are preserved and applied to the inserted text.
+	 *
+	 * The purpose of this action is to allow modifying a text without loosing the `linkHref` attribute (and other).
+	 *
+	 * See https://github.com/ckeditor/ckeditor5/issues/4762.
+	 *
+	 * @private
+	 */
+	_enableTypingOverLink() {
+		const editor = this.editor;
+		const view = editor.editing.view;
+
+		// Selection attributes when started typing over the link.
+		let selectionAttributes;
+
+		// Whether pressed `Backspace` or `Delete`. If so, attributes should not be preserved.
+		let deletedContent;
+
+		// Detect pressing `Backspace` / `Delete`.
+		this.listenTo( view.document, 'delete', () => {
+			deletedContent = true;
+		}, { priority: 'high' } );
+
+		// Listening to `model#deleteContent` allows detecting whether selected content was a link.
+		// If so, before removing the element, we will copy its attributes.
+		this.listenTo( editor.model, 'deleteContent', () => {
+			const selection = editor.model.document.selection;
+
+			// Copy attributes only if anything is selected.
+			if ( selection.isCollapsed ) {
+				return;
+			}
+
+			// When the content was deleted, do not preserve attributes.
+			if ( deletedContent ) {
+				deletedContent = false;
+
+				return;
+			}
+
+			// Enabled only when typing.
+			if ( !isTyping( editor ) ) {
+				return;
+			}
+
+			if ( shouldCopyAttributes( editor.model ) ) {
+				selectionAttributes = selection.getAttributes();
+			}
+		}, { priority: 'high' } );
+
+		// Listening to `model#insertContent` allows detecting the content insertion.
+		// We want to apply attributes that were removed while typing over the link.
+		this.listenTo( editor.model, 'insertContent', ( evt, [ element ] ) => {
+			deletedContent = false;
+
+			// Enabled only when typing.
+			if ( !isTyping( editor ) ) {
+				return;
+			}
+
+			if ( !selectionAttributes ) {
+				return;
+			}
+
+			editor.model.change( writer => {
+				for ( const [ attribute, value ] of selectionAttributes ) {
+					writer.setAttribute( attribute, value, element );
+				}
+			} );
+
+			selectionAttributes = null;
+		}, { priority: 'high' } );
+	}
+}
+
+// Checks whether selection's attributes should be copied to the new inserted text.
+//
+// @param {module:engine/model/model~Model} model
+// @returns {Boolean}
+function shouldCopyAttributes( model ) {
+	const selection = model.document.selection;
+	const firstPosition = selection.getFirstPosition();
+	const lastPosition = selection.getLastPosition();
+	const nodeAtFirstPosition = firstPosition.nodeAfter;
+
+	// The text link node does not exist...
+	if ( !nodeAtFirstPosition ) {
+		return false;
+	}
+
+	// ...or it isn't the text node...
+	if ( !nodeAtFirstPosition.is( 'text' ) ) {
+		return false;
+	}
+
+	// ...or isn't the link.
+	if ( !nodeAtFirstPosition.hasAttribute( 'linkHref' ) ) {
+		return false;
+	}
+
+	// `textNode` = the position is inside the link element.
+	// `nodeBefore` = the position is at the end of the link element.
+	const nodeAtLastPosition = lastPosition.textNode || lastPosition.nodeBefore;
+
+	// If both references the same node selection contains a single text node.
+	if ( nodeAtFirstPosition === nodeAtLastPosition ) {
+		return true;
+	}
+
+	// If nodes are not equal, maybe the link nodes has defined additional attributes inside.
+	// First, we need to find the entire link range.
+	const linkRange = findLinkRange( firstPosition, nodeAtFirstPosition.getAttribute( 'linkHref' ), model );
+
+	// Then we can check whether selected range is inside the found link range. If so, attributes should be preserved.
+	return linkRange.containsRange( model.createRange( firstPosition, lastPosition ), true );
+}
+
+// Checks whether provided changes were caused by typing.
+//
+// @params {module:core/editor/editor~Editor} editor
+// @returns {Boolean}
+function isTyping( editor ) {
+	const input = editor.plugins.get( 'Input' );
+
+	return input.isInput( editor.model.change( writer => writer.batch ) );
 }
 }

+ 2 - 0
packages/ckeditor5-link/src/linkimage.js

@@ -11,6 +11,8 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import LinkImageEditing from './linkimageediting';
 import LinkImageEditing from './linkimageediting';
 import LinkImageUI from './linkimageui';
 import LinkImageUI from './linkimageui';
 
 
+import '../theme/linkimage.css';
+
 /**
 /**
  * The `LinkImage` plugin.
  * The `LinkImage` plugin.
  *
  *

+ 13 - 0
packages/ckeditor5-link/src/linkimageediting.js

@@ -11,6 +11,8 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import ImageEditing from '@ckeditor/ckeditor5-image/src/image/imageediting';
 import ImageEditing from '@ckeditor/ckeditor5-image/src/image/imageediting';
 import LinkEditing from './linkediting';
 import LinkEditing from './linkediting';
 
 
+import linkIcon from '../theme/icons/link.svg';
+
 /**
 /**
  * The link image engine feature.
  * The link image engine feature.
  *
  *
@@ -115,6 +117,14 @@ function downcastImageLink() {
 			// But we need to check whether the link element exists.
 			// But we need to check whether the link element exists.
 			const linkInImage = Array.from( viewFigure.getChildren() ).find( child => child.name === 'a' );
 			const linkInImage = Array.from( viewFigure.getChildren() ).find( child => child.name === 'a' );
 
 
+			// Create an icon indicator for a linked image.
+			const linkIconIndicator = writer.createUIElement( 'span', { class: 'ck ck-link-image_icon' }, function( domDocument ) {
+				const domElement = this.toDomElement( domDocument );
+				domElement.innerHTML = linkIcon;
+
+				return domElement;
+			} );
+
 			// If so, update the attribute if it's defined or remove the entire link if the attribute is empty.
 			// If so, update the attribute if it's defined or remove the entire link if the attribute is empty.
 			if ( linkInImage ) {
 			if ( linkInImage ) {
 				if ( data.attributeNewValue ) {
 				if ( data.attributeNewValue ) {
@@ -135,6 +145,9 @@ function downcastImageLink() {
 
 
 				// 3. Move the image to the link.
 				// 3. Move the image to the link.
 				writer.move( writer.createRangeOn( viewFigure.getChild( 1 ) ), writer.createPositionAt( linkElement, 0 ) );
 				writer.move( writer.createRangeOn( viewFigure.getChild( 1 ) ), writer.createPositionAt( linkElement, 0 ) );
+
+				// 4. Inset the linked image icon indicator.
+				writer.insert( writer.createPositionAt( linkElement, 'end' ), linkIconIndicator );
 			}
 			}
 		} );
 		} );
 	};
 	};

+ 2 - 2
packages/ckeditor5-link/src/linkui.js

@@ -85,7 +85,7 @@ export default class LinkUI extends Plugin {
 		this._enableUserBalloonInteractions();
 		this._enableUserBalloonInteractions();
 
 
 		// Renders a fake visual selection marker on an expanded selection.
 		// Renders a fake visual selection marker on an expanded selection.
-		editor.conversion.for( 'downcast' ).markerToHighlight( {
+		editor.conversion.for( 'editingDowncast' ).markerToHighlight( {
 			model: VISUAL_SELECTION_MARKER_NAME,
 			model: VISUAL_SELECTION_MARKER_NAME,
 			view: {
 			view: {
 				classes: [ 'ck-fake-link-selection' ]
 				classes: [ 'ck-fake-link-selection' ]
@@ -93,7 +93,7 @@ export default class LinkUI extends Plugin {
 		} );
 		} );
 
 
 		// Renders a fake visual selection marker on a collapsed selection.
 		// Renders a fake visual selection marker on a collapsed selection.
-		editor.conversion.for( 'downcast' ).markerToElement( {
+		editor.conversion.for( 'editingDowncast' ).markerToElement( {
 			model: VISUAL_SELECTION_MARKER_NAME,
 			model: VISUAL_SELECTION_MARKER_NAME,
 			view: {
 			view: {
 				name: 'span',
 				name: 'span',

+ 351 - 11
packages/ckeditor5-link/tests/linkediting.js

@@ -8,15 +8,19 @@ import LinkCommand from '../src/linkcommand';
 import UnlinkCommand from '../src/unlinkcommand';
 import UnlinkCommand from '../src/unlinkcommand';
 
 
 import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
 import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
-import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
-import Enter from '@ckeditor/ckeditor5-enter/src/enter';
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import BoldEditing from '@ckeditor/ckeditor5-basic-styles/src/bold/boldediting';
+import ItalicEditing from '@ckeditor/ckeditor5-basic-styles/src/italic/italicediting';
+import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard';
+import Enter from '@ckeditor/ckeditor5-enter/src/enter';
+import DomEventData from '@ckeditor/ckeditor5-engine/src/view/observer/domeventdata';
+import ImageEditing from '@ckeditor/ckeditor5-image/src/image/imageediting';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import Input from '@ckeditor/ckeditor5-typing/src/input';
 import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 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 { isLinkElement } from '../src/utils';
 import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
 import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
-import Typing from '@ckeditor/ckeditor5-typing/src/typing';
-import BoldEditing from '@ckeditor/ckeditor5-basic-styles/src/bold/boldediting';
+import { isLinkElement } from '../src/utils';
 
 
 /* global document */
 /* global document */
 
 
@@ -869,7 +873,7 @@ describe( 'LinkEditing', () => {
 
 
 		beforeEach( async () => {
 		beforeEach( async () => {
 			editor = await ClassicTestEditor.create( element, {
 			editor = await ClassicTestEditor.create( element, {
-				plugins: [ Paragraph, LinkEditing, Enter, Typing, BoldEditing ],
+				plugins: [ Paragraph, LinkEditing, Enter, Input, BoldEditing ],
 				link: {
 				link: {
 					decorators: {
 					decorators: {
 						isFoo: {
 						isFoo: {
@@ -892,6 +896,11 @@ describe( 'LinkEditing', () => {
 
 
 			model = editor.model;
 			model = editor.model;
 			view = editor.editing.view;
 			view = editor.editing.view;
+
+			model.schema.extend( '$text', {
+				allowIn: '$root',
+				allowAttributes: [ 'linkIsFoo', 'linkIsBar' ]
+			} );
 		} );
 		} );
 
 
 		afterEach( async () => {
 		afterEach( async () => {
@@ -1033,11 +1042,6 @@ describe( 'LinkEditing', () => {
 		} );
 		} );
 
 
 		it( 'should remove manual decorators', () => {
 		it( 'should remove manual decorators', () => {
-			model.schema.extend( '$text', {
-				allowIn: '$root',
-				allowAttributes: [ 'linkIsFoo', 'linkIsBar' ]
-			} );
-
 			setModelData( model, '<paragraph><$text linkIsFoo="true" linkIsBar="true" linkHref="url">Bar[]</$text></paragraph>' );
 			setModelData( model, '<paragraph><$text linkIsFoo="true" linkIsBar="true" linkHref="url">Bar[]</$text></paragraph>' );
 
 
 			editor.editing.view.document.fire( 'mousedown' );
 			editor.editing.view.document.fire( 'mousedown' );
@@ -1056,4 +1060,340 @@ describe( 'LinkEditing', () => {
 			);
 			);
 		} );
 		} );
 	} );
 	} );
+
+	// https://github.com/ckeditor/ckeditor5/issues/4762
+	describe( 'typing over the link', () => {
+		let editor;
+
+		beforeEach( async () => {
+			editor = await ClassicTestEditor.create( element, {
+				plugins: [ Paragraph, LinkEditing, Enter, BoldEditing, ItalicEditing, ImageEditing ],
+				link: {
+					decorators: {
+						isFoo: {
+							mode: 'manual',
+							label: 'Foo',
+							attributes: {
+								class: 'foo'
+							}
+						},
+						isBar: {
+							mode: 'manual',
+							label: 'Bar',
+							attributes: {
+								target: '_blank'
+							}
+						}
+					}
+				}
+			} );
+
+			model = editor.model;
+			view = editor.editing.view;
+
+			model.schema.extend( '$text', {
+				allowIn: '$root',
+				allowAttributes: [ 'linkIsFoo', 'linkIsBar' ]
+			} );
+		} );
+
+		afterEach( async () => {
+			await editor.destroy();
+		} );
+
+		it( 'should require Clipboard plugin', () => {
+			expect( LinkEditing.requires.includes( Clipboard ) ).to.equal( true );
+		} );
+
+		it( 'should require Input plugin', () => {
+			expect( LinkEditing.requires.includes( Input ) ).to.equal( true );
+		} );
+
+		it( 'should preserve selection attributes when the entire link is selected', () => {
+			setModelData( model,
+				'<paragraph>This is [<$text linkHref="foo">Foo</$text>] from <$text linkHref="bar">Bar</$text>.</paragraph>'
+			);
+
+			editor.execute( 'input', {
+				text: 'Abcde'
+			} );
+
+			expect( getModelData( model ) ).to.equal(
+				'<paragraph>This is <$text linkHref="foo">Abcde</$text>[] from <$text linkHref="bar">Bar</$text>.</paragraph>'
+			);
+		} );
+
+		it( 'should preserve selection attributes when the entire link is selected (mixed attributes in the link)', () => {
+			setModelData( model,
+				'<paragraph>' +
+					'This is [' +
+					'<$text linkHref="foo" italic="true">F</$text>' +
+					'<$text linkHref="foo" bold="true">o</$text>' +
+					'<$text linkHref="foo" bold="true" italic="true">o</$text>' +
+					'<$text linkHref="foo" bold="true">B</$text>' +
+					'<$text linkHref="foo" bold="true" italic="true">a</$text>' +
+					'<$text linkHref="foo">r</$text>]' +
+					' from ' +
+					'<$text linkHref="bar">Bar</$text>' +
+					'.' +
+				'</paragraph>'
+			);
+
+			editor.execute( 'input', {
+				text: 'Abcde'
+			} );
+
+			expect( getModelData( model ) ).to.equal(
+				'<paragraph>' +
+					'This is ' +
+					'<$text italic="true" linkHref="foo">Abcde</$text>' +
+					'<$text italic="true">[]</$text>' +
+					' from ' +
+					'<$text linkHref="bar">Bar</$text>' +
+					'.' +
+				'</paragraph>'
+			);
+		} );
+
+		it( 'should preserve selection attributes when the selection starts at the beginning of the link', () => {
+			setModelData( model,
+				'<paragraph>This is [<$text linkHref="foo">Fo]o</$text> from <$text linkHref="bar">Bar</$text>.</paragraph>'
+			);
+
+			editor.execute( 'input', {
+				text: 'Abcde'
+			} );
+
+			expect( getModelData( model ) ).to.equal(
+				'<paragraph>This is <$text linkHref="foo">Abcde[]o</$text> from <$text linkHref="bar">Bar</$text>.</paragraph>'
+			);
+		} );
+
+		it( 'should preserve selection attributes when it starts at the beginning of the link (mixed attributes in the link)', () => {
+			setModelData( model,
+				'<paragraph>' +
+					'This is [' +
+					'<$text linkHref="foo" italic="true">F</$text>' +
+					'<$text linkHref="foo" bold="true">o</$text>' +
+					'<$text linkHref="foo" bold="true" italic="true">o</$text>' +
+					'<$text linkHref="foo" bold="true">B</$text>' +
+					'<$text linkHref="foo" bold="true" italic="true">a]</$text>' +
+					'<$text linkHref="foo">r</$text>' +
+					' from ' +
+					'<$text linkHref="bar">Bar</$text>' +
+					'.' +
+				'</paragraph>'
+			);
+
+			editor.execute( 'input', {
+				text: 'Abcde'
+			} );
+
+			expect( getModelData( model ) ).to.equal(
+				'<paragraph>' +
+					'This is ' +
+					'<$text italic="true" linkHref="foo">Abcde[]</$text>' +
+					'<$text linkHref="foo">r</$text>' +
+					' from ' +
+					'<$text linkHref="bar">Bar</$text>' +
+					'.' +
+				'</paragraph>'
+			);
+		} );
+
+		it( 'should preserve all attributes of the link node (decorators check)', () => {
+			setModelData( model,
+				'<paragraph>' +
+					'This is ' +
+					'<$text linkIsFoo="true" linkIsBar="true" linkHref="foo">[Foo]</$text>' +
+					' from ' +
+					'<$text linkHref="bar">Bar</$text>' +
+					'.' +
+				'</paragraph>'
+			);
+
+			editor.execute( 'input', {
+				text: 'Abcde'
+			} );
+
+			expect( getModelData( model ) ).to.equal(
+				'<paragraph>' +
+					'This is ' +
+					'<$text linkHref="foo" linkIsBar="true" linkIsFoo="true">Abcde</$text>[]' +
+					' from ' +
+					'<$text linkHref="bar">Bar</$text>' +
+					'.' +
+				'</paragraph>'
+			);
+		} );
+
+		it( 'should not preserve selection attributes when the changes are not caused by typing', () => {
+			setModelData( model,
+				'<paragraph>This is [<$text linkHref="foo">Foo</$text>] from <$text linkHref="bar">Bar</$text>.</paragraph>'
+			);
+
+			model.change( writer => {
+				model.deleteContent( model.document.selection );
+				model.insertContent( writer.createText( 'Abcde' ) );
+			} );
+
+			expect( getModelData( model ) ).to.equal(
+				'<paragraph>This is Abcde[] from <$text linkHref="bar">Bar</$text>.</paragraph>'
+			);
+		} );
+
+		it( 'should not preserve selection attributes when the changes are not caused by typing (pasting check)', () => {
+			setModelData( model,
+				'<paragraph>This is [<$text linkHref="foo">Foo</$text>] from <$text linkHref="bar">Bar</$text>.</paragraph>'
+			);
+
+			view.document.fire( 'paste', {
+				dataTransfer: createDataTransfer( {
+					'text/html': '<p>Abcde</p>',
+					'text/plain': 'Abcde'
+				} ),
+				preventDefault: sinon.spy(),
+				stopPropagation: sinon.spy()
+			} );
+
+			expect( getModelData( model ) ).to.equal(
+				'<paragraph>This is Abcde[] from <$text linkHref="bar">Bar</$text>.</paragraph>'
+			);
+		} );
+
+		it( 'should not preserve selection attributes when typed after cutting the content', () => {
+			setModelData( model,
+				'<paragraph>This is [<$text linkHref="foo">Foo</$text>] from <$text linkHref="bar">Bar</$text>.</paragraph>'
+			);
+
+			view.document.fire( 'cut', {
+				dataTransfer: createDataTransfer(),
+				preventDefault: sinon.spy(),
+				stopPropagation: sinon.spy()
+			} );
+
+			editor.execute( 'input', {
+				text: 'Abcde'
+			} );
+
+			expect( getModelData( model ) ).to.equal(
+				'<paragraph>This is Abcde[] from <$text linkHref="bar">Bar</$text>.</paragraph>'
+			);
+		} );
+
+		it( 'should not preserve anything if selected an element instead of text', () => {
+			setModelData( model,
+				'[<image src="/assets/sample.png"></image>]'
+			);
+
+			editor.execute( 'input', {
+				text: 'Abcde'
+			} );
+
+			expect( getModelData( model ) ).to.equal(
+				'Abcde[]'
+			);
+		} );
+
+		it( 'should not preserve anything if selected text does not have the `linkHref` attribute`', () => {
+			setModelData( model,
+				'<paragraph>This is [<$text bold="foo">Foo</$text>] from <$text linkHref="bar">Bar</$text>.</paragraph>'
+			);
+
+			editor.execute( 'input', {
+				text: 'Abcde'
+			} );
+
+			expect( getModelData( model ) ).to.equal(
+				'<paragraph>This is Abcde[] from <$text linkHref="bar">Bar</$text>.</paragraph>'
+			);
+		} );
+
+		it( 'should not preserve selection attributes when the entire link is selected and pressed "Backspace"', () => {
+			setModelData( model,
+				'<paragraph>This is [<$text linkHref="foo">Foo</$text>] from <$text linkHref="bar">Bar</$text>.</paragraph>'
+			);
+
+			view.document.fire( 'delete', new DomEventData( view.document, {
+				keyCode: keyCodes.backspace,
+				preventDefault: () => {}
+			} ) );
+
+			editor.execute( 'input', {
+				text: 'Abcde'
+			} );
+
+			expect( getModelData( model ) ).to.equal(
+				'<paragraph>This is Abcde[] from <$text linkHref="bar">Bar</$text>.</paragraph>'
+			);
+		} );
+
+		it( 'should not preserve selection attributes when the entire link is selected and pressed "Delete"', () => {
+			setModelData( model,
+				'<paragraph>This is [<$text linkHref="foo">Foo</$text>] from <$text linkHref="bar">Bar</$text>.</paragraph>'
+			);
+
+			view.document.fire( 'delete', new DomEventData( view.document, {
+				keyCode: keyCodes.delete,
+				preventDefault: () => {}
+			} ) );
+
+			editor.execute( 'input', {
+				text: 'Abcde'
+			} );
+
+			expect( getModelData( model ) ).to.equal(
+				'<paragraph>This is Abcde[] from <$text linkHref="bar">Bar</$text>.</paragraph>'
+			);
+		} );
+
+		it( 'should not preserve selection attributes when selected different links', () => {
+			setModelData( model,
+				'<paragraph>This is <$text linkHref="foo">[Foo</$text> from <$text linkHref="bar">Bar]</$text>.</paragraph>'
+			);
+
+			editor.execute( 'input', {
+				text: 'Abcde'
+			} );
+
+			expect( getModelData( model ) ).to.equal( '<paragraph>This is Abcde[].</paragraph>' );
+		} );
+
+		it( 'should not preserve selection attributes when selected more than single link (start of the selection)', () => {
+			setModelData( model,
+				'<paragraph>This is[ <$text linkHref="foo">Foo]</$text> from <$text linkHref="bar">Bar</$text>.</paragraph>'
+			);
+
+			editor.execute( 'input', {
+				text: 'Abcde'
+			} );
+
+			expect( getModelData( model ) ).to.equal(
+				'<paragraph>This isAbcde[] from <$text linkHref="bar">Bar</$text>.</paragraph>'
+			);
+		} );
+
+		it( 'should not preserve selection attributes when selected more than single link (end of the selection)', () => {
+			setModelData( model,
+				'<paragraph>This is <$text linkHref="foo">[Foo</$text> ]from <$text linkHref="bar">Bar</$text>.</paragraph>'
+			);
+
+			editor.execute( 'input', {
+				text: 'Abcde'
+			} );
+
+			expect( getModelData( model ) ).to.equal(
+				'<paragraph>This is Abcde[]from <$text linkHref="bar">Bar</$text>.</paragraph>'
+			);
+		} );
+
+		function createDataTransfer( data ) {
+			return {
+				getData( type ) {
+					return data[ type ];
+				},
+				setData() {}
+			};
+		}
+	} );
 } );
 } );

+ 20 - 3
packages/ckeditor5-link/tests/linkimageediting.js

@@ -11,6 +11,12 @@ import normalizeHtml from '@ckeditor/ckeditor5-utils/tests/_utils/normalizehtml'
 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 ImageCaptionEditing from '@ckeditor/ckeditor5-image/src/imagecaption/imagecaptionediting';
 import ImageCaptionEditing from '@ckeditor/ckeditor5-image/src/imagecaption/imagecaptionediting';
 
 
+import linkIcon from '../theme/icons/link.svg';
+
+// We can import the SVG code directly to avoid re-edit, but we have to make sure that the icon code has `</path>` closing tag.
+// After cleaning up the icons, the closing tag can be re-parsed and some tests will fail.
+const linkIconInditatorElement = '<span class="ck ck-link-image_icon">' + linkIcon + '</span>';
+
 describe( 'LinkImageEditing', () => {
 describe( 'LinkImageEditing', () => {
 	let editor, model, view;
 	let editor, model, view;
 
 
@@ -48,7 +54,9 @@ describe( 'LinkImageEditing', () => {
 				setModelData( model, '<image src="/assets/sample.png" alt="alt text" linkHref="http://ckeditor.com"></image>' );
 				setModelData( model, '<image src="/assets/sample.png" alt="alt text" linkHref="http://ckeditor.com"></image>' );
 
 
 				expect( editor.getData() ).to.equal(
 				expect( editor.getData() ).to.equal(
-					'<figure class="image"><a href="http://ckeditor.com"><img alt="alt text" src="/assets/sample.png"></a></figure>'
+					'<figure class="image"><a href="http://ckeditor.com"><img alt="alt text" src="/assets/sample.png">' +
+					linkIconInditatorElement +
+					'</a></figure>'
 				);
 				);
 			} );
 			} );
 
 
@@ -56,7 +64,9 @@ describe( 'LinkImageEditing', () => {
 				setModelData( model, '<image src="/assets/sample.png" linkHref="http://ckeditor.com"></image>' );
 				setModelData( model, '<image src="/assets/sample.png" linkHref="http://ckeditor.com"></image>' );
 
 
 				expect( editor.getData() ).to.equal(
 				expect( editor.getData() ).to.equal(
-					'<figure class="image"><a href="http://ckeditor.com"><img src="/assets/sample.png"></a></figure>'
+					'<figure class="image"><a href="http://ckeditor.com"><img src="/assets/sample.png">' +
+					linkIconInditatorElement +
+					'</a></figure>'
 				);
 				);
 			} );
 			} );
 
 
@@ -72,6 +82,7 @@ describe( 'LinkImageEditing', () => {
 					'<figure class="image">' +
 					'<figure class="image">' +
 						'<a href="http://ckeditor.com">' +
 						'<a href="http://ckeditor.com">' +
 							'<img sizes="100vw" src="/assets/sample.png" srcset="small.png 148w, big.png 1024w"></img>' +
 							'<img sizes="100vw" src="/assets/sample.png" srcset="small.png 148w, big.png 1024w"></img>' +
+							linkIconInditatorElement +
 						'</a>' +
 						'</a>' +
 					'</figure>'
 					'</figure>'
 				);
 				);
@@ -264,6 +275,8 @@ describe( 'LinkImageEditing', () => {
 					'<figure class="ck-widget image" contenteditable="false">' +
 					'<figure class="ck-widget image" contenteditable="false">' +
 						'<a href="http://ckeditor.com">' +
 						'<a href="http://ckeditor.com">' +
 							'<img alt="alt text" src="/assets/sample.png"></img>' +
 							'<img alt="alt text" src="/assets/sample.png"></img>' +
+							// Content of the UIElement is skipped here.
+							'<span class="ck ck-link-image_icon"></span>' +
 						'</a>' +
 						'</a>' +
 					'</figure>'
 					'</figure>'
 				);
 				);
@@ -281,6 +294,8 @@ describe( 'LinkImageEditing', () => {
 					'<figure class="ck-widget image" contenteditable="false">' +
 					'<figure class="ck-widget image" contenteditable="false">' +
 						'<a href="https://ckeditor.com/why-ckeditor/">' +
 						'<a href="https://ckeditor.com/why-ckeditor/">' +
 							'<img alt="alt text" src="/assets/sample.png"></img>' +
 							'<img alt="alt text" src="/assets/sample.png"></img>' +
+							// Content of the UIElement is skipped here.
+							'<span class="ck ck-link-image_icon"></span>' +
 						'</a>' +
 						'</a>' +
 					'</figure>'
 					'</figure>'
 				);
 				);
@@ -302,7 +317,7 @@ describe( 'LinkImageEditing', () => {
 			} );
 			} );
 		} );
 		} );
 
 
-		describe( 'figure > a > img + figcaption', () => {
+		describe( 'figure > a > img + span + figcaption', () => {
 			it( 'should convert a link and the caption element', () => {
 			it( 'should convert a link and the caption element', () => {
 				return VirtualTestEditor
 				return VirtualTestEditor
 					.create( {
 					.create( {
@@ -319,6 +334,8 @@ describe( 'LinkImageEditing', () => {
 							'<figure class="ck-widget image" contenteditable="false">' +
 							'<figure class="ck-widget image" contenteditable="false">' +
 								'<a href="http://ckeditor.com">' +
 								'<a href="http://ckeditor.com">' +
 									'<img alt="alt text" src="/assets/sample.png"></img>' +
 									'<img alt="alt text" src="/assets/sample.png"></img>' +
+									// Content of the UIElement is skipped here.
+									'<span class="ck ck-link-image_icon"></span>' +
 								'</a>' +
 								'</a>' +
 								'<figcaption class="ck-editor__editable ck-editor__nested-editable" ' +
 								'<figcaption class="ck-editor__editable ck-editor__nested-editable" ' +
 									'contenteditable="true" data-placeholder="Enter image caption">' +
 									'contenteditable="true" data-placeholder="Enter image caption">' +

+ 8 - 0
packages/ckeditor5-link/tests/linkui.js

@@ -481,6 +481,9 @@ describe( 'LinkUI', () => {
 			const markerRange = editor.model.markers.get( 'link-ui' ).getRange();
 			const markerRange = editor.model.markers.get( 'link-ui' ).getRange();
 
 
 			expect( markerRange.isEqual( expectedRange ) ).to.be.true;
 			expect( markerRange.isEqual( expectedRange ) ).to.be.true;
+
+			expect( getViewData( editor.editing.view ) ).to.equal( '<p>f{<span class="ck-fake-link-selection">o</span>}o</p>' );
+			expect( editor.getData() ).to.equal( '<p>foo</p>' );
 		} );
 		} );
 
 
 		it( 'should display a fake visual selection on a collapsed selection', () => {
 		it( 'should display a fake visual selection on a collapsed selection', () => {
@@ -498,6 +501,11 @@ describe( 'LinkUI', () => {
 			const markerRange = editor.model.markers.get( 'link-ui' ).getRange();
 			const markerRange = editor.model.markers.get( 'link-ui' ).getRange();
 
 
 			expect( markerRange.isEqual( expectedRange ) ).to.be.true;
 			expect( markerRange.isEqual( expectedRange ) ).to.be.true;
+
+			expect( getViewData( editor.editing.view ) ).to.equal(
+				'<p>f{}<span class="ck-fake-link-selection ck-fake-link-selection_collapsed"></span>o</p>'
+			);
+			expect( editor.getData() ).to.equal( '<p>fo</p>' );
 		} );
 		} );
 	} );
 	} );
 
 

+ 3 - 1
packages/ckeditor5-link/tests/manual/linkimage.md

@@ -1,3 +1,5 @@
 # Linking Image
 # Linking Image
 
 
-Both images should have defined the `linkHref` attribute.
+Only the first image should have:
+- defined the `linkHref` attribute.
+- the linked image icon indicator in the top-right corner (the element should be placed inside the `<a>` element).

+ 1 - 1
packages/ckeditor5-link/theme/icons/link.svg

@@ -1 +1 @@
-<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M11.077 15l.991-1.416a.75.75 0 1 1 1.229.86l-1.148 1.64a.748.748 0 0 1-.217.206 5.251 5.251 0 0 1-8.503-5.955.741.741 0 0 1 .12-.274l1.147-1.639a.75.75 0 1 1 1.228.86L4.933 10.7l.006.003a3.75 3.75 0 0 0 6.132 4.294l.006.004zm5.494-5.335a.748.748 0 0 1-.12.274l-1.147 1.639a.75.75 0 1 1-1.228-.86l.86-1.23a3.75 3.75 0 0 0-6.144-4.301l-.86 1.229a.75.75 0 0 1-1.229-.86l1.148-1.64a.748.748 0 0 1 .217-.206 5.251 5.251 0 0 1 8.503 5.955zm-4.563-2.532a.75.75 0 0 1 .184 1.045l-3.155 4.505a.75.75 0 1 1-1.229-.86l3.155-4.506a.75.75 0 0 1 1.045-.184z"/></svg>
+<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M11.077 15l.991-1.416a.75.75 0 1 1 1.229.86l-1.148 1.64a.748.748 0 0 1-.217.206 5.251 5.251 0 0 1-8.503-5.955.741.741 0 0 1 .12-.274l1.147-1.639a.75.75 0 1 1 1.228.86L4.933 10.7l.006.003a3.75 3.75 0 0 0 6.132 4.294l.006.004zm5.494-5.335a.748.748 0 0 1-.12.274l-1.147 1.639a.75.75 0 1 1-1.228-.86l.86-1.23a3.75 3.75 0 0 0-6.144-4.301l-.86 1.229a.75.75 0 0 1-1.229-.86l1.148-1.64a.748.748 0 0 1 .217-.206 5.251 5.251 0 0 1 8.503 5.955zm-4.563-2.532a.75.75 0 0 1 .184 1.045l-3.155 4.505a.75.75 0 1 1-1.229-.86l3.155-4.506a.75.75 0 0 1 1.045-.184z"></path></svg>

+ 18 - 0
packages/ckeditor5-link/theme/linkimage.css

@@ -0,0 +1,18 @@
+/*
+ * Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+.ck.ck-link-image_icon {
+	position: absolute;
+	top: var(--ck-spacing-medium);
+	right: var(--ck-spacing-medium);
+	width: 18px;
+	height: 18px;
+	padding: 5px;
+	border-radius: var(--ck-border-radius);
+
+	& svg {
+		fill: currentColor;
+	}
+}

+ 24 - 11
packages/ckeditor5-page-break/src/pagebreakediting.js

@@ -10,7 +10,6 @@
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import PageBreakCommand from './pagebreakcommand';
 import PageBreakCommand from './pagebreakcommand';
 import { toWidget } from '@ckeditor/ckeditor5-widget/src/utils';
 import { toWidget } from '@ckeditor/ckeditor5-widget/src/utils';
-import first from '@ckeditor/ckeditor5-utils/src/first';
 
 
 import '../theme/pagebreak.css';
 import '../theme/pagebreak.css';
 
 
@@ -85,27 +84,41 @@ export default class PageBreakEditing extends Plugin {
 		conversion.for( 'upcast' )
 		conversion.for( 'upcast' )
 			.elementToElement( {
 			.elementToElement( {
 				view: element => {
 				view: element => {
-					// The "page break" div must have specified value for the 'page-break-after' definition and single child only.
-					if ( !element.is( 'div' ) || element.getStyle( 'page-break-after' ) != 'always' || element.childCount != 1 ) {
-						return;
-					}
+					// For upcast conversion it's enough if we check for element style and verify if it's empty
+					// or contains only hidden span element.
 
 
-					const viewSpan = first( element.getChildren() );
+					const hasPageBreakBefore = element.getStyle( 'page-break-before' ) == 'always';
+					const hasPageBreakAfter = element.getStyle( 'page-break-after' ) == 'always';
 
 
-					// The child must be the "span" element that is not displayed and has a space inside.
-					if ( !viewSpan.is( 'span' ) || viewSpan.getStyle( 'display' ) != 'none' || viewSpan.childCount != 1 ) {
+					if ( !hasPageBreakBefore && !hasPageBreakAfter ) {
 						return;
 						return;
 					}
 					}
 
 
-					const text = first( viewSpan.getChildren() );
+					// The "page break" div accepts only single child or no child at all.
+					if ( element.childCount == 1 ) {
+						const viewSpan = element.getChild( 0 );
+
+						// The child must be the "span" element that is not displayed and has a space inside.
+						if ( !viewSpan.is( 'span' ) || viewSpan.getStyle( 'display' ) != 'none' || viewSpan.childCount != 1 ) {
+							return;
+						}
 
 
-					if ( !text.is( 'text' ) || text.data !== ' ' ) {
+						const text = viewSpan.getChild( 0 );
+
+						if ( !text.is( 'text' ) || text.data !== ' ' ) {
+							return;
+						}
+					} else if ( element.childCount > 1 ) {
 						return;
 						return;
 					}
 					}
 
 
 					return { name: true };
 					return { name: true };
 				},
 				},
-				model: 'pageBreak'
+				model: 'pageBreak',
+
+				// This conversion must be checked before <br> conversion because some editors use
+				// <br style="page-break-before:always"> as a page break marker.
+				converterPriority: 'high'
 			} );
 			} );
 
 
 		editor.commands.add( 'pageBreak', new PageBreakCommand( editor ) );
 		editor.commands.add( 'pageBreak', new PageBreakCommand( editor ) );

+ 30 - 2
packages/ckeditor5-page-break/tests/pagebreakediting.js

@@ -113,11 +113,39 @@ describe( 'PageBreakEditing', () => {
 					.to.equal( '' );
 					.to.equal( '' );
 			} );
 			} );
 
 
-			it( 'should not convert if outer div has no children', () => {
+			it( 'should convert if outer div has no children', () => {
 				editor.setData( '<div class="page-break" style="page-break-after:always;"></div>' );
 				editor.setData( '<div class="page-break" style="page-break-after:always;"></div>' );
 
 
 				expect( getModelData( model, { withoutSelection: true } ) )
 				expect( getModelData( model, { withoutSelection: true } ) )
-					.to.equal( '' );
+					.to.equal( '<pageBreak></pageBreak>' );
+			} );
+
+			it( 'should convert if outer div has page-break-before style', () => {
+				editor.setData( '<div class="page-break" style="page-break-before:always;"></div>' );
+
+				expect( getModelData( model, { withoutSelection: true } ) )
+					.to.equal( '<pageBreak></pageBreak>' );
+			} );
+
+			it( 'should convert if outer div has page-break-after style', () => {
+				editor.setData( '<div class="page-break" style="page-break-after:always;"></div>' );
+
+				expect( getModelData( model, { withoutSelection: true } ) )
+					.to.equal( '<pageBreak></pageBreak>' );
+			} );
+
+			it( 'should convert if page-break-after style is on br element', () => {
+				editor.setData( '<br style="page-break-after:always;"/>' );
+
+				expect( getModelData( model, { withoutSelection: true } ) )
+					.to.equal( '<pageBreak></pageBreak>' );
+			} );
+
+			it( 'should convert if page-break-before style is on br element', () => {
+				editor.setData( '<br style="page-break-after:always;"/>' );
+
+				expect( getModelData( model, { withoutSelection: true } ) )
+					.to.equal( '<pageBreak></pageBreak>' );
 			} );
 			} );
 
 
 			it( 'should not convert if outer div has too many children', () => {
 			it( 'should not convert if outer div has too many children', () => {

+ 1 - 0
packages/ckeditor5-paste-from-office/package.json

@@ -26,6 +26,7 @@
     "@ckeditor/ckeditor5-link": "^20.0.0",
     "@ckeditor/ckeditor5-link": "^20.0.0",
     "@ckeditor/ckeditor5-list": "^20.0.0",
     "@ckeditor/ckeditor5-list": "^20.0.0",
     "@ckeditor/ckeditor5-paragraph": "^20.0.0",
     "@ckeditor/ckeditor5-paragraph": "^20.0.0",
+    "@ckeditor/ckeditor5-page-break": "^20.0.0",
     "@ckeditor/ckeditor5-table": "^20.0.0",
     "@ckeditor/ckeditor5-table": "^20.0.0",
     "@ckeditor/ckeditor5-utils": "^20.0.0"
     "@ckeditor/ckeditor5-utils": "^20.0.0"
   },
   },

+ 16 - 0
packages/ckeditor5-paste-from-office/tests/_data/page-break/index.js

@@ -0,0 +1,16 @@
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+import pageBreak from './page-break/input.html';
+import pageBreakModel from './page-break/model.html';
+
+export const fixtures = {
+	input: {
+		pageBreak
+	},
+	model: {
+		pageBreak: pageBreakModel
+	}
+};

+ 738 - 0
packages/ckeditor5-paste-from-office/tests/_data/page-break/page-break/input.html

@@ -0,0 +1,738 @@
+<html xmlns:o="urn:schemas-microsoft-com:office:office"
+	  xmlns:w="urn:schemas-microsoft-com:office:word"
+	  xmlns:m="http://schemas.microsoft.com/office/2004/12/omml"
+	  xmlns="http://www.w3.org/TR/REC-html40">
+
+<head>
+	<meta http-equiv=Content-Type content="text/html; charset=utf-8">
+	<meta name=ProgId content=Word.Document>
+	<meta name=Generator content="Microsoft Word 15">
+	<meta name=Originator content="Microsoft Word 15">
+	<link rel=File-List
+		  href="file:////Users/kuba/Library/Group%20Containers/UBF8T346G9.Office/TemporaryItems/msohtmlclip/clip_filelist.xml">
+	<!--[if gte mso 9]><xml>
+	<o:OfficeDocumentSettings>
+		<o:AllowPNG/>
+	</o:OfficeDocumentSettings>
+</xml><![endif]-->
+	<link rel=themeData
+		  href="file:////Users/kuba/Library/Group%20Containers/UBF8T346G9.Office/TemporaryItems/msohtmlclip/clip_themedata.thmx">
+	<link rel=colorSchemeMapping
+		  href="file:////Users/kuba/Library/Group%20Containers/UBF8T346G9.Office/TemporaryItems/msohtmlclip/clip_colorschememapping.xml">
+	<!--[if gte mso 9]><xml>
+	<w:WordDocument>
+	<w:View>Normal</w:View>
+	<w:Zoom>0</w:Zoom>
+	<w:TrackMoves/>
+	<w:TrackFormatting/>
+	<w:HyphenationZone>21</w:HyphenationZone>
+	<w:PunctuationKerning/>
+	<w:ValidateAgainstSchemas/>
+	<w:SaveIfXMLInvalid>false</w:SaveIfXMLInvalid>
+	<w:IgnoreMixedContent>false</w:IgnoreMixedContent>
+	<w:AlwaysShowPlaceholderText>false</w:AlwaysShowPlaceholderText>
+	<w:DoNotPromoteQF/>
+	<w:LidThemeOther>PL</w:LidThemeOther>
+	<w:LidThemeAsian>X-NONE</w:LidThemeAsian>
+	<w:LidThemeComplexScript>X-NONE</w:LidThemeComplexScript>
+	<w:Compatibility>
+	<w:BreakWrappedTables/>
+	<w:SnapToGridInCell/>
+	<w:WrapTextWithPunct/>
+	<w:UseAsianBreakRules/>
+	<w:DontGrowAutofit/>
+	<w:SplitPgBreakAndParaMark/>
+	<w:EnableOpenTypeKerning/>
+	<w:DontFlipMirrorIndents/>
+	<w:OverrideTableStyleHps/>
+	</w:Compatibility>
+	<m:mathPr>
+	<m:mathFont m:val="Cambria Math"/>
+	<m:brkBin m:val="before"/>
+	<m:brkBinSub m:val="&#45;-"/>
+	<m:smallFrac m:val="off"/>
+	<m:dispDef/>
+	<m:lMargin m:val="0"/>
+	<m:rMargin m:val="0"/>
+	<m:defJc m:val="centerGroup"/>
+	<m:wrapIndent m:val="1440"/>
+	<m:intLim m:val="subSup"/>
+	<m:naryLim m:val="undOvr"/>
+	</m:mathPr></w:WordDocument>
+	</xml><![endif]--><!--[if gte mso 9]><xml>
+	<w:LatentStyles DefLockedState="false" DefUnhideWhenUsed="false"
+					DefSemiHidden="false" DefQFormat="false" DefPriority="99"
+					LatentStyleCount="376">
+		<w:LsdException Locked="false" Priority="0" QFormat="true" Name="Normal"/>
+		<w:LsdException Locked="false" Priority="9" QFormat="true" Name="heading 1"/>
+		<w:LsdException Locked="false" Priority="9" SemiHidden="true"
+						UnhideWhenUsed="true" QFormat="true" Name="heading 2"/>
+		<w:LsdException Locked="false" Priority="9" SemiHidden="true"
+						UnhideWhenUsed="true" QFormat="true" Name="heading 3"/>
+		<w:LsdException Locked="false" Priority="9" SemiHidden="true"
+						UnhideWhenUsed="true" QFormat="true" Name="heading 4"/>
+		<w:LsdException Locked="false" Priority="9" SemiHidden="true"
+						UnhideWhenUsed="true" QFormat="true" Name="heading 5"/>
+		<w:LsdException Locked="false" Priority="9" SemiHidden="true"
+						UnhideWhenUsed="true" QFormat="true" Name="heading 6"/>
+		<w:LsdException Locked="false" Priority="9" SemiHidden="true"
+						UnhideWhenUsed="true" QFormat="true" Name="heading 7"/>
+		<w:LsdException Locked="false" Priority="9" SemiHidden="true"
+						UnhideWhenUsed="true" QFormat="true" Name="heading 8"/>
+		<w:LsdException Locked="false" Priority="9" SemiHidden="true"
+						UnhideWhenUsed="true" QFormat="true" Name="heading 9"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="index 1"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="index 2"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="index 3"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="index 4"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="index 5"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="index 6"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="index 7"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="index 8"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="index 9"/>
+		<w:LsdException Locked="false" Priority="39" SemiHidden="true"
+						UnhideWhenUsed="true" Name="toc 1"/>
+		<w:LsdException Locked="false" Priority="39" SemiHidden="true"
+						UnhideWhenUsed="true" Name="toc 2"/>
+		<w:LsdException Locked="false" Priority="39" SemiHidden="true"
+						UnhideWhenUsed="true" Name="toc 3"/>
+		<w:LsdException Locked="false" Priority="39" SemiHidden="true"
+						UnhideWhenUsed="true" Name="toc 4"/>
+		<w:LsdException Locked="false" Priority="39" SemiHidden="true"
+						UnhideWhenUsed="true" Name="toc 5"/>
+		<w:LsdException Locked="false" Priority="39" SemiHidden="true"
+						UnhideWhenUsed="true" Name="toc 6"/>
+		<w:LsdException Locked="false" Priority="39" SemiHidden="true"
+						UnhideWhenUsed="true" Name="toc 7"/>
+		<w:LsdException Locked="false" Priority="39" SemiHidden="true"
+						UnhideWhenUsed="true" Name="toc 8"/>
+		<w:LsdException Locked="false" Priority="39" SemiHidden="true"
+						UnhideWhenUsed="true" Name="toc 9"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Normal Indent"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="footnote text"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="annotation text"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="header"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="footer"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="index heading"/>
+		<w:LsdException Locked="false" Priority="35" SemiHidden="true"
+						UnhideWhenUsed="true" QFormat="true" Name="caption"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="table of figures"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="envelope address"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="envelope return"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="footnote reference"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="annotation reference"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="line number"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="page number"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="endnote reference"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="endnote text"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="table of authorities"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="macro"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="toa heading"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="List"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="List Bullet"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="List Number"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="List 2"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="List 3"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="List 4"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="List 5"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="List Bullet 2"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="List Bullet 3"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="List Bullet 4"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="List Bullet 5"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="List Number 2"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="List Number 3"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="List Number 4"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="List Number 5"/>
+		<w:LsdException Locked="false" Priority="10" QFormat="true" Name="Title"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Closing"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Signature"/>
+		<w:LsdException Locked="false" Priority="1" SemiHidden="true"
+						UnhideWhenUsed="true" Name="Default Paragraph Font"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Body Text"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Body Text Indent"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="List Continue"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="List Continue 2"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="List Continue 3"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="List Continue 4"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="List Continue 5"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Message Header"/>
+		<w:LsdException Locked="false" Priority="11" QFormat="true" Name="Subtitle"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Salutation"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Date"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Body Text First Indent"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Body Text First Indent 2"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Note Heading"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Body Text 2"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Body Text 3"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Body Text Indent 2"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Body Text Indent 3"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Block Text"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Hyperlink"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="FollowedHyperlink"/>
+		<w:LsdException Locked="false" Priority="22" QFormat="true" Name="Strong"/>
+		<w:LsdException Locked="false" Priority="20" QFormat="true" Name="Emphasis"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Document Map"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Plain Text"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="E-mail Signature"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="HTML Top of Form"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="HTML Bottom of Form"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Normal (Web)"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="HTML Acronym"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="HTML Address"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="HTML Cite"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="HTML Code"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="HTML Definition"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="HTML Keyboard"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="HTML Preformatted"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="HTML Sample"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="HTML Typewriter"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="HTML Variable"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Normal Table"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="annotation subject"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="No List"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Outline List 1"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Outline List 2"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Outline List 3"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Table Simple 1"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Table Simple 2"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Table Simple 3"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Table Classic 1"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Table Classic 2"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Table Classic 3"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Table Classic 4"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Table Colorful 1"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Table Colorful 2"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Table Colorful 3"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Table Columns 1"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Table Columns 2"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Table Columns 3"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Table Columns 4"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Table Columns 5"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Table Grid 1"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Table Grid 2"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Table Grid 3"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Table Grid 4"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Table Grid 5"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Table Grid 6"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Table Grid 7"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Table Grid 8"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Table List 1"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Table List 2"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Table List 3"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Table List 4"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Table List 5"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Table List 6"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Table List 7"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Table List 8"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Table 3D effects 1"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Table 3D effects 2"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Table 3D effects 3"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Table Contemporary"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Table Elegant"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Table Professional"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Table Subtle 1"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Table Subtle 2"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Table Web 1"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Table Web 2"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Table Web 3"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Balloon Text"/>
+		<w:LsdException Locked="false" Priority="39" Name="Table Grid"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Table Theme"/>
+		<w:LsdException Locked="false" SemiHidden="true" Name="Placeholder Text"/>
+		<w:LsdException Locked="false" Priority="1" QFormat="true" Name="No Spacing"/>
+		<w:LsdException Locked="false" Priority="60" Name="Light Shading"/>
+		<w:LsdException Locked="false" Priority="61" Name="Light List"/>
+		<w:LsdException Locked="false" Priority="62" Name="Light Grid"/>
+		<w:LsdException Locked="false" Priority="63" Name="Medium Shading 1"/>
+		<w:LsdException Locked="false" Priority="64" Name="Medium Shading 2"/>
+		<w:LsdException Locked="false" Priority="65" Name="Medium List 1"/>
+		<w:LsdException Locked="false" Priority="66" Name="Medium List 2"/>
+		<w:LsdException Locked="false" Priority="67" Name="Medium Grid 1"/>
+		<w:LsdException Locked="false" Priority="68" Name="Medium Grid 2"/>
+		<w:LsdException Locked="false" Priority="69" Name="Medium Grid 3"/>
+		<w:LsdException Locked="false" Priority="70" Name="Dark List"/>
+		<w:LsdException Locked="false" Priority="71" Name="Colorful Shading"/>
+		<w:LsdException Locked="false" Priority="72" Name="Colorful List"/>
+		<w:LsdException Locked="false" Priority="73" Name="Colorful Grid"/>
+		<w:LsdException Locked="false" Priority="60" Name="Light Shading Accent 1"/>
+		<w:LsdException Locked="false" Priority="61" Name="Light List Accent 1"/>
+		<w:LsdException Locked="false" Priority="62" Name="Light Grid Accent 1"/>
+		<w:LsdException Locked="false" Priority="63" Name="Medium Shading 1 Accent 1"/>
+		<w:LsdException Locked="false" Priority="64" Name="Medium Shading 2 Accent 1"/>
+		<w:LsdException Locked="false" Priority="65" Name="Medium List 1 Accent 1"/>
+		<w:LsdException Locked="false" SemiHidden="true" Name="Revision"/>
+		<w:LsdException Locked="false" Priority="34" QFormat="true"
+						Name="List Paragraph"/>
+		<w:LsdException Locked="false" Priority="29" QFormat="true" Name="Quote"/>
+		<w:LsdException Locked="false" Priority="30" QFormat="true"
+						Name="Intense Quote"/>
+		<w:LsdException Locked="false" Priority="66" Name="Medium List 2 Accent 1"/>
+		<w:LsdException Locked="false" Priority="67" Name="Medium Grid 1 Accent 1"/>
+		<w:LsdException Locked="false" Priority="68" Name="Medium Grid 2 Accent 1"/>
+		<w:LsdException Locked="false" Priority="69" Name="Medium Grid 3 Accent 1"/>
+		<w:LsdException Locked="false" Priority="70" Name="Dark List Accent 1"/>
+		<w:LsdException Locked="false" Priority="71" Name="Colorful Shading Accent 1"/>
+		<w:LsdException Locked="false" Priority="72" Name="Colorful List Accent 1"/>
+		<w:LsdException Locked="false" Priority="73" Name="Colorful Grid Accent 1"/>
+		<w:LsdException Locked="false" Priority="60" Name="Light Shading Accent 2"/>
+		<w:LsdException Locked="false" Priority="61" Name="Light List Accent 2"/>
+		<w:LsdException Locked="false" Priority="62" Name="Light Grid Accent 2"/>
+		<w:LsdException Locked="false" Priority="63" Name="Medium Shading 1 Accent 2"/>
+		<w:LsdException Locked="false" Priority="64" Name="Medium Shading 2 Accent 2"/>
+		<w:LsdException Locked="false" Priority="65" Name="Medium List 1 Accent 2"/>
+		<w:LsdException Locked="false" Priority="66" Name="Medium List 2 Accent 2"/>
+		<w:LsdException Locked="false" Priority="67" Name="Medium Grid 1 Accent 2"/>
+		<w:LsdException Locked="false" Priority="68" Name="Medium Grid 2 Accent 2"/>
+		<w:LsdException Locked="false" Priority="69" Name="Medium Grid 3 Accent 2"/>
+		<w:LsdException Locked="false" Priority="70" Name="Dark List Accent 2"/>
+		<w:LsdException Locked="false" Priority="71" Name="Colorful Shading Accent 2"/>
+		<w:LsdException Locked="false" Priority="72" Name="Colorful List Accent 2"/>
+		<w:LsdException Locked="false" Priority="73" Name="Colorful Grid Accent 2"/>
+		<w:LsdException Locked="false" Priority="60" Name="Light Shading Accent 3"/>
+		<w:LsdException Locked="false" Priority="61" Name="Light List Accent 3"/>
+		<w:LsdException Locked="false" Priority="62" Name="Light Grid Accent 3"/>
+		<w:LsdException Locked="false" Priority="63" Name="Medium Shading 1 Accent 3"/>
+		<w:LsdException Locked="false" Priority="64" Name="Medium Shading 2 Accent 3"/>
+		<w:LsdException Locked="false" Priority="65" Name="Medium List 1 Accent 3"/>
+		<w:LsdException Locked="false" Priority="66" Name="Medium List 2 Accent 3"/>
+		<w:LsdException Locked="false" Priority="67" Name="Medium Grid 1 Accent 3"/>
+		<w:LsdException Locked="false" Priority="68" Name="Medium Grid 2 Accent 3"/>
+		<w:LsdException Locked="false" Priority="69" Name="Medium Grid 3 Accent 3"/>
+		<w:LsdException Locked="false" Priority="70" Name="Dark List Accent 3"/>
+		<w:LsdException Locked="false" Priority="71" Name="Colorful Shading Accent 3"/>
+		<w:LsdException Locked="false" Priority="72" Name="Colorful List Accent 3"/>
+		<w:LsdException Locked="false" Priority="73" Name="Colorful Grid Accent 3"/>
+		<w:LsdException Locked="false" Priority="60" Name="Light Shading Accent 4"/>
+		<w:LsdException Locked="false" Priority="61" Name="Light List Accent 4"/>
+		<w:LsdException Locked="false" Priority="62" Name="Light Grid Accent 4"/>
+		<w:LsdException Locked="false" Priority="63" Name="Medium Shading 1 Accent 4"/>
+		<w:LsdException Locked="false" Priority="64" Name="Medium Shading 2 Accent 4"/>
+		<w:LsdException Locked="false" Priority="65" Name="Medium List 1 Accent 4"/>
+		<w:LsdException Locked="false" Priority="66" Name="Medium List 2 Accent 4"/>
+		<w:LsdException Locked="false" Priority="67" Name="Medium Grid 1 Accent 4"/>
+		<w:LsdException Locked="false" Priority="68" Name="Medium Grid 2 Accent 4"/>
+		<w:LsdException Locked="false" Priority="69" Name="Medium Grid 3 Accent 4"/>
+		<w:LsdException Locked="false" Priority="70" Name="Dark List Accent 4"/>
+		<w:LsdException Locked="false" Priority="71" Name="Colorful Shading Accent 4"/>
+		<w:LsdException Locked="false" Priority="72" Name="Colorful List Accent 4"/>
+		<w:LsdException Locked="false" Priority="73" Name="Colorful Grid Accent 4"/>
+		<w:LsdException Locked="false" Priority="60" Name="Light Shading Accent 5"/>
+		<w:LsdException Locked="false" Priority="61" Name="Light List Accent 5"/>
+		<w:LsdException Locked="false" Priority="62" Name="Light Grid Accent 5"/>
+		<w:LsdException Locked="false" Priority="63" Name="Medium Shading 1 Accent 5"/>
+		<w:LsdException Locked="false" Priority="64" Name="Medium Shading 2 Accent 5"/>
+		<w:LsdException Locked="false" Priority="65" Name="Medium List 1 Accent 5"/>
+		<w:LsdException Locked="false" Priority="66" Name="Medium List 2 Accent 5"/>
+		<w:LsdException Locked="false" Priority="67" Name="Medium Grid 1 Accent 5"/>
+		<w:LsdException Locked="false" Priority="68" Name="Medium Grid 2 Accent 5"/>
+		<w:LsdException Locked="false" Priority="69" Name="Medium Grid 3 Accent 5"/>
+		<w:LsdException Locked="false" Priority="70" Name="Dark List Accent 5"/>
+		<w:LsdException Locked="false" Priority="71" Name="Colorful Shading Accent 5"/>
+		<w:LsdException Locked="false" Priority="72" Name="Colorful List Accent 5"/>
+		<w:LsdException Locked="false" Priority="73" Name="Colorful Grid Accent 5"/>
+		<w:LsdException Locked="false" Priority="60" Name="Light Shading Accent 6"/>
+		<w:LsdException Locked="false" Priority="61" Name="Light List Accent 6"/>
+		<w:LsdException Locked="false" Priority="62" Name="Light Grid Accent 6"/>
+		<w:LsdException Locked="false" Priority="63" Name="Medium Shading 1 Accent 6"/>
+		<w:LsdException Locked="false" Priority="64" Name="Medium Shading 2 Accent 6"/>
+		<w:LsdException Locked="false" Priority="65" Name="Medium List 1 Accent 6"/>
+		<w:LsdException Locked="false" Priority="66" Name="Medium List 2 Accent 6"/>
+		<w:LsdException Locked="false" Priority="67" Name="Medium Grid 1 Accent 6"/>
+		<w:LsdException Locked="false" Priority="68" Name="Medium Grid 2 Accent 6"/>
+		<w:LsdException Locked="false" Priority="69" Name="Medium Grid 3 Accent 6"/>
+		<w:LsdException Locked="false" Priority="70" Name="Dark List Accent 6"/>
+		<w:LsdException Locked="false" Priority="71" Name="Colorful Shading Accent 6"/>
+		<w:LsdException Locked="false" Priority="72" Name="Colorful List Accent 6"/>
+		<w:LsdException Locked="false" Priority="73" Name="Colorful Grid Accent 6"/>
+		<w:LsdException Locked="false" Priority="19" QFormat="true"
+						Name="Subtle Emphasis"/>
+		<w:LsdException Locked="false" Priority="21" QFormat="true"
+						Name="Intense Emphasis"/>
+		<w:LsdException Locked="false" Priority="31" QFormat="true"
+						Name="Subtle Reference"/>
+		<w:LsdException Locked="false" Priority="32" QFormat="true"
+						Name="Intense Reference"/>
+		<w:LsdException Locked="false" Priority="33" QFormat="true" Name="Book Title"/>
+		<w:LsdException Locked="false" Priority="37" SemiHidden="true"
+						UnhideWhenUsed="true" Name="Bibliography"/>
+		<w:LsdException Locked="false" Priority="39" SemiHidden="true"
+						UnhideWhenUsed="true" QFormat="true" Name="TOC Heading"/>
+		<w:LsdException Locked="false" Priority="41" Name="Plain Table 1"/>
+		<w:LsdException Locked="false" Priority="42" Name="Plain Table 2"/>
+		<w:LsdException Locked="false" Priority="43" Name="Plain Table 3"/>
+		<w:LsdException Locked="false" Priority="44" Name="Plain Table 4"/>
+		<w:LsdException Locked="false" Priority="45" Name="Plain Table 5"/>
+		<w:LsdException Locked="false" Priority="40" Name="Grid Table Light"/>
+		<w:LsdException Locked="false" Priority="46" Name="Grid Table 1 Light"/>
+		<w:LsdException Locked="false" Priority="47" Name="Grid Table 2"/>
+		<w:LsdException Locked="false" Priority="48" Name="Grid Table 3"/>
+		<w:LsdException Locked="false" Priority="49" Name="Grid Table 4"/>
+		<w:LsdException Locked="false" Priority="50" Name="Grid Table 5 Dark"/>
+		<w:LsdException Locked="false" Priority="51" Name="Grid Table 6 Colorful"/>
+		<w:LsdException Locked="false" Priority="52" Name="Grid Table 7 Colorful"/>
+		<w:LsdException Locked="false" Priority="46"
+						Name="Grid Table 1 Light Accent 1"/>
+		<w:LsdException Locked="false" Priority="47" Name="Grid Table 2 Accent 1"/>
+		<w:LsdException Locked="false" Priority="48" Name="Grid Table 3 Accent 1"/>
+		<w:LsdException Locked="false" Priority="49" Name="Grid Table 4 Accent 1"/>
+		<w:LsdException Locked="false" Priority="50" Name="Grid Table 5 Dark Accent 1"/>
+		<w:LsdException Locked="false" Priority="51"
+						Name="Grid Table 6 Colorful Accent 1"/>
+		<w:LsdException Locked="false" Priority="52"
+						Name="Grid Table 7 Colorful Accent 1"/>
+		<w:LsdException Locked="false" Priority="46"
+						Name="Grid Table 1 Light Accent 2"/>
+		<w:LsdException Locked="false" Priority="47" Name="Grid Table 2 Accent 2"/>
+		<w:LsdException Locked="false" Priority="48" Name="Grid Table 3 Accent 2"/>
+		<w:LsdException Locked="false" Priority="49" Name="Grid Table 4 Accent 2"/>
+		<w:LsdException Locked="false" Priority="50" Name="Grid Table 5 Dark Accent 2"/>
+		<w:LsdException Locked="false" Priority="51"
+						Name="Grid Table 6 Colorful Accent 2"/>
+		<w:LsdException Locked="false" Priority="52"
+						Name="Grid Table 7 Colorful Accent 2"/>
+		<w:LsdException Locked="false" Priority="46"
+						Name="Grid Table 1 Light Accent 3"/>
+		<w:LsdException Locked="false" Priority="47" Name="Grid Table 2 Accent 3"/>
+		<w:LsdException Locked="false" Priority="48" Name="Grid Table 3 Accent 3"/>
+		<w:LsdException Locked="false" Priority="49" Name="Grid Table 4 Accent 3"/>
+		<w:LsdException Locked="false" Priority="50" Name="Grid Table 5 Dark Accent 3"/>
+		<w:LsdException Locked="false" Priority="51"
+						Name="Grid Table 6 Colorful Accent 3"/>
+		<w:LsdException Locked="false" Priority="52"
+						Name="Grid Table 7 Colorful Accent 3"/>
+		<w:LsdException Locked="false" Priority="46"
+						Name="Grid Table 1 Light Accent 4"/>
+		<w:LsdException Locked="false" Priority="47" Name="Grid Table 2 Accent 4"/>
+		<w:LsdException Locked="false" Priority="48" Name="Grid Table 3 Accent 4"/>
+		<w:LsdException Locked="false" Priority="49" Name="Grid Table 4 Accent 4"/>
+		<w:LsdException Locked="false" Priority="50" Name="Grid Table 5 Dark Accent 4"/>
+		<w:LsdException Locked="false" Priority="51"
+						Name="Grid Table 6 Colorful Accent 4"/>
+		<w:LsdException Locked="false" Priority="52"
+						Name="Grid Table 7 Colorful Accent 4"/>
+		<w:LsdException Locked="false" Priority="46"
+						Name="Grid Table 1 Light Accent 5"/>
+		<w:LsdException Locked="false" Priority="47" Name="Grid Table 2 Accent 5"/>
+		<w:LsdException Locked="false" Priority="48" Name="Grid Table 3 Accent 5"/>
+		<w:LsdException Locked="false" Priority="49" Name="Grid Table 4 Accent 5"/>
+		<w:LsdException Locked="false" Priority="50" Name="Grid Table 5 Dark Accent 5"/>
+		<w:LsdException Locked="false" Priority="51"
+						Name="Grid Table 6 Colorful Accent 5"/>
+		<w:LsdException Locked="false" Priority="52"
+						Name="Grid Table 7 Colorful Accent 5"/>
+		<w:LsdException Locked="false" Priority="46"
+						Name="Grid Table 1 Light Accent 6"/>
+		<w:LsdException Locked="false" Priority="47" Name="Grid Table 2 Accent 6"/>
+		<w:LsdException Locked="false" Priority="48" Name="Grid Table 3 Accent 6"/>
+		<w:LsdException Locked="false" Priority="49" Name="Grid Table 4 Accent 6"/>
+		<w:LsdException Locked="false" Priority="50" Name="Grid Table 5 Dark Accent 6"/>
+		<w:LsdException Locked="false" Priority="51"
+						Name="Grid Table 6 Colorful Accent 6"/>
+		<w:LsdException Locked="false" Priority="52"
+						Name="Grid Table 7 Colorful Accent 6"/>
+		<w:LsdException Locked="false" Priority="46" Name="List Table 1 Light"/>
+		<w:LsdException Locked="false" Priority="47" Name="List Table 2"/>
+		<w:LsdException Locked="false" Priority="48" Name="List Table 3"/>
+		<w:LsdException Locked="false" Priority="49" Name="List Table 4"/>
+		<w:LsdException Locked="false" Priority="50" Name="List Table 5 Dark"/>
+		<w:LsdException Locked="false" Priority="51" Name="List Table 6 Colorful"/>
+		<w:LsdException Locked="false" Priority="52" Name="List Table 7 Colorful"/>
+		<w:LsdException Locked="false" Priority="46"
+						Name="List Table 1 Light Accent 1"/>
+		<w:LsdException Locked="false" Priority="47" Name="List Table 2 Accent 1"/>
+		<w:LsdException Locked="false" Priority="48" Name="List Table 3 Accent 1"/>
+		<w:LsdException Locked="false" Priority="49" Name="List Table 4 Accent 1"/>
+		<w:LsdException Locked="false" Priority="50" Name="List Table 5 Dark Accent 1"/>
+		<w:LsdException Locked="false" Priority="51"
+						Name="List Table 6 Colorful Accent 1"/>
+		<w:LsdException Locked="false" Priority="52"
+						Name="List Table 7 Colorful Accent 1"/>
+		<w:LsdException Locked="false" Priority="46"
+						Name="List Table 1 Light Accent 2"/>
+		<w:LsdException Locked="false" Priority="47" Name="List Table 2 Accent 2"/>
+		<w:LsdException Locked="false" Priority="48" Name="List Table 3 Accent 2"/>
+		<w:LsdException Locked="false" Priority="49" Name="List Table 4 Accent 2"/>
+		<w:LsdException Locked="false" Priority="50" Name="List Table 5 Dark Accent 2"/>
+		<w:LsdException Locked="false" Priority="51"
+						Name="List Table 6 Colorful Accent 2"/>
+		<w:LsdException Locked="false" Priority="52"
+						Name="List Table 7 Colorful Accent 2"/>
+		<w:LsdException Locked="false" Priority="46"
+						Name="List Table 1 Light Accent 3"/>
+		<w:LsdException Locked="false" Priority="47" Name="List Table 2 Accent 3"/>
+		<w:LsdException Locked="false" Priority="48" Name="List Table 3 Accent 3"/>
+		<w:LsdException Locked="false" Priority="49" Name="List Table 4 Accent 3"/>
+		<w:LsdException Locked="false" Priority="50" Name="List Table 5 Dark Accent 3"/>
+		<w:LsdException Locked="false" Priority="51"
+						Name="List Table 6 Colorful Accent 3"/>
+		<w:LsdException Locked="false" Priority="52"
+						Name="List Table 7 Colorful Accent 3"/>
+		<w:LsdException Locked="false" Priority="46"
+						Name="List Table 1 Light Accent 4"/>
+		<w:LsdException Locked="false" Priority="47" Name="List Table 2 Accent 4"/>
+		<w:LsdException Locked="false" Priority="48" Name="List Table 3 Accent 4"/>
+		<w:LsdException Locked="false" Priority="49" Name="List Table 4 Accent 4"/>
+		<w:LsdException Locked="false" Priority="50" Name="List Table 5 Dark Accent 4"/>
+		<w:LsdException Locked="false" Priority="51"
+						Name="List Table 6 Colorful Accent 4"/>
+		<w:LsdException Locked="false" Priority="52"
+						Name="List Table 7 Colorful Accent 4"/>
+		<w:LsdException Locked="false" Priority="46"
+						Name="List Table 1 Light Accent 5"/>
+		<w:LsdException Locked="false" Priority="47" Name="List Table 2 Accent 5"/>
+		<w:LsdException Locked="false" Priority="48" Name="List Table 3 Accent 5"/>
+		<w:LsdException Locked="false" Priority="49" Name="List Table 4 Accent 5"/>
+		<w:LsdException Locked="false" Priority="50" Name="List Table 5 Dark Accent 5"/>
+		<w:LsdException Locked="false" Priority="51"
+						Name="List Table 6 Colorful Accent 5"/>
+		<w:LsdException Locked="false" Priority="52"
+						Name="List Table 7 Colorful Accent 5"/>
+		<w:LsdException Locked="false" Priority="46"
+						Name="List Table 1 Light Accent 6"/>
+		<w:LsdException Locked="false" Priority="47" Name="List Table 2 Accent 6"/>
+		<w:LsdException Locked="false" Priority="48" Name="List Table 3 Accent 6"/>
+		<w:LsdException Locked="false" Priority="49" Name="List Table 4 Accent 6"/>
+		<w:LsdException Locked="false" Priority="50" Name="List Table 5 Dark Accent 6"/>
+		<w:LsdException Locked="false" Priority="51"
+						Name="List Table 6 Colorful Accent 6"/>
+		<w:LsdException Locked="false" Priority="52"
+						Name="List Table 7 Colorful Accent 6"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Mention"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Smart Hyperlink"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Hashtag"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Unresolved Mention"/>
+		<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"
+						Name="Smart Link"/>
+	</w:LatentStyles>
+</xml><![endif]-->
+	<style>
+		<!--
+		/* Font Definitions */
+		@font-face
+		{font-family:"Cambria Math";
+			panose-1:2 4 5 3 5 4 6 3 2 4;
+			mso-font-charset:0;
+			mso-generic-font-family:roman;
+			mso-font-pitch:variable;
+			mso-font-signature:3 0 0 0 1 0;}
+		@font-face
+		{font-family:Calibri;
+			panose-1:2 15 5 2 2 2 4 3 2 4;
+			mso-font-charset:0;
+			mso-generic-font-family:swiss;
+			mso-font-pitch:variable;
+			mso-font-signature:-536859905 -1073732485 9 0 511 0;}
+		/* Style Definitions */
+		p.MsoNormal, li.MsoNormal, div.MsoNormal
+		{mso-style-unhide:no;
+			mso-style-qformat:yes;
+			mso-style-parent:"";
+			margin:0cm;
+			mso-pagination:widow-orphan;
+			font-size:12.0pt;
+			font-family:"Calibri",sans-serif;
+			mso-ascii-font-family:Calibri;
+			mso-ascii-theme-font:minor-latin;
+			mso-fareast-font-family:Calibri;
+			mso-fareast-theme-font:minor-latin;
+			mso-hansi-font-family:Calibri;
+			mso-hansi-theme-font:minor-latin;
+			mso-bidi-font-family:"Times New Roman";
+			mso-bidi-theme-font:minor-bidi;
+			mso-fareast-language:EN-US;}
+		.MsoChpDefault
+		{mso-style-type:export-only;
+			mso-default-props:yes;
+			font-family:"Calibri",sans-serif;
+			mso-ascii-font-family:Calibri;
+			mso-ascii-theme-font:minor-latin;
+			mso-fareast-font-family:Calibri;
+			mso-fareast-theme-font:minor-latin;
+			mso-hansi-font-family:Calibri;
+			mso-hansi-theme-font:minor-latin;
+			mso-bidi-font-family:"Times New Roman";
+			mso-bidi-theme-font:minor-bidi;
+			mso-fareast-language:EN-US;}
+		@page WordSection1
+		{size:595.3pt 841.9pt;
+			margin:70.85pt 70.85pt 70.85pt 70.85pt;
+			mso-header-margin:35.4pt;
+			mso-footer-margin:35.4pt;
+			mso-paper-source:0;}
+		div.WordSection1
+		{page:WordSection1;}
+		-->
+	</style>
+	<!--[if gte mso 10]>
+	<style>
+		/* Style Definitions */
+		table.MsoNormalTable
+		{mso-style-name:Standardowy;
+			mso-tstyle-rowband-size:0;
+			mso-tstyle-colband-size:0;
+			mso-style-noshow:yes;
+			mso-style-priority:99;
+			mso-style-parent:"";
+			mso-padding-alt:0cm 5.4pt 0cm 5.4pt;
+			mso-para-margin:0cm;
+			mso-pagination:widow-orphan;
+			font-size:12.0pt;
+			font-family:"Calibri",sans-serif;
+			mso-ascii-font-family:Calibri;
+			mso-ascii-theme-font:minor-latin;
+			mso-hansi-font-family:Calibri;
+			mso-hansi-theme-font:minor-latin;
+			mso-bidi-font-family:"Times New Roman";
+			mso-bidi-theme-font:minor-bidi;
+			mso-fareast-language:EN-US;}
+	</style>
+	<![endif]-->
+</head>
+
+<body lang=PL style='tab-interval:35.4pt'>
+<!--StartFragment-->
+
+<p class=MsoNormal>foo<br clear=all style='mso-special-character:line-break;
+page-break-before:always'>
+	<o:p></o:p></p>
+
+<p class=MsoNormal>bar<o:p></o:p></p>
+
+<!--EndFragment-->
+</body>
+
+</html>

+ 3 - 0
packages/ckeditor5-paste-from-office/tests/_data/page-break/page-break/model.html

@@ -0,0 +1,3 @@
+<paragraph>foo</paragraph>
+<pageBreak></pageBreak>
+<paragraph>bar</paragraph>

BIN
packages/ckeditor5-paste-from-office/tests/_data/page-break/page-break/page-break.docx


+ 3 - 1
packages/ckeditor5-paste-from-office/tests/_utils/fixtures.js

@@ -13,6 +13,7 @@ import { fixtures as googleDocsBoldWrapper, browserFixtures as googleDocsBoldWra
 	from '../_data/paste-from-google-docs/bold-wrapper/index';
 	from '../_data/paste-from-google-docs/bold-wrapper/index';
 import { fixtures as googleDocsList, browserFixtures as googleDocsListBrowser } from '../_data/paste-from-google-docs/lists/index.js';
 import { fixtures as googleDocsList, browserFixtures as googleDocsListBrowser } from '../_data/paste-from-google-docs/lists/index.js';
 import { fixtures as table } from '../_data/table/index.js';
 import { fixtures as table } from '../_data/table/index.js';
+import { fixtures as pageBreak } from '../_data/page-break/index.js';
 
 
 // Generic fixtures.
 // Generic fixtures.
 export const fixtures = {
 export const fixtures = {
@@ -23,7 +24,8 @@ export const fixtures = {
 	spacing,
 	spacing,
 	'google-docs-bold-wrapper': googleDocsBoldWrapper,
 	'google-docs-bold-wrapper': googleDocsBoldWrapper,
 	'google-docs-list': googleDocsList,
 	'google-docs-list': googleDocsList,
-	table
+	table,
+	'page-break': pageBreak
 };
 };
 
 
 // Browser specific fixtures.
 // Browser specific fixtures.

+ 10 - 0
packages/ckeditor5-paste-from-office/tests/data/integration.js

@@ -22,6 +22,7 @@ import FontColor from '@ckeditor/ckeditor5-font/src/fontcolor';
 
 
 import PasteFromOffice from '../../src/pastefromoffice';
 import PasteFromOffice from '../../src/pastefromoffice';
 import { generateTests } from '../_utils/utils';
 import { generateTests } from '../_utils/utils';
+import PageBreak from '@ckeditor/ckeditor5-page-break/src/pagebreak';
 
 
 const browsers = [ 'chrome', 'firefox', 'safari', 'edge' ];
 const browsers = [ 'chrome', 'firefox', 'safari', 'edge' ];
 
 
@@ -122,4 +123,13 @@ describe( 'PasteFromOffice - integration', () => {
 				FontColor, FontBackgroundColor ]
 				FontColor, FontBackgroundColor ]
 		}
 		}
 	} );
 	} );
+
+	generateTests( {
+		input: 'page-break',
+		type: 'integration',
+		browsers,
+		editorConfig: {
+			plugins: [ Clipboard, Paragraph, Bold, PasteFromOffice, PageBreak ]
+		}
+	} );
 } );
 } );

+ 3 - 2
packages/ckeditor5-paste-from-office/tests/manual/integration.js

@@ -15,6 +15,7 @@ import TableToolbar from '@ckeditor/ckeditor5-table/src/tabletoolbar';
 import EasyImage from '@ckeditor/ckeditor5-easy-image/src/easyimage';
 import EasyImage from '@ckeditor/ckeditor5-easy-image/src/easyimage';
 import FontColor from '@ckeditor/ckeditor5-font/src/fontcolor';
 import FontColor from '@ckeditor/ckeditor5-font/src/fontcolor';
 import FontBackgroundColor from '@ckeditor/ckeditor5-font/src/fontbackgroundcolor';
 import FontBackgroundColor from '@ckeditor/ckeditor5-font/src/fontbackgroundcolor';
+import PageBreak from '@ckeditor/ckeditor5-page-break/src/pagebreak';
 import TableProperties from '@ckeditor/ckeditor5-table/src/tableproperties';
 import TableProperties from '@ckeditor/ckeditor5-table/src/tableproperties';
 import TableCellProperties from '@ckeditor/ckeditor5-table/src/tablecellproperties';
 import TableCellProperties from '@ckeditor/ckeditor5-table/src/tablecellproperties';
 
 
@@ -30,10 +31,10 @@ const dataDiv = document.querySelector( '#data' );
 
 
 ClassicEditor
 ClassicEditor
 	.create( document.querySelector( '#editor' ), {
 	.create( document.querySelector( '#editor' ), {
-		plugins: [ ArticlePluginSet, Strikethrough, Underline, Table, TableToolbar,
+		plugins: [ ArticlePluginSet, Strikethrough, Underline, Table, TableToolbar, PageBreak,
 			TableProperties, TableCellProperties, EasyImage, PasteFromOffice, FontColor, FontBackgroundColor ],
 			TableProperties, TableCellProperties, EasyImage, PasteFromOffice, FontColor, FontBackgroundColor ],
 		toolbar: [ 'heading', '|', 'bold', 'italic', 'strikethrough', 'underline', 'link',
 		toolbar: [ 'heading', '|', 'bold', 'italic', 'strikethrough', 'underline', 'link',
-			'bulletedList', 'numberedList', 'blockQuote', 'insertTable', 'undo', 'redo' ],
+			'bulletedList', 'numberedList', 'blockQuote', 'insertTable', 'pageBreak', 'undo', 'redo' ],
 		table: {
 		table: {
 			contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells', 'tableProperties', 'tableCellProperties' ]
 			contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells', 'tableProperties', 'tableCellProperties' ]
 		},
 		},

+ 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 ) ) {

+ 42 - 18
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,9 +162,20 @@ 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' );
+
+			const cellsToSelect = replaceSelectedCellsWithPasted( pastedTable, pastedDimensions, selectedTable, selection, writer );
 
 
-			replaceSelectedCellsWithPasted( pastedTable, pastedDimensions, selectedTable, selection, writer );
+			if ( this.editor.plugins.get( 'TableSelection' ).isEnabled ) {
+				// 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 );
+			} else {
+				// Set selection inside first cell if multi-cell selection is disabled.
+				writer.setSelection( cellsToSelect[ 0 ], 0 );
+			}
 		} );
 		} );
 	}
 	}
 }
 }
@@ -186,7 +194,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 +213,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
@@ -251,6 +259,7 @@ function prepareTableForPasting( selectedTableCells, pastedDimensions, writer, t
 // @param {Number} selection.lastColumn
 // @param {Number} selection.lastColumn
 // @param {Number} selection.lastRow
 // @param {Number} selection.lastRow
 // @param {module:engine/model/writer~Writer} writer
 // @param {module:engine/model/writer~Writer} writer
+// @returns {Array.<module:engine/model/element~Element>}
 function replaceSelectedCellsWithPasted( pastedTable, pastedDimensions, selectedTable, selection, writer ) {
 function replaceSelectedCellsWithPasted( pastedTable, pastedDimensions, selectedTable, selection, writer ) {
 	const { width: pastedWidth, height: pastedHeight } = pastedDimensions;
 	const { width: pastedWidth, height: pastedHeight } = pastedDimensions;
 
 
@@ -316,17 +325,37 @@ 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 );
+	}
+
+	return cellsToSelect;
 }
 }
 
 
 // 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 +363,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 +517,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 +531,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`.  

+ 368 - 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 );
@@ -899,6 +899,21 @@ describe( 'table clipboard', () => {
 						[ '', '', 'ba', 'bb' ]
 						[ '', '', 'ba', 'bb' ]
 					] ) );
 					] ) );
 				} );
 				} );
+
+				it( 'should not set multi-cell selection if TableSelection plugin is disabled', () => {
+					editor.plugins.get( 'TableSelection' ).forceDisabled();
+
+					pasteTable( [
+						[ 'aa', 'ab' ],
+						[ 'ba', 'bb' ]
+					] );
+
+					assertEqualMarkup( getModelData( model ), modelTable( [
+						[ '[]aa', 'ab', '02' ],
+						[ 'ba', 'bb', '12' ],
+						[ '20', '21', '22' ]
+					] ) );
+				} );
 			} );
 			} );
 
 
 			describe( 'with spanned cells', () => {
 			describe( 'with spanned cells', () => {
@@ -3385,6 +3400,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 +3891,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 +3930,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, {

+ 9 - 0
packages/ckeditor5-theme-lark/theme/ckeditor5-link/linkimage.css

@@ -0,0 +1,9 @@
+/*
+ * Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+.ck.ck-link-image_icon {
+	color: hsl(0, 0%, 100%);
+	background: hsla(0, 0%, 0%, .4);
+}

+ 3 - 3
packages/ckeditor5-typing/src/inputcommand.js

@@ -89,6 +89,9 @@ export default class InputCommand extends Command {
 		model.enqueueChange( this._buffer.batch, writer => {
 		model.enqueueChange( this._buffer.batch, writer => {
 			this._buffer.lock();
 			this._buffer.lock();
 
 
+			// Store the batch as an 'input' batch for the Input.isInput( batch ) check.
+			this._batches.add( this._buffer.batch );
+
 			model.deleteContent( selection );
 			model.deleteContent( selection );
 
 
 			if ( text ) {
 			if ( text ) {
@@ -104,9 +107,6 @@ export default class InputCommand extends Command {
 			this._buffer.unlock();
 			this._buffer.unlock();
 
 
 			this._buffer.input( textInsertions );
 			this._buffer.input( textInsertions );
-
-			// Store the batch as an 'input' batch for the Input.isInput( batch ) check.
-			this._batches.add( this._buffer.batch );
 		} );
 		} );
 	}
 	}
 }
 }

+ 4 - 1
packages/ckeditor5-typing/src/utils/injectunsafekeystrokeshandling.js

@@ -110,7 +110,10 @@ export default function injectUnsafeKeystrokesHandling( editor ) {
 
 
 		buffer.lock();
 		buffer.lock();
 
 
-		model.enqueueChange( buffer.batch, () => {
+		const batch = buffer.batch;
+		inputCommand._batches.add( batch );
+
+		model.enqueueChange( batch, () => {
 			model.deleteContent( model.document.selection );
 			model.deleteContent( model.document.selection );
 		} );
 		} );
 
 

+ 24 - 2
packages/ckeditor5-typing/tests/inputcommand.js

@@ -12,7 +12,7 @@ import ChangeBuffer from '../src/utils/changebuffer';
 import Input from '../src/input';
 import Input from '../src/input';
 
 
 describe( 'InputCommand', () => {
 describe( 'InputCommand', () => {
-	let editor, model, doc, buffer;
+	let editor, model, doc, buffer, inputCommand;
 
 
 	testUtils.createSinonSandbox();
 	testUtils.createSinonSandbox();
 
 
@@ -23,7 +23,7 @@ describe( 'InputCommand', () => {
 				model = editor.model;
 				model = editor.model;
 				doc = model.document;
 				doc = model.document;
 
 
-				const inputCommand = new InputCommand( editor, 20 );
+				inputCommand = new InputCommand( editor, 20 );
 				editor.commands.add( 'input', inputCommand );
 				editor.commands.add( 'input', inputCommand );
 
 
 				buffer = inputCommand.buffer;
 				buffer = inputCommand.buffer;
@@ -281,6 +281,28 @@ describe( 'InputCommand', () => {
 				'<paragraph>z</paragraph>'
 				'<paragraph>z</paragraph>'
 			);
 			);
 		} );
 		} );
+
+		it( 'uses typing batch while removing and inserting the content', () => {
+			expect( inputCommand._batches.has( getCurrentBatch() ), 'batch before typing' ).to.equal( false );
+
+			model.on( 'deleteContent', () => {
+				expect( inputCommand._batches.has( getCurrentBatch() ), 'batch when deleting content' ).to.equal( true );
+			}, { priority: 'highest' } );
+
+			model.on( 'insertContent', () => {
+				expect( inputCommand._batches.has( getCurrentBatch() ), 'batch when inserting content' ).to.equal( true );
+			}, { priority: 'lowest' } );
+
+			setData( model, '<paragraph>[foo]</paragraph>' );
+
+			editor.execute( 'input', { text: 'bar' } );
+
+			expect( getData( model ) ).to.equal( '<paragraph>bar[]</paragraph>' );
+
+			function getCurrentBatch() {
+				return editor.model.change( writer => writer.batch );
+			}
+		} );
 	} );
 	} );
 
 
 	describe( 'destroy()', () => {
 	describe( 'destroy()', () => {

+ 46 - 3
packages/ckeditor5-typing/tests/utils/injectunsafekeystrokeshandling.js

@@ -3,10 +3,12 @@
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
  */
 
 
-import {
-	keyCodes
-} from '@ckeditor/ckeditor5-utils/src/keyboard';
+import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
+import DomEventData from '@ckeditor/ckeditor5-engine/src/view/observer/domeventdata';
+import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+import { keyCodes, getCode } from '@ckeditor/ckeditor5-utils/src/keyboard';
 import { isNonTypingKeystroke } from '../../src/utils/injectunsafekeystrokeshandling';
 import { isNonTypingKeystroke } from '../../src/utils/injectunsafekeystrokeshandling';
+import Typing from '../../src/typing';
 
 
 describe( 'unsafe keystroke handling utils', () => {
 describe( 'unsafe keystroke handling utils', () => {
 	describe( 'isNonTypingKeystroke()', () => {
 	describe( 'isNonTypingKeystroke()', () => {
@@ -82,4 +84,45 @@ describe( 'unsafe keystroke handling utils', () => {
 			expect( isNonTypingKeystroke( { keyCode: keyCodes.a, altKey: true } ), 'Alt+a' ).to.be.false;
 			expect( isNonTypingKeystroke( { keyCode: keyCodes.a, altKey: true } ), 'Alt+a' ).to.be.false;
 		} );
 		} );
 	} );
 	} );
+
+	describe( 'injectUnsafeKeystrokesHandling()', () => {
+		let editor, model;
+
+		beforeEach( () => {
+			return ModelTestEditor.create( { plugins: [ Typing ] } )
+				.then( newEditor => {
+					editor = newEditor;
+					model = editor.model;
+
+					model.schema.register( 'paragraph', { inheritAllFrom: '$block' } );
+				} );
+		} );
+
+		afterEach( () => {
+			return editor.destroy();
+		} );
+
+		it( 'uses typing batch while removing the content', () => {
+			const inputCommand = editor.commands.get( 'input' );
+
+			expect( inputCommand._batches.has( getCurrentBatch() ), 'batch before typing' ).to.equal( false );
+
+			model.on( 'deleteContent', () => {
+				expect( inputCommand._batches.has( getCurrentBatch() ), 'batch when deleting content' ).to.equal( true );
+			}, { priority: 'highest' } );
+
+			setData( model, '<paragraph>[foo]</paragraph>' );
+
+			editor.editing.view.document.fire( 'keydown', new DomEventData( editor.editing.view.document, {
+				preventDefault: () => {},
+				keyCode: getCode( 'A' )
+			} ) );
+
+			expect( getData( model ) ).to.equal( '<paragraph>[]</paragraph>' );
+
+			function getCurrentBatch() {
+				return editor.model.change( writer => writer.batch );
+			}
+		} );
+	} );
 } );
 } );

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;
+}

+ 292 - 18
scripts/docs/build-content-styles.js

@@ -7,26 +7,66 @@
 
 
 const path = require( 'path' );
 const path = require( 'path' );
 const fs = require( 'fs' );
 const fs = require( 'fs' );
+const chalk = require( 'chalk' );
+const glob = require( 'glob' );
+const mkdirp = require( 'mkdirp' );
+const postcss = require( 'postcss' );
 const webpack = require( 'webpack' );
 const webpack = require( 'webpack' );
-const { styles } = require( '@ckeditor/ckeditor5-dev-utils' );
+const { tools, styles } = require( '@ckeditor/ckeditor5-dev-utils' );
 const { version } = require( '../../package.json' );
 const { version } = require( '../../package.json' );
 
 
 const DESTINATION_DIRECTORY = path.join( __dirname, '..', '..', 'build', 'content-styles' );
 const DESTINATION_DIRECTORY = path.join( __dirname, '..', '..', 'build', 'content-styles' );
 const DOCUMENTATION_URL = 'https://ckeditor.com/docs/ckeditor5/latest/builds/guides/integration/content-styles.html';
 const DOCUMENTATION_URL = 'https://ckeditor.com/docs/ckeditor5/latest/builds/guides/integration/content-styles.html';
 const VARIABLE_DEFINITION_REGEXP = /(--[\w-]+):\s+(.*);/g;
 const VARIABLE_DEFINITION_REGEXP = /(--[\w-]+):\s+(.*);/g;
 const VARIABLE_USAGE_REGEXP = /var\((--[\w-]+)\)/g;
 const VARIABLE_USAGE_REGEXP = /var\((--[\w-]+)\)/g;
+const CONTENT_STYLES_GUIDE_PATH = path.join( __dirname, '..', '..', 'docs', 'builds', 'guides', 'integration', 'content-styles.md' );
 
 
 const contentRules = {
 const contentRules = {
 	selector: [],
 	selector: [],
 	variables: [],
 	variables: [],
 	atRules: {}
 	atRules: {}
 };
 };
-
-const webpackConfig = getWebpackConfig();
 const packagesPath = path.join( process.cwd(), 'packages' );
 const packagesPath = path.join( process.cwd(), 'packages' );
+const shouldUpdateGuide = process.argv.includes( '--commit' );
+
+logProcess( 'Gathering all CKEditor 5 modules...' );
+
+getCkeditor5ModulePaths()
+	.then( files => {
+		console.log( `Found ${ files.length } files.` );
+		logProcess( 'Filtering CKEditor 5 plugins...' );
+
+		let promise = Promise.resolve();
+		const ckeditor5Modules = [];
+
+		for ( const modulePath of files ) {
+			promise = promise.then( () => {
+				return checkWhetherIsCKEditor5Plugin( modulePath )
+					.then( isModule => {
+						if ( isModule ) {
+							ckeditor5Modules.push( path.join( process.cwd(), modulePath ) );
+						}
+					} );
+			} );
+		}
 
 
-runWebpack( webpackConfig )
+		return promise.then( () => ckeditor5Modules );
+	} )
+	.then( ckeditor5Modules => {
+		console.log( `Found ${ ckeditor5Modules.length } plugins.` );
+		logProcess( 'Generating source file...' );
+
+		return mkdirp( DESTINATION_DIRECTORY ).then( () => generateCKEditor5Source( ckeditor5Modules ) );
+	} )
 	.then( () => {
 	.then( () => {
+		logProcess( 'Building the editor...' );
+		const webpackConfig = getWebpackConfig();
+
+		return runWebpack( webpackConfig );
+	} )
+	.then( () => {
+		logProcess( 'Preparing the content styles file...' );
+
 		// All variables are placed inside the `:root` selector. Let's extract their names and values as a map.
 		// All variables are placed inside the `:root` selector. Let's extract their names and values as a map.
 		const cssVariables = new Map( contentRules.variables
 		const cssVariables = new Map( contentRules.variables
 			.map( rule => {
 			.map( rule => {
@@ -121,11 +161,130 @@ runWebpack( webpackConfig )
 	} )
 	} )
 	.then( () => {
 	.then( () => {
 		console.log( `Content styles have been extracted to ${ path.join( DESTINATION_DIRECTORY, 'content-styles.css' ) }` );
 		console.log( `Content styles have been extracted to ${ path.join( DESTINATION_DIRECTORY, 'content-styles.css' ) }` );
+
+		if ( !shouldUpdateGuide ) {
+			logProcess( 'Done.' );
+
+			return Promise.resolve();
+		}
+
+		logProcess( 'Updating the content styles guide...' );
+
+		const promises = [
+			readFile( CONTENT_STYLES_GUIDE_PATH ),
+			readFile( path.join( DESTINATION_DIRECTORY, 'content-styles.css' ) )
+		];
+
+		return Promise.all( promises )
+			.then( ( [ guideContent, newContentStyles ] ) => {
+				guideContent = guideContent.replace( /```css([^`]+)```/, newContentStyles );
+
+				return writeFile( CONTENT_STYLES_GUIDE_PATH, guideContent );
+			} )
+			.then( () => {
+				logProcess( 'Saving and committing...' );
+
+				const contentStyleFile = CONTENT_STYLES_GUIDE_PATH.replace( process.cwd() + path.sep, '' );
+
+				// Commit the documentation.
+				if ( exec( `git diff --name-only ${ contentStyleFile }` ).trim().length ) {
+					exec( `git add ${ contentStyleFile }` );
+					exec( 'git commit -m "Docs (ckeditor5): Updated the content styles stylesheet."' );
+
+					console.log( 'Successfully updated the content styles guide.' );
+				} else {
+					console.log( 'Nothing to commit. The content styles guide is up to date.' );
+				}
+
+				logProcess( 'Done.' );
+			} );
 	} )
 	} )
 	.catch( err => {
 	.catch( err => {
 		console.log( err );
 		console.log( err );
 	} );
 	} );
 
 
+/**
+ * Resolves the promise with an array of paths to CKEditor 5 modules.
+ *
+ * @returns {Promise.<Array>}
+ */
+function getCkeditor5ModulePaths() {
+	return new Promise( ( resolve, reject ) => {
+		glob( 'packages/*/src/**/*.js', ( err, files ) => {
+			if ( err ) {
+				return reject( err );
+			}
+
+			return resolve( files );
+		} );
+	} );
+}
+
+/**
+ * Resolves the promise with a boolean value that indicates whether the module under `modulePath` is the CKEditor 5 plugin.
+ *
+ * @param modulePath
+ * @returns {Promise.<Boolean>}
+ */
+function checkWhetherIsCKEditor5Plugin( modulePath ) {
+	return readFile( path.join( process.cwd(), modulePath ) )
+		.then( content => {
+			const pluginName = path.basename( modulePath, '.js' );
+
+			if ( content.match( new RegExp( `export default class ${ pluginName } extends Plugin`, 'i' ) ) ) {
+				return Promise.resolve( true );
+			}
+
+			return Promise.resolve( false );
+		} );
+}
+
+/**
+ * Generates a source file that will be used to build the editor.
+ *
+ * @param {Array.<String>} ckeditor5Modules Paths to CKEditor 5 modules.
+ * @returns {Promise>}
+ */
+function generateCKEditor5Source( ckeditor5Modules ) {
+	ckeditor5Modules = ckeditor5Modules.map( modulePath => {
+		const pluginName = capitalize( path.basename( modulePath, '.js' ) );
+		return { modulePath, pluginName };
+	} );
+
+	const sourceFileContent = [
+		'/**',
+		` * @license Copyright (c) 2003-${ new Date().getFullYear() }, CKSource - Frederico Knabben. All rights reserved.`,
+		' * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license',
+		' */',
+		'',
+		'// The editor creator to use.',
+		'import ClassicEditorBase from \'@ckeditor/ckeditor5-editor-classic/src/classiceditor\';',
+		''
+	];
+
+	for ( const { modulePath, pluginName } of ckeditor5Modules ) {
+		sourceFileContent.push( `import ${ pluginName } from '${ modulePath }';` );
+	}
+
+	sourceFileContent.push( '' );
+	sourceFileContent.push( 'export default class ClassicEditor extends ClassicEditorBase {}' );
+	sourceFileContent.push( '' );
+	sourceFileContent.push( '// Plugins to include in the build.' );
+	sourceFileContent.push( 'ClassicEditor.builtinPlugins = [' );
+
+	for ( const { pluginName } of ckeditor5Modules ) {
+		sourceFileContent.push( '\t' + pluginName + ',' );
+	}
+
+	sourceFileContent.push( '];' );
+
+	return writeFile( path.join( DESTINATION_DIRECTORY, 'source.js' ), sourceFileContent.join( '\n' ) );
+
+	function capitalize( value ) {
+		return value.charAt( 0 ).toUpperCase() + value.slice( 1 );
+	}
+}
+
 /**
 /**
  * Prepares the configuration for webpack.
  * Prepares the configuration for webpack.
  *
  *
@@ -139,34 +298,24 @@ function getWebpackConfig() {
 		minify: false
 		minify: false
 	} );
 	} );
 
 
-	const contentStylesPlugin = require( './content-styles/list-content-styles' )( { contentRules } );
-
-	postCssConfig.plugins.push( contentStylesPlugin );
+	postCssConfig.plugins.push( postCssContentStylesPlugin( contentRules ) );
 
 
 	return {
 	return {
 		mode: 'development',
 		mode: 'development',
-
 		devtool: 'source-map',
 		devtool: 'source-map',
-
 		entry: {
 		entry: {
-			ckeditor5: path.join( __dirname, 'content-styles', 'ckeditor.js' )
+			ckeditor5: path.join( DESTINATION_DIRECTORY, 'source.js' )
 		},
 		},
-
 		output: {
 		output: {
 			path: DESTINATION_DIRECTORY,
 			path: DESTINATION_DIRECTORY,
 			filename: '[name].js'
 			filename: '[name].js'
 		},
 		},
-
-		// Configure the paths so building CKEditor 5 snippets work even if the script
-		// is triggered from a directory outside `ckeditor5` (e.g. in a multi-project case).
 		resolve: {
 		resolve: {
 			modules: getModuleResolvePaths()
 			modules: getModuleResolvePaths()
 		},
 		},
-
 		resolveLoader: {
 		resolveLoader: {
 			modules: getModuleResolvePaths()
 			modules: getModuleResolvePaths()
 		},
 		},
-
 		module: {
 		module: {
 			rules: [
 			rules: [
 				{
 				{
@@ -188,6 +337,92 @@ function getWebpackConfig() {
 	};
 	};
 }
 }
 
 
+/**
+ * Returns the PostCSS plugin that allows intercepting CSS definition used in the editor's build.
+ *
+ * @param {Object} contentRules
+ * @param {Array.<String>} contentRules.variables Variables defined as `:root`.
+ * @param {Object} contentRules.atRules Definitions of behaves.
+ * @param {Array.<String>} contentRules.selector CSS definitions for all selectors.
+ * @returns {Function}
+ */
+function postCssContentStylesPlugin( contentRules ) {
+	return postcss.plugin( 'list-content-styles', function() {
+		const selectorStyles = contentRules.selector;
+		const variables = contentRules.variables;
+
+		return root => {
+			root.walkRules( rule => {
+				for ( const selector of rule.selectors ) {
+					const data = {
+						file: root.source.input.file,
+						css: rule.toString()
+					};
+
+					if ( selector.match( ':root' ) ) {
+						addDefinition( variables, data );
+					}
+
+					if ( selector.match( '.ck-content' ) ) {
+						if ( rule.parent.name && rule.parent.params ) {
+							const atRule = getAtRuleArray( contentRules.atRules, rule.parent.name, rule.parent.params );
+
+							addDefinition( atRule, data );
+						} else {
+							addDefinition( selectorStyles, data );
+						}
+					}
+				}
+			} );
+		};
+	} );
+
+	/**
+	 * @param {Object} collection
+	 * @param {String} name Name of an `at-rule`.
+	 * @param {String} params Parameters that describes the `at-rule`.
+	 * @returns {Array}
+	 */
+	function getAtRuleArray( collection, name, params ) {
+		const definition = `${ name } ${ params }`;
+
+		if ( !collection[ definition ] ) {
+			collection[ definition ] = [];
+		}
+
+		return collection[ definition ];
+	}
+
+	/**
+	 * Checks whether specified definition is duplicated in the colletion.
+	 *
+	 * @param {Array.<StyleStructure>} collection
+	 * @param {StyleStructure} def
+	 * @returns {Boolean}
+	 */
+	function isDuplicatedDefinition( collection, def ) {
+		for ( const item of collection ) {
+			if ( item.file === def.file && item.css === def.css ) {
+				return true;
+			}
+		}
+
+		return false;
+	}
+
+	/**
+	 * Adds definition to the collection if it does not exist in the collection.
+	 *
+	 * @param {Array.<StyleStructure>} collection
+	 * @param {StyleStructure} def
+	 */
+	function addDefinition( collection, def ) {
+		if ( !isDuplicatedDefinition( collection, def ) ) {
+			collection.push( def );
+		}
+	}
+}
+
 /**
 /**
  * @param {Object} webpackConfig
  * @param {Object} webpackConfig
  * @returns {Promise}
  * @returns {Promise}
@@ -216,9 +451,34 @@ function getModuleResolvePaths() {
 	];
 	];
 }
 }
 
 
-function writeFile( file, data ) {
+/**
+ * Resolves the promise with the content of the file saved under the `filePath` location.
+ *
+ * @param {String} filePath The path to fhe file.
+ * @returns {Promise.<String>}
+ */
+function readFile( filePath ) {
 	return new Promise( ( resolve, reject ) => {
 	return new Promise( ( resolve, reject ) => {
-		fs.writeFile( file, data, err => {
+		fs.readFile( filePath, 'utf-8', ( err, content ) => {
+			if ( err ) {
+				return reject( err );
+			}
+
+			return resolve( content );
+		} );
+	} );
+}
+
+/**
+ * Saves the `data` value to the file saved under the `filePath` location.
+ *
+ * @param {String} filePath The path to fhe file.
+ * @param {String} data The content to save.
+ * @returns {Promise.<String>}
+ */
+function writeFile( filePath, data ) {
+	return new Promise( ( resolve, reject ) => {
+		fs.writeFile( filePath, data, err => {
 			if ( err ) {
 			if ( err ) {
 				return reject( err );
 				return reject( err );
 			}
 			}
@@ -275,3 +535,17 @@ function transformCssRules( rules ) {
 		} )
 		} )
 		.join( '\n' );
 		.join( '\n' );
 }
 }
+
+function exec( command ) {
+	return tools.shExec( command, { verbosity: 'error' } );
+}
+
+function logProcess( message ) {
+	console.log( '\n📍 ' + chalk.cyan( message ) );
+}
+
+/**
+ * @typedef {Object} StyleStructure
+ * @property {String} file An absolute path to the file where a definition is defined.
+ * @property {String} css Definition.
+ */

+ 0 - 96
scripts/docs/content-styles/ckeditor.js

@@ -1,96 +0,0 @@
-/**
- * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
- */
-
-// The editor creator to use.
-import ClassicEditorBase from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
-
-import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials';
-import UploadAdapter from '@ckeditor/ckeditor5-adapter-ckfinder/src/uploadadapter';
-import Autoformat from '@ckeditor/ckeditor5-autoformat/src/autoformat';
-import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
-import Code from '@ckeditor/ckeditor5-basic-styles/src/code';
-import CodeBlock from '@ckeditor/ckeditor5-code-block/src/codeblock';
-import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
-import Strikethrough from '@ckeditor/ckeditor5-basic-styles/src/strikethrough';
-import Subscript from '@ckeditor/ckeditor5-basic-styles/src/subscript';
-import Superscript from '@ckeditor/ckeditor5-basic-styles/src/superscript';
-import Underline from '@ckeditor/ckeditor5-basic-styles/src/underline';
-import BlockQuote from '@ckeditor/ckeditor5-block-quote/src/blockquote';
-import CKFinder from '@ckeditor/ckeditor5-ckfinder/src/ckfinder';
-import EasyImage from '@ckeditor/ckeditor5-easy-image/src/easyimage';
-import Heading from '@ckeditor/ckeditor5-heading/src/heading';
-import HorizontalLine from '@ckeditor/ckeditor5-horizontal-line/src/horizontalline';
-import Image from '@ckeditor/ckeditor5-image/src/image';
-import ImageCaption from '@ckeditor/ckeditor5-image/src/imagecaption';
-import ImageResize from '@ckeditor/ckeditor5-image/src/imageresize';
-import ImageStyle from '@ckeditor/ckeditor5-image/src/imagestyle';
-import ImageToolbar from '@ckeditor/ckeditor5-image/src/imagetoolbar';
-import ImageUpload from '@ckeditor/ckeditor5-image/src/imageupload';
-import Link from '@ckeditor/ckeditor5-link/src/link';
-import List from '@ckeditor/ckeditor5-list/src/list';
-import TodoList from '@ckeditor/ckeditor5-list/src/todolist';
-import MediaEmbed from '@ckeditor/ckeditor5-media-embed/src/mediaembed';
-import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
-import PageBreak from '@ckeditor/ckeditor5-page-break/src/pagebreak';
-import PasteFromOffice from '@ckeditor/ckeditor5-paste-from-office/src/pastefromoffice';
-import SpecialCharacters from '@ckeditor/ckeditor5-special-characters/src/specialcharacters';
-import SpecialCharactersEssentials from '@ckeditor/ckeditor5-special-characters/src/specialcharactersessentials';
-import StandardEditingMode from '@ckeditor/ckeditor5-restricted-editing/src/standardeditingmode';
-import RestrictedEditingMode from '@ckeditor/ckeditor5-restricted-editing/src/restrictededitingmode';
-import Table from '@ckeditor/ckeditor5-table/src/table';
-import TableProperties from '@ckeditor/ckeditor5-table/src/tableproperties';
-import TableCellProperties from '@ckeditor/ckeditor5-table/src/tablecellproperties';
-import TableSelection from '@ckeditor/ckeditor5-table/src/tableselection';
-import TableToolbar from '@ckeditor/ckeditor5-table/src/tabletoolbar';
-import Font from '@ckeditor/ckeditor5-font/src/font';
-import Highlight from '@ckeditor/ckeditor5-highlight/src/highlight';
-import Alignment from '@ckeditor/ckeditor5-alignment/src/alignment';
-
-export default class ClassicEditor extends ClassicEditorBase {}
-
-// Plugins to include in the build.
-ClassicEditor.builtinPlugins = [
-	Essentials,
-	UploadAdapter,
-	Autoformat,
-	Bold,
-	Code,
-	CodeBlock,
-	Italic,
-	Strikethrough,
-	Subscript,
-	Superscript,
-	Underline,
-	BlockQuote,
-	CKFinder,
-	EasyImage,
-	Heading,
-	HorizontalLine,
-	Image,
-	ImageCaption,
-	ImageResize,
-	ImageStyle,
-	ImageToolbar,
-	ImageUpload,
-	Link,
-	List,
-	TodoList,
-	MediaEmbed,
-	PageBreak,
-	Paragraph,
-	PasteFromOffice,
-	SpecialCharacters,
-	SpecialCharactersEssentials,
-	StandardEditingMode,
-	RestrictedEditingMode,
-	Table,
-	TableProperties,
-	TableCellProperties,
-	TableSelection,
-	TableToolbar,
-	Font,
-	Highlight,
-	Alignment
-];

+ 0 - 91
scripts/docs/content-styles/list-content-styles.js

@@ -1,91 +0,0 @@
-#!/usr/bin/env node
-
-/**
- * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
- */
-
-/* eslint-env node */
-
-const postcss = require( 'postcss' );
-
-module.exports = postcss.plugin( 'list-content-styles', function( options ) {
-	const selectorStyles = options.contentRules.selector;
-	const variables = options.contentRules.variables;
-
-	return root => {
-		root.walkRules( rule => {
-			for ( const selector of rule.selectors ) {
-				const data = {
-					file: root.source.input.file,
-					css: rule.toString()
-				};
-
-				if ( selector.match( ':root' ) ) {
-					addDefinition( variables, data );
-				}
-
-				if ( selector.match( '.ck-content' ) ) {
-					if ( rule.parent.name && rule.parent.params ) {
-						const atRule = getAtRuleArray( options.contentRules.atRules, rule.parent.name, rule.parent.params );
-
-						addDefinition( atRule, data );
-					} else {
-						addDefinition( selectorStyles, data );
-					}
-				}
-			}
-		} );
-	};
-} );
-
-/**
- * @param {Object} collection
- * @param {String} name Name of an `at-rule`.
- * @param {String} params Parameters that describes the `at-rule`.
- * @returns {Array}
- */
-function getAtRuleArray( collection, name, params ) {
-	const definition = `${ name } ${ params }`;
-
-	if ( !collection[ definition ] ) {
-		collection[ definition ] = [];
-	}
-
-	return collection[ definition ];
-}
-
-/**
- * Checks whether specified definition is duplicated in the colletion.
- *
- * @param {Array.<StyleStructure>} collection
- * @param {StyleStructure} def
- * @returns {Boolean}
- */
-function isDuplicatedDefinition( collection, def ) {
-	for ( const item of collection ) {
-		if ( item.file === def.file && item.css === def.css ) {
-			return true;
-		}
-	}
-
-	return false;
-}
-
-/**
- * Adds definition to the collection if it does not exist in the collection.
- *
- * @param {Array.<StyleStructure>} collection
- * @param {StyleStructure} def
- */
-function addDefinition( collection, def ) {
-	if ( !isDuplicatedDefinition( collection, def ) ) {
-		collection.push( def );
-	}
-}
-
-/**
- * @typedef {Object} StyleStructure
- * @property {String} file An absolute path to the file where a definition is defined.
- * @property {String} css Definition.
- */

Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels