Sfoglia il codice sorgente

Merge branch 'master' into i/5734-balloon-comment

Aleksander Nowodzinski 5 anni fa
parent
commit
8acf8f67d0
81 ha cambiato i file con 4626 aggiunte e 1690 eliminazioni
  1. 120 96
      docs/builds/guides/integration/content-styles.md
  2. 6 1
      packages/ckeditor5-core/tests/_utils-tests/articlepluginset.js
  3. 8 1
      packages/ckeditor5-engine/src/model/utils/selection-post-fixer.js
  4. 45 11
      packages/ckeditor5-engine/tests/manual/highlight.html
  5. 3 2
      packages/ckeditor5-engine/tests/manual/highlight.js
  6. 6 1
      packages/ckeditor5-engine/tests/manual/highlight.md
  7. 29 0
      packages/ckeditor5-engine/tests/model/utils/selection-post-fixer.js
  8. 10 1
      packages/ckeditor5-font/tests/integration.js
  9. 1 5
      packages/ckeditor5-image/src/image/converters.js
  10. 13 1
      packages/ckeditor5-image/src/image/utils.js
  11. 0 6
      packages/ckeditor5-image/tests/image.js
  12. 15 2
      packages/ckeditor5-image/tests/image/converters.js
  13. 4 3
      packages/ckeditor5-image/tests/image/imageediting.js
  14. 55 2
      packages/ckeditor5-image/tests/image/utils.js
  15. 4 1
      packages/ckeditor5-image/tests/imagetoolbar.js
  16. 5 1
      packages/ckeditor5-image/tests/integration.js
  17. 24 0
      packages/ckeditor5-link/docs/features/link.md
  18. 2 0
      packages/ckeditor5-link/package.json
  19. 22 0
      packages/ckeditor5-link/src/link.js
  20. 40 1
      packages/ckeditor5-link/src/linkcommand.js
  21. 36 0
      packages/ckeditor5-link/src/linkimage.js
  22. 141 0
      packages/ckeditor5-link/src/linkimageediting.js
  23. 39 0
      packages/ckeditor5-link/src/linkimageui.js
  24. 15 2
      packages/ckeditor5-link/src/linkui.js
  25. 6 5
      packages/ckeditor5-link/src/ui/linkformview.js
  26. 62 3
      packages/ckeditor5-link/tests/linkcommand.js
  27. 18 0
      packages/ckeditor5-link/tests/linkimage.js
  28. 334 0
      packages/ckeditor5-link/tests/linkimageediting.js
  29. 41 0
      packages/ckeditor5-link/tests/linkimageui.js
  30. 145 1
      packages/ckeditor5-link/tests/linkui.js
  31. 11 0
      packages/ckeditor5-link/tests/manual/linkimage.html
  32. 49 0
      packages/ckeditor5-link/tests/manual/linkimage.js
  33. 3 0
      packages/ckeditor5-link/tests/manual/linkimage.md
  34. 59 0
      packages/ckeditor5-link/tests/manual/protocol.html
  35. 37 0
      packages/ckeditor5-link/tests/manual/protocol.js
  36. 6 0
      packages/ckeditor5-link/tests/manual/protocol.md
  37. BIN
      packages/ckeditor5-link/tests/manual/sample.jpg
  38. 20 2
      packages/ckeditor5-media-embed/tests/mediaembedtoolbar.js
  39. 18 5
      packages/ckeditor5-paragraph/src/insertparagraphcommand.js
  40. 28 6
      packages/ckeditor5-paragraph/tests/insertparagraphcommand.js
  41. 38 30
      packages/ckeditor5-table/src/converters/downcast.js
  42. 5 9
      packages/ckeditor5-table/src/tableediting.js
  43. 10 76
      packages/ckeditor5-table/src/tablekeyboard.js
  44. 12 94
      packages/ckeditor5-table/tests/_utils/utils.js
  45. 51 20
      packages/ckeditor5-table/tests/commands/insertcolumncommand.js
  46. 71 22
      packages/ckeditor5-table/tests/commands/insertrowcommand.js
  47. 6 8
      packages/ckeditor5-table/tests/commands/inserttablecommand.js
  48. 18 11
      packages/ckeditor5-table/tests/commands/mergecellcommand.js
  49. 7 8
      packages/ckeditor5-table/tests/commands/removecolumncommand.js
  50. 6 6
      packages/ckeditor5-table/tests/commands/selectcolumncommand.js
  51. 6 6
      packages/ckeditor5-table/tests/commands/selectrowcommand.js
  52. 7 8
      packages/ckeditor5-table/tests/commands/setheadercolumncommand.js
  53. 8 8
      packages/ckeditor5-table/tests/commands/setheaderrowcommand.js
  54. 7 8
      packages/ckeditor5-table/tests/commands/splitcellcommand.js
  55. 443 537
      packages/ckeditor5-table/tests/converters/downcast.js
  56. 7 12
      packages/ckeditor5-table/tests/converters/table-cell-refresh-post-fixer.js
  57. 7 3
      packages/ckeditor5-table/tests/converters/upcasttable.js
  58. 19 7
      packages/ckeditor5-table/tests/table-integration.js
  59. 3 0
      packages/ckeditor5-table/tests/tablekeyboard.js
  60. 123 171
      packages/ckeditor5-table/tests/tableutils.js
  61. 5 6
      packages/ckeditor5-table/tests/tablewalker.js
  62. 7 7
      packages/ckeditor5-table/tests/utils/common.js
  63. 101 7
      packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widgettypearound.css
  64. 15 9
      packages/ckeditor5-typing/src/utils/injectunsafekeystrokeshandling.js
  65. 85 0
      packages/ckeditor5-typing/tests/utils/injectunsafekeystrokeshandling.js
  66. 61 0
      packages/ckeditor5-utils/src/keyboard.js
  67. 108 1
      packages/ckeditor5-utils/tests/keyboard.js
  68. 2 0
      packages/ckeditor5-widget/package.json
  69. 100 114
      packages/ckeditor5-widget/src/widget.js
  70. 18 1
      packages/ckeditor5-widget/src/widgettoolbarrepository.js
  71. 0 38
      packages/ckeditor5-widget/src/widgettypearound/utils.js
  72. 502 52
      packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js
  73. 2 2
      packages/ckeditor5-widget/tests/manual/type-around.html
  74. 15 2
      packages/ckeditor5-widget/tests/manual/type-around.js
  75. 7 6
      packages/ckeditor5-widget/tests/widget-integration.js
  76. 151 171
      packages/ckeditor5-widget/tests/widget.js
  77. 4 1
      packages/ckeditor5-widget/tests/widgetresize.js
  78. 4 1
      packages/ckeditor5-widget/tests/widgetresize/resizer.js
  79. 15 1
      packages/ckeditor5-widget/tests/widgettoolbarrepository.js
  80. 1054 57
      packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js
  81. 36 11
      packages/ckeditor5-widget/theme/widgettypearound.css

+ 120 - 96
docs/builds/guides/integration/content-styles.md

@@ -48,18 +48,18 @@ Below there is a full list of content styles used by the editor features. You ca
 
 
 ```css
 ```css
 /*
 /*
- * CKEditor 5 (v17.0.0) content styles.
- * Generated on Fri, 13 Mar 2020 13:27:10 GMT.
+ * CKEditor 5 (v19.1.1) content styles.
+ * Generated on Tue, 09 Jun 2020 10:37:49 GMT.
  * For more information, check out https://ckeditor.com/docs/ckeditor5/latest/builds/guides/integration/content-styles.html
  * For more information, check out https://ckeditor.com/docs/ckeditor5/latest/builds/guides/integration/content-styles.html
  */
  */
 
 
 :root {
 :root {
-	--ck-highlight-marker-blue: #72cdfd;
-	--ck-highlight-marker-green: #63f963;
-	--ck-highlight-marker-pink: #fc7999;
-	--ck-highlight-marker-yellow: #fdfd77;
-	--ck-highlight-pen-green: #118800;
-	--ck-highlight-pen-red: #e91313;
+	--ck-highlight-marker-blue: hsl(201, 97%, 72%);
+	--ck-highlight-marker-green: hsl(120, 93%, 68%);
+	--ck-highlight-marker-pink: hsl(345, 96%, 73%);
+	--ck-highlight-marker-yellow: hsl(60, 97%, 73%);
+	--ck-highlight-pen-green: hsl(112, 100%, 27%);
+	--ck-highlight-pen-red: hsl(0, 85%, 49%);
 	--ck-image-style-spacing: 1.5em;
 	--ck-image-style-spacing: 1.5em;
 	--ck-todo-list-checkmark-size: 16px;
 	--ck-todo-list-checkmark-size: 16px;
 }
 }
@@ -78,26 +78,23 @@ Below there is a full list of content styles used by the editor features. You ca
 .ck-content .image.image_resized > figcaption {
 .ck-content .image.image_resized > figcaption {
 	display: block;
 	display: block;
 }
 }
+/* ckeditor5-image/theme/imagecaption.css */
+.ck-content .image > figcaption {
+	display: table-caption;
+	caption-side: bottom;
+	word-break: break-word;
+	color: hsl(0, 0%, 20%);
+	background-color: hsl(0, 0%, 97%);
+	padding: .6em;
+	font-size: .75em;
+	outline-offset: -1px;
+}
 /* ckeditor5-basic-styles/theme/code.css */
 /* ckeditor5-basic-styles/theme/code.css */
 .ck-content code {
 .ck-content code {
 	background-color: hsla(0, 0%, 78%, 0.3);
 	background-color: hsla(0, 0%, 78%, 0.3);
 	padding: .15em;
 	padding: .15em;
 	border-radius: 2px;
 	border-radius: 2px;
 }
 }
-/* ckeditor5-image/theme/image.css */
-.ck-content .image {
-	display: table;
-	clear: both;
-	text-align: center;
-	margin: 1em auto;
-}
-/* ckeditor5-image/theme/image.css */
-.ck-content .image > img {
-	display: block;
-	margin: 0 auto;
-	max-width: 100%;
-	min-width: 50px;
-}
 /* ckeditor5-image/theme/imagestyle.css */
 /* ckeditor5-image/theme/imagestyle.css */
 .ck-content .image-style-side,
 .ck-content .image-style-side,
 .ck-content .image-style-align-left,
 .ck-content .image-style-align-left,
@@ -125,42 +122,6 @@ Below there is a full list of content styles used by the editor features. You ca
 	float: right;
 	float: right;
 	margin-left: var(--ck-image-style-spacing);
 	margin-left: var(--ck-image-style-spacing);
 }
 }
-/* ckeditor5-page-break/theme/pagebreak.css */
-.ck-content .page-break {
-	position: relative;
-	clear: both;
-	padding: 5px 0;
-	display: flex;
-	align-items: center;
-	justify-content: center;
-}
-/* ckeditor5-page-break/theme/pagebreak.css */
-.ck-content .page-break::after {
-	content: '';
-	position: absolute;
-	border-bottom: 2px dashed hsl(0, 0%, 77%);
-	width: 100%;
-}
-/* ckeditor5-page-break/theme/pagebreak.css */
-.ck-content .page-break__label {
-	position: relative;
-	z-index: 1;
-	padding: .3em .6em;
-	display: block;
-	text-transform: uppercase;
-	border: 1px solid hsl(0, 0%, 77%);
-	border-radius: 2px;
-	font-family: Helvetica, Arial, Tahoma, Verdana, Sans-Serif;
-	font-size: 0.75em;
-	font-weight: bold;
-	color: hsl(0, 0%, 20%);
-	background: #fff;
-	box-shadow: 2px 2px 1px hsla(0, 0%, 0%, 0.15);
-	-webkit-user-select: none;
-	-moz-user-select: none;
-	-ms-user-select: none;
-	user-select: none;
-}
 /* ckeditor5-block-quote/theme/blockquote.css */
 /* ckeditor5-block-quote/theme/blockquote.css */
 .ck-content blockquote {
 .ck-content blockquote {
 	overflow: hidden;
 	overflow: hidden;
@@ -176,12 +137,46 @@ Below there is a full list of content styles used by the editor features. You ca
 	border-left: 0;
 	border-left: 0;
 	border-right: solid 5px hsl(0, 0%, 80%);
 	border-right: solid 5px hsl(0, 0%, 80%);
 }
 }
-/* ckeditor5-media-embed/theme/mediaembed.css */
-.ck-content .media {
+/* ckeditor5-code-block/theme/codeblock.css */
+.ck-content pre {
+	padding: 1em;
+	color: hsl(0, 0%, 20.8%);
+	background: hsla(0, 0%, 78%, 0.3);
+	border: 1px solid hsl(0, 0%, 77%);
+	border-radius: 2px;
+	text-align: left;
+	direction: ltr;
+	tab-size: 4;
+	white-space: pre-wrap;
+	font-style: normal;
+	min-width: 200px;
+}
+/* ckeditor5-code-block/theme/codeblock.css */
+.ck-content pre code {
+	background: unset;
+	padding: 0;
+	border-radius: 0;
+}
+/* ckeditor5-horizontal-line/theme/horizontalline.css */
+.ck-content hr {
+	border-width: 1px 0 0;
+	border-style: solid;
+	border-color: hsl(0, 0%, 37%);
+	margin: 0;
+}
+/* ckeditor5-image/theme/image.css */
+.ck-content .image {
+	display: table;
 	clear: both;
 	clear: both;
-	margin: 1em 0;
+	text-align: center;
+	margin: 1em auto;
+}
+/* ckeditor5-image/theme/image.css */
+.ck-content .image > img {
 	display: block;
 	display: block;
-	min-width: 15em;
+	margin: 0 auto;
+	max-width: 100%;
+	min-width: 50px;
 }
 }
 /* ckeditor5-table/theme/table.css */
 /* ckeditor5-table/theme/table.css */
 .ck-content .table {
 .ck-content .table {
@@ -201,13 +196,28 @@ Below there is a full list of content styles used by the editor features. You ca
 .ck-content .table table th {
 .ck-content .table table th {
 	min-width: 2em;
 	min-width: 2em;
 	padding: .4em;
 	padding: .4em;
-	border-color: hsl(0, 0%, 75%);
+	border: 1px solid hsl(0, 0%, 75%);
 }
 }
 /* ckeditor5-table/theme/table.css */
 /* ckeditor5-table/theme/table.css */
 .ck-content .table table th {
 .ck-content .table table th {
 	font-weight: bold;
 	font-weight: bold;
 	background: hsla(0, 0%, 0%, 5%);
 	background: hsla(0, 0%, 0%, 5%);
 }
 }
+/* ckeditor5-table/theme/table.css */
+.ck-content[dir="rtl"] .table th {
+	text-align: right;
+}
+/* ckeditor5-table/theme/table.css */
+.ck-content[dir="ltr"] .table th {
+	text-align: left;
+}
+/* ckeditor5-media-embed/theme/mediaembed.css */
+.ck-content .media {
+	clear: both;
+	margin: 1em 0;
+	display: block;
+	min-width: 15em;
+}
 /* ckeditor5-list/theme/todolist.css */
 /* ckeditor5-list/theme/todolist.css */
 .ck-content .todo-list {
 .ck-content .todo-list {
 	list-style: none;
 	list-style: none;
@@ -275,17 +285,6 @@ Below there is a full list of content styles used by the editor features. You ca
 .ck-content .todo-list .todo-list__label .todo-list__label__description {
 .ck-content .todo-list .todo-list__label .todo-list__label__description {
 	vertical-align: middle;
 	vertical-align: middle;
 }
 }
-/* ckeditor5-image/theme/imagecaption.css */
-.ck-content .image > figcaption {
-	display: table-caption;
-	caption-side: bottom;
-	word-break: break-word;
-	color: hsl(0, 0%, 20%);
-	background-color: hsl(0, 0%, 97%);
-	padding: .6em;
-	font-size: .75em;
-	outline-offset: -1px;
-}
 /* ckeditor5-highlight/theme/highlight.css */
 /* ckeditor5-highlight/theme/highlight.css */
 .ck-content .marker-yellow {
 .ck-content .marker-yellow {
 	background-color: var(--ck-highlight-marker-yellow);
 	background-color: var(--ck-highlight-marker-yellow);
@@ -312,32 +311,57 @@ Below there is a full list of content styles used by the editor features. You ca
 	color: var(--ck-highlight-pen-green);
 	color: var(--ck-highlight-pen-green);
 	background-color: transparent;
 	background-color: transparent;
 }
 }
-/* ckeditor5-horizontal-line/theme/horizontalline.css */
-.ck-content hr {
-	border-width: 1px 0 0;
-	border-style: solid;
-	border-color: hsl(0, 0%, 37%);
-	margin: 0;
+/* ckeditor5-page-break/theme/pagebreak.css */
+.ck-content .page-break {
+	position: relative;
+	clear: both;
+	padding: 5px 0;
+	display: flex;
+	align-items: center;
+	justify-content: center;
 }
 }
-/* ckeditor5-code-block/theme/codeblock.css */
-.ck-content pre {
-	padding: 1em;
-	color: #353535;
-	background: hsla(0, 0%, 78%, 0.3);
+/* ckeditor5-page-break/theme/pagebreak.css */
+.ck-content .page-break::after {
+	content: '';
+	position: absolute;
+	border-bottom: 2px dashed hsl(0, 0%, 77%);
+	width: 100%;
+}
+/* ckeditor5-page-break/theme/pagebreak.css */
+.ck-content .page-break__label {
+	position: relative;
+	z-index: 1;
+	padding: .3em .6em;
+	display: block;
+	text-transform: uppercase;
 	border: 1px solid hsl(0, 0%, 77%);
 	border: 1px solid hsl(0, 0%, 77%);
 	border-radius: 2px;
 	border-radius: 2px;
-	text-align: left;
-	direction: ltr;
-	tab-size: 4;
-	white-space: pre-wrap;
-	font-style: normal;
-	min-width: 200px;
+	font-family: Helvetica, Arial, Tahoma, Verdana, Sans-Serif;
+	font-size: 0.75em;
+	font-weight: bold;
+	color: hsl(0, 0%, 20%);
+	background: hsl(0, 0%, 100%);
+	box-shadow: 2px 2px 1px hsla(0, 0%, 0%, 0.15);
+	-webkit-user-select: none;
+	-moz-user-select: none;
+	-ms-user-select: none;
+	user-select: none;
 }
 }
-/* ckeditor5-code-block/theme/codeblock.css */
-.ck-content pre code {
-	background: unset;
-	padding: 0;
-	border-radius: 0;
+/* ckeditor5-font/theme/fontsize.css */
+.ck-content .text-tiny {
+	font-size: .7em;
+}
+/* ckeditor5-font/theme/fontsize.css */
+.ck-content .text-small {
+	font-size: .85em;
+}
+/* ckeditor5-font/theme/fontsize.css */
+.ck-content .text-big {
+	font-size: 1.4em;
+}
+/* ckeditor5-font/theme/fontsize.css */
+.ck-content .text-huge {
+	font-size: 1.8em;
 }
 }
 @media print {
 @media print {
 	/* ckeditor5-page-break/theme/pagebreak.css */
 	/* ckeditor5-page-break/theme/pagebreak.css */

+ 6 - 1
packages/ckeditor5-core/tests/_utils-tests/articlepluginset.js

@@ -33,7 +33,12 @@ describe( 'ArticlePluginSet', () => {
 		editorElement = document.createElement( 'div' );
 		editorElement = document.createElement( 'div' );
 		document.body.appendChild( editorElement );
 		document.body.appendChild( editorElement );
 
 
-		editor = await ClassicTestEditor.create( editorElement, { plugins: [ ArticlePluginSet ] } );
+		editor = await ClassicTestEditor.create( editorElement, {
+			plugins: [ ArticlePluginSet ],
+			image: {
+				toolbar: [ 'imageStyle:full', 'imageStyle:side' ]
+			}
+		} );
 	} );
 	} );
 
 
 	afterEach( async () => {
 	afterEach( async () => {

+ 8 - 1
packages/ckeditor5-engine/src/model/utils/selection-post-fixer.js

@@ -82,7 +82,14 @@ function selectionPostFixer( writer, model ) {
 		// Those ranges might overlap but will be corrected later.
 		// Those ranges might overlap but will be corrected later.
 		const correctedRange = tryFixingRange( modelRange, schema );
 		const correctedRange = tryFixingRange( modelRange, schema );
 
 
-		if ( correctedRange ) {
+		// "Selection fixing" algorithms sometimes get lost. In consequence, it may happen
+		// that a new range is returned but, in fact, it has the same positions as the original
+		// range anyway. If this range is not discarded, a new selection will be set and that,
+		// for instance, would destroy the selection attributes. Let's make sure that the post-fixer
+		// actually worked first before setting a new selection.
+		//
+		// https://github.com/ckeditor/ckeditor5/issues/6693
+		if ( correctedRange && !correctedRange.isEqual( modelRange ) ) {
 			ranges.push( correctedRange );
 			ranges.push( correctedRange );
 			wasFixed = true;
 			wasFixed = true;
 		} else {
 		} else {

+ 45 - 11
packages/ckeditor5-engine/tests/manual/highlight.html

@@ -1,37 +1,61 @@
 <style>
 <style>
+	:root {
+		--ck-manual-test-highlight-yellow: hsla(60, 100%, 50%, 0.5);
+		--ck-manual-test-highlight-red: hsla(0, 100%, 50%, 0.5);
+		--ck-manual-test-highlight-blue: hsla(240, 100%, 50%, 0.5);
+	}
+
 	span.highlight-yellow {
 	span.highlight-yellow {
-		background-color: rgba( 255, 255, 0, 0.5 );
+		background-color: var(--ck-manual-test-highlight-yellow);
 	}
 	}
 
 
 	span.highlight-blue {
 	span.highlight-blue {
-		background-color: rgba( 0, 0, 255, 0.5 );
+		background-color: var(--ck-manual-test-highlight-blue);
 	}
 	}
 
 
 	span.highlight-red {
 	span.highlight-red {
-		background-color: rgba( 255, 0, 0, 0.5 );
+		background-color: var(--ck-manual-test-highlight-red);
 	}
 	}
 
 
 	.ck-widget.fancy-widget {
 	.ck-widget.fancy-widget {
-		background-color: black;
-		color: white;
+		background-color: hsl(0, 0%, 0%);
+		color: hsl(0, 0%, 100%);
 		text-align: center;
 		text-align: center;
 		height: 40px;
 		height: 40px;
 		line-height: 40px;
 		line-height: 40px;
 	}
 	}
 
 
 	.ck-widget.fancy-widget.highlight-yellow {
 	.ck-widget.fancy-widget.highlight-yellow {
-		background-color: yellow;
-		color: black;
+		background-color: var(--ck-manual-test-highlight-yellow);
+		color: hsl(0, 0%, 0%);
 	}
 	}
 
 
 	.ck-widget.fancy-widget.highlight-blue {
 	.ck-widget.fancy-widget.highlight-blue {
-		background-color: blue;
-		color: black;
+		background-color: var(--ck-manual-test-highlight-blue);
+		color: hsl(0, 0%, 0%);
 	}
 	}
 
 
 	.ck-widget.fancy-widget.highlight-red {
 	.ck-widget.fancy-widget.highlight-red {
-		background-color: red;
-		color: black;
+		background-color: var(--ck-manual-test-highlight-red);
+		color: hsl(0, 0%, 0%);
+	}
+
+	.table td.highlight-yellow,
+	.table th.highlight-yellow {
+		/* !important to override selected editable styles. */
+		background-color: var(--ck-manual-test-highlight-yellow) !important;
+	}
+
+	.table td.highlight-blue,
+	.table th.highlight-blue {
+		/* !important to override selected editable styles. */
+		background-color: var(--ck-manual-test-highlight-blue) !important;
+	}
+
+	.table td.highlight-red,
+	.table th.highlight-red {
+		/* !important to override selected editable styles. */
+		background-color: var(--ck-manual-test-highlight-red) !important;
 	}
 	}
 
 
 	button {
 	button {
@@ -43,6 +67,16 @@
 	<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas at diam quis justo imperdiet posuere non eu tortor. Mauris vel magna eu sem hendrerit varius. Ut imperdiet velit ut ante interdum convallis. Vestibulum vitae lacinia mi. </p>
 	<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas at diam quis justo imperdiet posuere non eu tortor. Mauris vel magna eu sem hendrerit varius. Ut imperdiet velit ut ante interdum convallis. Vestibulum vitae lacinia mi. </p>
 	<figure></figure>
 	<figure></figure>
 	<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas at diam quis justo imperdiet posuere non eu tortor. Mauris vel magna eu sem hendrerit varius. Ut imperdiet velit ut ante interdum convallis. Vestibulum vitae lacinia mi. </p>
 	<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas at diam quis justo imperdiet posuere non eu tortor. Mauris vel magna eu sem hendrerit varius. Ut imperdiet velit ut ante interdum convallis. Vestibulum vitae lacinia mi. </p>
+	<table>
+		<tr>
+			<td>foo</td>
+			<td>bar</td>
+		</tr>
+		<tr>
+			<td>baz</td>
+			<td>qux</td>
+		</tr>
+	</table>
 </div>
 </div>
 <h2>Markers</h2>
 <h2>Markers</h2>
 <button id="add-marker-yellow">Set yellow highlight</button><button id="remove-marker-yellow">Remove yellow highlight</button>
 <button id="add-marker-yellow">Set yellow highlight</button><button id="remove-marker-yellow">Remove yellow highlight</button>

+ 3 - 2
packages/ckeditor5-engine/tests/manual/highlight.js

@@ -16,6 +16,7 @@ import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
 import List from '@ckeditor/ckeditor5-list/src/list';
 import List from '@ckeditor/ckeditor5-list/src/list';
 import global from '@ckeditor/ckeditor5-utils/src/dom/global';
 import global from '@ckeditor/ckeditor5-utils/src/dom/global';
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import Table from '@ckeditor/ckeditor5-table/src/table';
 import Widget from '@ckeditor/ckeditor5-widget/src/widget';
 import Widget from '@ckeditor/ckeditor5-widget/src/widget';
 import { toWidget } from '@ckeditor/ckeditor5-widget/src/utils';
 import { toWidget } from '@ckeditor/ckeditor5-widget/src/utils';
 
 
@@ -53,8 +54,8 @@ class FancyWidget extends Plugin {
 }
 }
 
 
 ClassicEditor.create( global.document.querySelector( '#editor' ), {
 ClassicEditor.create( global.document.querySelector( '#editor' ), {
-	plugins: [ Enter, Typing, Paragraph, Undo, Heading, Bold, Italic, List, FancyWidget ],
-	toolbar: [ 'heading', '|', 'undo', 'redo', 'bold', 'italic', 'numberedList', 'bulletedList' ]
+	plugins: [ Enter, Typing, Paragraph, Undo, Heading, Bold, Italic, List, Table, FancyWidget ],
+	toolbar: [ 'heading', '|', 'undo', 'redo', 'bold', 'italic', 'numberedList', 'bulletedList', 'insertTable' ]
 } )
 } )
 	.then( editor => {
 	.then( editor => {
 		window.editor = editor;
 		window.editor = editor;

+ 6 - 1
packages/ckeditor5-engine/tests/manual/highlight.md

@@ -10,5 +10,10 @@ applied.
 1. Red highlight should be visible on the whole content, including the widget.
 1. Red highlight should be visible on the whole content, including the widget.
 1. Play with highlights, add and remove them, check if they are applied correctly.
 1. Play with highlights, add and remove them, check if they are applied correctly.
 
 
-NOTE: Yellow highlight should be visible over red and blue one. Red highlight should be visible over blue one. 
+### Highlighting table cells
+
+1. You can select the entire table cell and apply a highlight to it.
+1. The highlight should change the background of an **entire cell** (not only its content).
+
+NOTE: Yellow highlight should be visible over red and blue one. Red highlight should be visible over blue one.
 
 

+ 29 - 0
packages/ckeditor5-engine/tests/model/utils/selection-post-fixer.js

@@ -835,6 +835,35 @@ describe( 'Selection post-fixer', () => {
 					'</table>]'
 					'</table>]'
 				);
 				);
 			} );
 			} );
+
+			it( 'should not reset the selection if the final range is the same as the initial one', () => {
+				setModelData( model,
+					'<table>' +
+						'<tableRow>' +
+							'<tableCell>[<image></image>]</tableCell>' +
+						'</tableRow>' +
+					'</table>'
+				);
+
+				// Setting a selection attribute will trigger the post-fixer. However, because this
+				// action does not affect ranges, the post-fixer should not set a new selection and,
+				// in consequence, should not clear the selection attribute (like it normally would when
+				// a new selection is set).
+				// https://github.com/ckeditor/ckeditor5/issues/6693
+				model.change( writer => {
+					writer.setSelectionAttribute( 'foo', 'bar' );
+				} );
+
+				assertEqualMarkup( getModelData( model ),
+					'<table>' +
+						'<tableRow>' +
+							'<tableCell>[<image></image>]</tableCell>' +
+						'</tableRow>' +
+					'</table>'
+				);
+
+				expect( model.document.selection.hasAttribute( 'foo' ) ).to.be.true;
+			} );
 		} );
 		} );
 
 
 		describe( 'non-collapsed selection - image scenarios', () => {
 		describe( 'non-collapsed selection - image scenarios', () => {

+ 10 - 1
packages/ckeditor5-font/tests/integration.js

@@ -19,7 +19,10 @@ describe( 'Integration test Font', () => {
 
 
 		return ClassicTestEditor
 		return ClassicTestEditor
 			.create( element, {
 			.create( element, {
-				plugins: [ Font, ArticlePluginSet ]
+				plugins: [ Font, ArticlePluginSet ],
+				image: {
+					toolbar: [ 'imageStyle:full', 'imageStyle:side' ]
+				}
 			} )
 			} )
 			.then( newEditor => {
 			.then( newEditor => {
 				editor = newEditor;
 				editor = newEditor;
@@ -66,6 +69,9 @@ describe( 'Integration test Font', () => {
 					fontSize: {
 					fontSize: {
 						options: [ 10, 12, 14 ],
 						options: [ 10, 12, 14 ],
 						supportAllValues: true
 						supportAllValues: true
+					},
+					image: {
+						toolbar: [ 'imageStyle:full', 'imageStyle:side' ]
 					}
 					}
 				} )
 				} )
 				.then( editor => {
 				.then( editor => {
@@ -129,6 +135,9 @@ describe( 'Integration test Font', () => {
 					fontSize: {
 					fontSize: {
 						options: [ 10, 12, 14 ],
 						options: [ 10, 12, 14 ],
 						supportAllValues: true
 						supportAllValues: true
+					},
+					image: {
+						toolbar: [ 'imageStyle:full', 'imageStyle:side' ]
 					}
 					}
 				} )
 				} )
 				.then( editor => {
 				.then( editor => {

+ 1 - 5
packages/ckeditor5-image/src/image/converters.js

@@ -125,10 +125,6 @@ export function modelToViewAttributeConverter( attributeKey ) {
 		const figure = conversionApi.mapper.toViewElement( data.item );
 		const figure = conversionApi.mapper.toViewElement( data.item );
 		const img = getViewImgFromWidget( figure );
 		const img = getViewImgFromWidget( figure );
 
 
-		if ( data.attributeNewValue !== null ) {
-			viewWriter.setAttribute( data.attributeKey, data.attributeNewValue, img );
-		} else {
-			viewWriter.removeAttribute( data.attributeKey, img );
-		}
+		viewWriter.setAttribute( data.attributeKey, data.attributeNewValue || '', img );
 	}
 	}
 }
 }

+ 13 - 1
packages/ckeditor5-image/src/image/utils.js

@@ -113,11 +113,23 @@ export function isImageAllowed( model ) {
  * Assuming that image is always a first child of a widget (ie. `figureView.getChild( 0 )`) is unsafe as other features might
  * Assuming that image is always a first child of a widget (ie. `figureView.getChild( 0 )`) is unsafe as other features might
  * inject their own elements to the widget.
  * inject their own elements to the widget.
  *
  *
+ * The `<img>` can be wrapped to other elements, e.g. `<a>`. Nested check required.
+ *
  * @param {module:engine/view/element~Element} figureView
  * @param {module:engine/view/element~Element} figureView
  * @returns {module:engine/view/element~Element}
  * @returns {module:engine/view/element~Element}
  */
  */
 export function getViewImgFromWidget( figureView ) {
 export function getViewImgFromWidget( figureView ) {
-	return Array.from( figureView.getChildren() ).find( viewChild => viewChild.is( 'img' ) );
+	const figureChildren = [];
+
+	for ( const figureChild of figureView.getChildren() ) {
+		figureChildren.push( figureChild );
+
+		if ( figureChild.is( 'element' ) ) {
+			figureChildren.push( ...figureChild.getChildren() );
+		}
+	}
+
+	return figureChildren.find( viewChild => viewChild.is( 'img' ) );
 }
 }
 
 
 // Checks if image is allowed by schema in optimal insertion parent.
 // Checks if image is allowed by schema in optimal insertion parent.

+ 0 - 6
packages/ckeditor5-image/tests/image.js

@@ -64,7 +64,6 @@ describe( 'Image', () => {
 			expect( getViewData( view ) ).to.equal(
 			expect( getViewData( view ) ).to.equal(
 				'[<figure class="' +
 				'[<figure class="' +
 					'ck-widget ' +
 					'ck-widget ' +
-					'ck-widget_can-type-around_after ck-widget_can-type-around_before ' +
 					'ck-widget_selected image" contenteditable="false"' +
 					'ck-widget_selected image" contenteditable="false"' +
 				'>' +
 				'>' +
 					'<img alt="alt text" src="/assets/sample.png"></img>' +
 					'<img alt="alt text" src="/assets/sample.png"></img>' +
@@ -82,7 +81,6 @@ describe( 'Image', () => {
 			expect( getViewData( view ) ).to.equal(
 			expect( getViewData( view ) ).to.equal(
 				'[<figure class="' +
 				'[<figure class="' +
 					'ck-widget ' +
 					'ck-widget ' +
-					'ck-widget_can-type-around_after ck-widget_can-type-around_before ' +
 					'ck-widget_selected image" contenteditable="false"' +
 					'ck-widget_selected image" contenteditable="false"' +
 				'>' +
 				'>' +
 					'<img alt="" src="/assets/sample.png"></img>' +
 					'<img alt="" src="/assets/sample.png"></img>' +
@@ -103,7 +101,6 @@ describe( 'Image', () => {
 			expect( getViewData( view ) ).to.equal(
 			expect( getViewData( view ) ).to.equal(
 				'[<figure class="' +
 				'[<figure class="' +
 					'ck-widget ' +
 					'ck-widget ' +
-					'ck-widget_can-type-around_after ck-widget_can-type-around_before ' +
 					'ck-widget_selected image" contenteditable="false"' +
 					'ck-widget_selected image" contenteditable="false"' +
 				'>' +
 				'>' +
 					'<img alt="alt text" src="/assets/sample.png"></img>' +
 					'<img alt="alt text" src="/assets/sample.png"></img>' +
@@ -111,7 +108,6 @@ describe( 'Image', () => {
 				'</figure>]' +
 				'</figure>]' +
 				'<figure class="' +
 				'<figure class="' +
 					'ck-widget ' +
 					'ck-widget ' +
-					'ck-widget_can-type-around_after ck-widget_can-type-around_before ' +
 					'image" contenteditable="false"' +
 					'image" contenteditable="false"' +
 				'>' +
 				'>' +
 					'<img alt="alt text" src="/assets/sample.png"></img>' +
 					'<img alt="alt text" src="/assets/sample.png"></img>' +
@@ -127,7 +123,6 @@ describe( 'Image', () => {
 			expect( getViewData( view ) ).to.equal(
 			expect( getViewData( view ) ).to.equal(
 				'<figure class="' +
 				'<figure class="' +
 					'ck-widget ' +
 					'ck-widget ' +
-					'ck-widget_can-type-around_after ck-widget_can-type-around_before ' +
 					'image" contenteditable="false"' +
 					'image" contenteditable="false"' +
 				'>' +
 				'>' +
 					'<img alt="alt text" src="/assets/sample.png"></img>' +
 					'<img alt="alt text" src="/assets/sample.png"></img>' +
@@ -135,7 +130,6 @@ describe( 'Image', () => {
 				'</figure>' +
 				'</figure>' +
 				'[<figure class="' +
 				'[<figure class="' +
 					'ck-widget ' +
 					'ck-widget ' +
-					'ck-widget_can-type-around_after ck-widget_can-type-around_before ' +
 					'ck-widget_selected image" contenteditable="false"' +
 					'ck-widget_selected image" contenteditable="false"' +
 				'>' +
 				'>' +
 					'<img alt="alt text" src="/assets/sample.png"></img>' +
 					'<img alt="alt text" src="/assets/sample.png"></img>' +

+ 15 - 2
packages/ckeditor5-image/tests/image/converters.js

@@ -218,7 +218,20 @@ describe( 'Image converters', () => {
 			);
 			);
 		} );
 		} );
 
 
-		it( 'should convert removing attribute from image', () => {
+		it( 'should convert an empty "src" attribute from image even if removed', () => {
+			setModelData( model, '<image src="" alt="foo bar"></image>' );
+			const image = document.getRoot().getChild( 0 );
+
+			model.change( writer => {
+				writer.removeAttribute( 'src', image );
+			} );
+
+			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
+				'<figure class="ck-widget image" contenteditable="false"><img alt="foo bar" src=""></img></figure>'
+			);
+		} );
+
+		it( 'should convert an empty "alt" attribute from image even if removed', () => {
 			setModelData( model, '<image src="" alt="foo bar"></image>' );
 			setModelData( model, '<image src="" alt="foo bar"></image>' );
 			const image = document.getRoot().getChild( 0 );
 			const image = document.getRoot().getChild( 0 );
 
 
@@ -227,7 +240,7 @@ describe( 'Image converters', () => {
 			} );
 			} );
 
 
 			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
 			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
-				'<figure class="ck-widget image" contenteditable="false"><img src=""></img></figure>'
+				'<figure class="ck-widget image" contenteditable="false"><img alt="" src=""></img></figure>'
 			);
 			);
 		} );
 		} );
 
 

+ 4 - 3
packages/ckeditor5-image/tests/image/imageediting.js

@@ -458,7 +458,7 @@ describe( 'ImageEditing', () => {
 				);
 				);
 			} );
 			} );
 
 
-			it( 'should convert attribute removal', () => {
+			it( 'should convert attribute removal (but keeps an empty "alt" to the data)', () => {
 				setModelData( model, '<image src="/assets/sample.png" alt="alt text"></image>' );
 				setModelData( model, '<image src="/assets/sample.png" alt="alt text"></image>' );
 				const image = doc.getRoot().getChild( 0 );
 				const image = doc.getRoot().getChild( 0 );
 
 
@@ -466,8 +466,9 @@ describe( 'ImageEditing', () => {
 					writer.removeAttribute( 'alt', image );
 					writer.removeAttribute( 'alt', image );
 				} );
 				} );
 
 
-				expect( getViewData( view, { withoutSelection: true } ) )
-					.to.equal( '<figure class="ck-widget image" contenteditable="false"><img src="/assets/sample.png"></img></figure>' );
+				expect( getViewData( view, { withoutSelection: true } ) ).to.equal(
+					'<figure class="ck-widget image" contenteditable="false"><img alt="" src="/assets/sample.png"></img></figure>'
+				);
 			} );
 			} );
 
 
 			it( 'should not convert change if is already consumed', () => {
 			it( 'should not convert change if is already consumed', () => {

+ 55 - 2
packages/ckeditor5-image/tests/image/utils.js

@@ -7,18 +7,27 @@ import ViewDocumentFragment from '@ckeditor/ckeditor5-engine/src/view/documentfr
 import ViewDowncastWriter from '@ckeditor/ckeditor5-engine/src/view/downcastwriter';
 import ViewDowncastWriter from '@ckeditor/ckeditor5-engine/src/view/downcastwriter';
 import ViewDocument from '@ckeditor/ckeditor5-engine/src/view/document';
 import ViewDocument from '@ckeditor/ckeditor5-engine/src/view/document';
 import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element';
 import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element';
-import { toImageWidget, isImageWidget, getSelectedImageWidget, isImage, isImageAllowed, insertImage } from '../../src/image/utils';
+import {
+	toImageWidget,
+	isImageWidget,
+	getSelectedImageWidget,
+	isImage,
+	isImageAllowed,
+	insertImage,
+	getViewImgFromWidget
+} from '../../src/image/utils';
 import { isWidget, getLabel } from '@ckeditor/ckeditor5-widget/src/utils';
 import { isWidget, getLabel } from '@ckeditor/ckeditor5-widget/src/utils';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
 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';
 import Image from '../../src/image/imageediting';
 import Image from '../../src/image/imageediting';
+import { StylesProcessor } from '@ckeditor/ckeditor5-engine/src/view/stylesmap';
 
 
 describe( 'image widget utils', () => {
 describe( 'image widget utils', () => {
 	let element, image, writer, viewDocument;
 	let element, image, writer, viewDocument;
 
 
 	beforeEach( () => {
 	beforeEach( () => {
-		viewDocument = new ViewDocument();
+		viewDocument = new ViewDocument( new StylesProcessor() );
 		writer = new ViewDowncastWriter( viewDocument );
 		writer = new ViewDowncastWriter( viewDocument );
 		image = writer.createContainerElement( 'img' );
 		image = writer.createContainerElement( 'img' );
 		element = writer.createContainerElement( 'figure' );
 		element = writer.createContainerElement( 'figure' );
@@ -266,4 +275,48 @@ describe( 'image widget utils', () => {
 			expect( getModelData( model ) ).to.equal( '<other>[]</other>' );
 			expect( getModelData( model ) ).to.equal( '<other>[]</other>' );
 		} );
 		} );
 	} );
 	} );
+
+	describe( 'getViewImgFromWidget()', () => {
+		// figure
+		//   img
+		it( 'returns the the img element from widget if the img is the first children', () => {
+			expect( getViewImgFromWidget( element ) ).to.equal( image );
+		} );
+
+		// figure
+		//   div
+		//   img
+		it( 'returns the the img element from widget if the img is not the first children', () => {
+			writer.insert( writer.createPositionAt( element, 0 ), writer.createContainerElement( 'div' ) );
+			expect( getViewImgFromWidget( element ) ).to.equal( image );
+		} );
+
+		// figure
+		//   div
+		//     img
+		it( 'returns the the img element from widget if the img is a child of another element', () => {
+			const divElement = writer.createContainerElement( 'div' );
+
+			writer.insert( writer.createPositionAt( element, 0 ), divElement );
+			writer.move( writer.createRangeOn( image ), writer.createPositionAt( divElement, 0 ) );
+
+			expect( getViewImgFromWidget( element ) ).to.equal( image );
+		} );
+
+		// figure
+		//   div
+		//     "Bar"
+		//     img
+		//   "Foo"
+		it( 'does not throw an error if text node found', () => {
+			const divElement = writer.createContainerElement( 'div' );
+
+			writer.insert( writer.createPositionAt( element, 0 ), divElement );
+			writer.insert( writer.createPositionAt( element, 0 ), writer.createText( 'Foo' ) );
+			writer.insert( writer.createPositionAt( divElement, 0 ), writer.createText( 'Bar' ) );
+			writer.move( writer.createRangeOn( image ), writer.createPositionAt( divElement, 1 ) );
+
+			expect( getViewImgFromWidget( element ) ).to.equal( image );
+		} );
+	} );
 } );
 } );

+ 4 - 1
packages/ckeditor5-image/tests/imagetoolbar.js

@@ -3,7 +3,7 @@
  * 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
  */
  */
 
 
-/* global document */
+/* global document, console */
 
 
 import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
 import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
 import ImageToolbar from '../src/imagetoolbar';
 import ImageToolbar from '../src/imagetoolbar';
@@ -53,6 +53,7 @@ describe( 'ImageToolbar', () => {
 	} );
 	} );
 
 
 	it( 'should not initialize if there is no configuration', () => {
 	it( 'should not initialize if there is no configuration', () => {
+		const consoleWarnStub = sinon.stub( console, 'warn' );
 		const editorElement = global.document.createElement( 'div' );
 		const editorElement = global.document.createElement( 'div' );
 		global.document.body.appendChild( editorElement );
 		global.document.body.appendChild( editorElement );
 
 
@@ -61,6 +62,8 @@ describe( 'ImageToolbar', () => {
 		} )
 		} )
 			.then( editor => {
 			.then( editor => {
 				expect( editor.plugins.get( ImageToolbar )._toolbar ).to.be.undefined;
 				expect( editor.plugins.get( ImageToolbar )._toolbar ).to.be.undefined;
+				expect( consoleWarnStub.calledOnce ).to.equal( true );
+				expect( consoleWarnStub.firstCall.args[ 0 ] ).to.match( /^widget-toolbar-no-items:/ );
 
 
 				editorElement.remove();
 				editorElement.remove();
 				return editor.destroy();
 				return editor.destroy();

+ 5 - 1
packages/ckeditor5-image/tests/integration.js

@@ -12,6 +12,7 @@ import Image from '../src/image';
 import ImageToolbar from '../src/imagetoolbar';
 import ImageToolbar from '../src/imagetoolbar';
 import View from '@ckeditor/ckeditor5-ui/src/view';
 import View from '@ckeditor/ckeditor5-ui/src/view';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+import ImageTextAlternative from '../src/imagetextalternative';
 
 
 describe( 'ImageToolbar integration', () => {
 describe( 'ImageToolbar integration', () => {
 	describe( 'with the BalloonToolbar', () => {
 	describe( 'with the BalloonToolbar', () => {
@@ -25,7 +26,10 @@ describe( 'ImageToolbar integration', () => {
 
 
 			return ClassicTestEditor
 			return ClassicTestEditor
 				.create( editorElement, {
 				.create( editorElement, {
-					plugins: [ Image, ImageToolbar, BalloonToolbar, Paragraph ]
+					plugins: [ Image, ImageTextAlternative, ImageToolbar, BalloonToolbar, Paragraph ],
+					image: {
+						toolbar: [ 'imageTextAlternative' ]
+					}
 				} )
 				} )
 				.then( editor => {
 				.then( editor => {
 					newEditor = editor;
 					newEditor = editor;

+ 24 - 0
packages/ckeditor5-link/docs/features/link.md

@@ -147,6 +147,30 @@ ClassicEditor
 	.catch( ... );
 	.catch( ... );
 ```
 ```
 
 
+#### Adding default link protocol for the external links
+
+Default link protocol can be usefull when user forget to type a full URL address to an external source, site etc. Sometimes copying the text, like for example `ckeditor.com` and converting it to a link may cause some issues. When you do this, the created link will direct you to `yourdomain.com/ckeditor.com`, because you forgot to pass the right protocol which makes the link relative to the site where it appears.
+
+Enabling the `{@link module:link/link~LinkConfig#defaultProtocol config.link.defaultProtocol}`, the {@link module:link/link~Link} feature will handle this issue for you. By default it doesn't fix the passed link value, but when you set `{@link module:link/link~LinkConfig#defaultProtocol config.link.defaultProtocol}` to — for example — `http://`, the plugin will add the given protocol to the every link that may need it (like `ckeditor.com`, `example.com` etc. where `[protocol://]example.com` is missing). Here's the basic configuration example:
+
+```js
+ClassicEditor
+	.create( document.querySelector( '#editor' ), {
+		// ...
+		link: {
+			defaultProtocol: 'http://'
+		}
+	} )
+	.then( ... )
+	.catch( ... );
+```
+
+<info-box>
+	Having `config.link.defaultProtocol` enabled you are still able to link things locally using `#` or `/`. Protocol won't be added to those links.
+
+	Enabled feature also gives you an **email addresses auto-detection** feature. When you submit `hello@example.com`, the plugin will change it automatically to `mailto:hello@example.com`.
+</info-box>
+
 #### Adding attributes to links based on pre–defined rules (automatic decorators)
 #### Adding attributes to links based on pre–defined rules (automatic decorators)
 
 
 Automatic link decorators match all links in the editor content against a {@link module:link/link~LinkDecoratorAutomaticDefinition function} which decides whether the link should receive some set of attributes, considering the URL (`href`) of the link. These decorators work silently and are being applied during the {@link framework/guides/architecture/editing-engine#conversion data downcast} only.
 Automatic link decorators match all links in the editor content against a {@link module:link/link~LinkDecoratorAutomaticDefinition function} which decides whether the link should receive some set of attributes, considering the URL (`href`) of the link. These decorators work silently and are being applied during the {@link framework/guides/architecture/editing-engine#conversion data downcast} only.

+ 2 - 0
packages/ckeditor5-link/package.json

@@ -16,10 +16,12 @@
     "lodash-es": "^4.17.15"
     "lodash-es": "^4.17.15"
   },
   },
   "devDependencies": {
   "devDependencies": {
+    "@ckeditor/ckeditor5-basic-styles": "^19.0.1",
     "@ckeditor/ckeditor5-block-quote": "^19.0.1",
     "@ckeditor/ckeditor5-block-quote": "^19.0.1",
     "@ckeditor/ckeditor5-clipboard": "^19.0.1",
     "@ckeditor/ckeditor5-clipboard": "^19.0.1",
     "@ckeditor/ckeditor5-editor-classic": "^19.0.1",
     "@ckeditor/ckeditor5-editor-classic": "^19.0.1",
     "@ckeditor/ckeditor5-enter": "^19.0.1",
     "@ckeditor/ckeditor5-enter": "^19.0.1",
+    "@ckeditor/ckeditor5-image": "^19.0.1",
     "@ckeditor/ckeditor5-paragraph": "^19.1.0",
     "@ckeditor/ckeditor5-paragraph": "^19.1.0",
     "@ckeditor/ckeditor5-theme-lark": "^19.1.0",
     "@ckeditor/ckeditor5-theme-lark": "^19.1.0",
     "@ckeditor/ckeditor5-typing": "^19.0.1",
     "@ckeditor/ckeditor5-typing": "^19.0.1",

+ 22 - 0
packages/ckeditor5-link/src/link.js

@@ -57,6 +57,28 @@ export default class Link extends Plugin {
  * @interface LinkConfig
  * @interface LinkConfig
  */
  */
 
 
+/**
+ * When set, the editor will add the given protocol to the link when the user creates a link without one.
+ * For example, when the user is creating a link and types `ckeditor.com` in the link form input — during link submission —
+ * the editor will automatically add the `http://` protocol, so the link will be as follows: `http://ckeditor.com`.
+ *
+ * The feature also comes with an email auto-detection. When you submit `hello@example.com`
+ * the plugin will automatically change it to `mailto:hello@example.com`.
+ *
+ * 		ClassicEditor
+ *			.create( editorElement, {
+ * 				link: {
+ * 					defaultProtocol: 'http://'
+ * 				}
+ *			} )
+ *			.then( ... )
+ *			.catch( ... );
+ *
+ * **NOTE:** In case no configuration is provided, the editor won't auto-fix the links.
+ *
+ * @member {String} module:link/link~LinkConfig#defaultProtocol
+ */
+
 /**
 /**
  * When set to `true`, the `target="blank"` and `rel="noopener noreferrer"` attributes are automatically added to all external links
  * When set to `true`, the `target="blank"` and `rel="noopener noreferrer"` attributes are automatically added to all external links
  * in the editor. "External links" are all links in the editor content starting with `http`, `https`, or `//`.
  * in the editor. "External links" are all links in the editor content starting with `http`, `https`, or `//`.

+ 40 - 1
packages/ckeditor5-link/src/linkcommand.js

@@ -187,10 +187,30 @@ export default class LinkCommand extends Command {
 				}
 				}
 			} else {
 			} else {
 				// If selection has non-collapsed ranges, we change attribute on nodes inside those ranges
 				// If selection has non-collapsed ranges, we change attribute on nodes inside those ranges
-				// omitting nodes where `linkHref` attribute is disallowed.
+				// omitting nodes where the `linkHref` attribute is disallowed.
 				const ranges = model.schema.getValidRanges( selection.getRanges(), 'linkHref' );
 				const ranges = model.schema.getValidRanges( selection.getRanges(), 'linkHref' );
 
 
+				// But for the first, check whether the `linkHref` attribute is allowed on selected blocks (e.g. the "image" element).
+				const allowedRanges = [];
+
+				for ( const element of selection.getSelectedBlocks() ) {
+					if ( model.schema.checkAttribute( element, 'linkHref' ) ) {
+						allowedRanges.push( writer.createRangeOn( element ) );
+					}
+				}
+
+				// Ranges that accept the `linkHref` attribute. Since we will iterate over `allowedRanges`, let's clone it.
+				const rangesToUpdate = allowedRanges.slice();
+
+				// For all selection ranges we want to check whether given range is inside an element that accepts the `linkHref` attribute.
+				// If so, we don't want to propagate applying the attribute to its children.
 				for ( const range of ranges ) {
 				for ( const range of ranges ) {
+					if ( this._isRangeToUpdate( range, allowedRanges ) ) {
+						rangesToUpdate.push( range );
+					}
+				}
+
+				for ( const range of rangesToUpdate ) {
 					writer.setAttribute( 'linkHref', href, range );
 					writer.setAttribute( 'linkHref', href, range );
 
 
 					truthyManualDecorators.forEach( item => {
 					truthyManualDecorators.forEach( item => {
@@ -216,4 +236,23 @@ export default class LinkCommand extends Command {
 		const doc = this.editor.model.document;
 		const doc = this.editor.model.document;
 		return doc.selection.getAttribute( decoratorName );
 		return doc.selection.getAttribute( decoratorName );
 	}
 	}
+
+	/**
+	 * Checks whether specified `range` is inside an element that accepts the `linkHref` attribute.
+	 *
+	 * @private
+	 * @param {module:engine/view/range~Range} range A range to check.
+	 * @param {Array.<module:engine/view/range~Range>} allowedRanges An array of ranges created on elements where the attribute is accepted.
+	 * @returns {Boolean}
+	 */
+	_isRangeToUpdate( range, allowedRanges ) {
+		for ( const allowedRange of allowedRanges ) {
+			// A range is inside an element that will have the `linkHref` attribute. Do not modify its nodes.
+			if ( allowedRange.containsRange( range ) ) {
+				return false;
+			}
+		}
+
+		return true;
+	}
 }
 }

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

@@ -0,0 +1,36 @@
+/**
+ * @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 link/linkimage
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import LinkImageEditing from './linkimageediting';
+import LinkImageUI from './linkimageui';
+
+/**
+ * The `LinkImage` plugin.
+ *
+ * This is a "glue" plugin that loads the {@link module:link/linkimageediting~LinkImageEditing link image editing feature}
+ * and {@link module:link/linkimageui~LinkImageUI linkimage UI feature}.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class LinkImage extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get requires() {
+		return [ LinkImageEditing, LinkImageUI ];
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static get pluginName() {
+		return 'LinkImage';
+	}
+}

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

@@ -0,0 +1,141 @@
+/**
+ * @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 link/linkimageediting
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import ImageEditing from '@ckeditor/ckeditor5-image/src/image/imageediting';
+import LinkEditing from './linkediting';
+
+/**
+ * The link image engine feature.
+ *
+ * It accepts the `linkHref="url"` attribute in the model for the {@link module:image/image~Image `<image>`} element
+ * which allows linking images.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class LinkImageEditing extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get requires() {
+		return [ ImageEditing, LinkEditing ];
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static get pluginName() {
+		return 'LinkImageEditing';
+	}
+
+	init() {
+		const editor = this.editor;
+
+		editor.model.schema.extend( 'image', { allowAttributes: [ 'linkHref' ] } );
+
+		editor.conversion.for( 'upcast' ).add( upcastLink() );
+		editor.conversion.for( 'downcast' ).add( downcastImageLink() );
+	}
+}
+
+// Returns a converter that consumes the 'href' attribute if a link contains an image.
+//
+// @private
+// @returns {Function}
+//
+function upcastLink() {
+	return dispatcher => {
+		dispatcher.on( 'element:a', ( evt, data, conversionApi ) => {
+			const viewLink = data.viewItem;
+			const imageInLink = Array.from( viewLink.getChildren() ).find( child => child.name === 'img' );
+
+			if ( !imageInLink ) {
+				return;
+			}
+
+			// There's an image inside an <a> element - we consume it so it won't be picked up by the Link plugin.
+			const consumableAttributes = { attributes: [ 'href' ] };
+
+			// Consume the `href` attribute so the default one will not convert it to $text attribute.
+			if ( !conversionApi.consumable.consume( viewLink, consumableAttributes ) ) {
+				// Might be consumed by something else - i.e. other converter with priority=highest - a standard check.
+				return;
+			}
+
+			const linkHref = viewLink.getAttribute( 'href' );
+
+			// Missing the 'href' attribute.
+			if ( !linkHref ) {
+				return;
+			}
+
+			// A full definition of the image feature.
+			// figure > a > img: parent of the link element is an image element.
+			let modelElement = data.modelCursor.parent;
+
+			if ( !modelElement.is( 'image' ) ) {
+				// a > img: parent of the link is not the image element. We need to convert it manually.
+				const conversionResult = conversionApi.convertItem( imageInLink, data.modelCursor );
+
+				// Set image range as conversion result.
+				data.modelRange = conversionResult.modelRange;
+
+				// Continue conversion where image conversion ends.
+				data.modelCursor = conversionResult.modelCursor;
+
+				modelElement = data.modelCursor.nodeBefore;
+			}
+
+			if ( modelElement && modelElement.is( 'image' ) ) {
+				// Set the linkHref attribute from link element on model image element.
+				conversionApi.writer.setAttribute( 'linkHref', linkHref, modelElement );
+			}
+		}, { priority: 'high' } );
+	};
+}
+
+// Return a converter that adds the `<a>` element to data.
+//
+// @private
+// @returns {Function}
+//
+function downcastImageLink() {
+	return dispatcher => {
+		dispatcher.on( 'attribute:linkHref:image', ( evt, data, conversionApi ) => {
+			// The image will be already converted - so it will be present in the view.
+			const viewFigure = conversionApi.mapper.toViewElement( data.item );
+			const writer = conversionApi.writer;
+
+			// But we need to check whether the link element exists.
+			const linkInImage = Array.from( viewFigure.getChildren() ).find( child => child.name === 'a' );
+
+			// If so, update the attribute if it's defined or remove the entire link if the attribute is empty.
+			if ( linkInImage ) {
+				if ( data.attributeNewValue ) {
+					writer.setAttribute( 'href', data.attributeNewValue, linkInImage );
+				} else {
+					const viewImage = Array.from( linkInImage.getChildren() ).find( child => child.name === 'img' );
+
+					writer.move( writer.createRangeOn( viewImage ), writer.createPositionAt( viewFigure, 0 ) );
+					writer.remove( linkInImage );
+				}
+			} else {
+				// But if it does not exist. Let's wrap already converted image by newly created link element.
+				// 1. Create an empty link element.
+				const linkElement = writer.createContainerElement( 'a', { href: data.attributeNewValue } );
+
+				// 2. Insert link inside the associated image.
+				writer.insert( writer.createPositionAt( viewFigure, 0 ), linkElement );
+
+				// 3. Move the image to the link.
+				writer.move( writer.createRangeOn( viewFigure.getChild( 1 ) ), writer.createPositionAt( linkElement, 0 ) );
+			}
+		} );
+	};
+}

+ 39 - 0
packages/ckeditor5-link/src/linkimageui.js

@@ -0,0 +1,39 @@
+/**
+ * @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 link/linkimageui
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import Image from '@ckeditor/ckeditor5-image/src/image';
+import LinkUI from './linkui';
+import LinkEditing from './linkediting';
+
+/**
+ * The link image UI plugin.
+ *
+ * TODO: Docs.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class LinkImageUI extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get requires() {
+		return [ Image, LinkEditing, LinkUI ];
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static get pluginName() {
+		return 'LinkImageUI';
+	}
+
+	init() {
+	}
+}

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

@@ -21,6 +21,8 @@ import LinkActionsView from './ui/linkactionsview';
 import linkIcon from '../theme/icons/link.svg';
 import linkIcon from '../theme/icons/link.svg';
 
 
 const linkKeystroke = 'Ctrl+K';
 const linkKeystroke = 'Ctrl+K';
+const protocolRegExp = /^((\w+:(\/{2,})?)|(\W))/i;
+const emailRegExp = /[\w-]+@[\w-]+\.+[\w-]+/i;
 
 
 /**
 /**
  * The link UI plugin. It introduces the `'link'` and `'unlink'` buttons and support for the <kbd>Ctrl+K</kbd> keystroke.
  * The link UI plugin. It introduces the `'link'` and `'unlink'` buttons and support for the <kbd>Ctrl+K</kbd> keystroke.
@@ -143,8 +145,9 @@ export default class LinkUI extends Plugin {
 	_createFormView() {
 	_createFormView() {
 		const editor = this.editor;
 		const editor = this.editor;
 		const linkCommand = editor.commands.get( 'link' );
 		const linkCommand = editor.commands.get( 'link' );
+		const defaultProtocol = editor.config.get( 'link.defaultProtocol' );
 
 
-		const formView = new LinkFormView( editor.locale, linkCommand );
+		const formView = new LinkFormView( editor.locale, linkCommand, defaultProtocol );
 
 
 		formView.urlInputView.fieldView.bind( 'value' ).to( linkCommand, 'value' );
 		formView.urlInputView.fieldView.bind( 'value' ).to( linkCommand, 'value' );
 
 
@@ -154,7 +157,17 @@ export default class LinkUI extends Plugin {
 
 
 		// Execute link command after clicking the "Save" button.
 		// Execute link command after clicking the "Save" button.
 		this.listenTo( formView, 'submit', () => {
 		this.listenTo( formView, 'submit', () => {
-			editor.execute( 'link', formView.urlInputView.fieldView.element.value, formView.getDecoratorSwitchesState() );
+			const { value } = formView.urlInputView.fieldView.element;
+
+			// The regex checks for the protocol syntax ('xxxx://' or 'xxxx:')
+			// or non-word charecters at the begining of the link ('/', '#' etc.).
+			const isProtocolNeeded = !!defaultProtocol && !protocolRegExp.test( value );
+			const isEmail = emailRegExp.test( value );
+
+			const protocol = isEmail ? 'mailto:' : defaultProtocol;
+			const parsedValue = value && isProtocolNeeded ? protocol + value : value;
+
+			editor.execute( 'link', parsedValue, formView.getDecoratorSwitchesState() );
 			this._closeFormView();
 			this._closeFormView();
 		} );
 		} );
 
 

+ 6 - 5
packages/ckeditor5-link/src/ui/linkformview.js

@@ -40,8 +40,9 @@ export default class LinkFormView extends View {
 	 *
 	 *
 	 * @param {module:utils/locale~Locale} [locale] The localization services instance.
 	 * @param {module:utils/locale~Locale} [locale] The localization services instance.
 	 * @param {module:link/linkcommand~LinkCommand} linkCommand Reference to {@link module:link/linkcommand~LinkCommand}.
 	 * @param {module:link/linkcommand~LinkCommand} linkCommand Reference to {@link module:link/linkcommand~LinkCommand}.
+	 * @param {String} [protocol] A value of a protocol to be displayed in the input's placeholder.
 	 */
 	 */
-	constructor( locale, linkCommand ) {
+	constructor( locale, linkCommand, protocol ) {
 		super( locale );
 		super( locale );
 
 
 		const t = locale.t;
 		const t = locale.t;
@@ -67,7 +68,7 @@ export default class LinkFormView extends View {
 		 *
 		 *
 		 * @member {module:ui/labeledfield/labeledfieldview~LabeledFieldView}
 		 * @member {module:ui/labeledfield/labeledfieldview~LabeledFieldView}
 		 */
 		 */
-		this.urlInputView = this._createUrlInput();
+		this.urlInputView = this._createUrlInput( protocol );
 
 
 		/**
 		/**
 		 * The Save button view.
 		 * The Save button view.
@@ -207,15 +208,15 @@ export default class LinkFormView extends View {
 	 * Creates a labeled input view.
 	 * Creates a labeled input view.
 	 *
 	 *
 	 * @private
 	 * @private
+	 * @param {String} [protocol=http://] A value of a protocol to be displayed in the input's placeholder.
 	 * @returns {module:ui/labeledfield/labeledfieldview~LabeledFieldView} Labeled field view instance.
 	 * @returns {module:ui/labeledfield/labeledfieldview~LabeledFieldView} Labeled field view instance.
 	 */
 	 */
-	_createUrlInput() {
+	_createUrlInput( protocol = 'https://' ) {
 		const t = this.locale.t;
 		const t = this.locale.t;
-
 		const labeledInput = new LabeledFieldView( this.locale, createLabeledInputText );
 		const labeledInput = new LabeledFieldView( this.locale, createLabeledInputText );
 
 
 		labeledInput.label = t( 'Link URL' );
 		labeledInput.label = t( 'Link URL' );
-		labeledInput.fieldView.placeholder = 'https://example.com';
+		labeledInput.fieldView.placeholder = protocol + 'example.com';
 
 
 		return labeledInput;
 		return labeledInput;
 	}
 	}

+ 62 - 3
packages/ckeditor5-link/tests/linkcommand.js

@@ -193,8 +193,8 @@ describe( 'LinkCommand', () => {
 				expect( command.value ).to.equal( 'url' );
 				expect( command.value ).to.equal( 'url' );
 			} );
 			} );
 
 
-			it( 'should set `linkHref` attribute only to allowed elements and omit disallowed', () => {
-				model.schema.register( 'img', { allowWhere: '$text' } );
+			it( 'should set `linkHref` attribute to allowed elements', () => {
+				model.schema.register( 'img', { isBlock: true, allowWhere: '$text', allowAttributes: [ 'linkHref' ] } );
 
 
 				setData( model, '<p>f[oo<img></img>ba]r</p>' );
 				setData( model, '<p>f[oo<img></img>ba]r</p>' );
 
 
@@ -203,9 +203,68 @@ describe( 'LinkCommand', () => {
 				command.execute( 'url' );
 				command.execute( 'url' );
 
 
 				expect( getData( model ) )
 				expect( getData( model ) )
-					.to.equal( '<p>f[<$text linkHref="url">oo</$text><img></img><$text linkHref="url">ba</$text>]r</p>' );
+					.to.equal( '<p>f[<$text linkHref="url">oo</$text><img linkHref="url"></img><$text linkHref="url">ba</$text>]r</p>' );
 				expect( command.value ).to.equal( 'url' );
 				expect( command.value ).to.equal( 'url' );
 			} );
 			} );
+
+			it( 'should set `linkHref` attribute to nested allowed elements', () => {
+				model.schema.register( 'img', { isBlock: true, allowWhere: '$text', allowAttributes: [ 'linkHref' ] } );
+				model.schema.register( 'blockQuote', { allowWhere: '$block', allowContentOf: '$root' } );
+
+				setData( model, '<p>foo</p>[<blockQuote><img></img></blockQuote>]<p>bar</p>' );
+
+				command.execute( 'url' );
+
+				expect( getData( model ) )
+					.to.equal( '<p>foo</p>[<blockQuote><img linkHref="url"></img></blockQuote>]<p>bar</p>' );
+			} );
+
+			it( 'should set `linkHref` attribute to allowed elements on multi-selection', () => {
+				model.schema.register( 'img', { isBlock: true, allowWhere: '$text', allowAttributes: [ 'linkHref' ] } );
+
+				setData( model, '<p>[<img></img>][<img></img>]</p>' );
+
+				command.execute( 'url' );
+
+				expect( getData( model ) )
+					.to.equal( '<p>[<img linkHref="url"></img>][<img linkHref="url"></img>]</p>' );
+			} );
+
+			it( 'should set `linkHref` attribute to allowed elements and omit disallowed', () => {
+				model.schema.register( 'img', { isBlock: true, allowWhere: '$text' } );
+				model.schema.register( 'caption', { allowIn: 'img' } );
+				model.schema.extend( '$text', { allowIn: 'caption' } );
+
+				setData( model, '<p>f[oo<img><caption>xxx</caption></img>ba]r</p>' );
+
+				command.execute( 'url' );
+
+				expect( getData( model ) ).to.equal(
+					'<p>' +
+						'f[<$text linkHref="url">oo</$text>' +
+						'<img><caption><$text linkHref="url">xxx</$text></caption></img>' +
+						'<$text linkHref="url">ba</$text>]r' +
+					'</p>'
+				);
+			} );
+
+			it( 'should set `linkHref` attribute to allowed elements and omit their children even if they accept the attribute', () => {
+				model.schema.register( 'img', { isBlock: true, allowWhere: '$text', allowAttributes: [ 'linkHref' ] } );
+				model.schema.register( 'caption', { allowIn: 'img' } );
+				model.schema.extend( '$text', { allowIn: 'caption' } );
+
+				setData( model, '<p>f[oo<img><caption>xxx</caption></img>ba]r</p>' );
+
+				command.execute( 'url' );
+
+				expect( getData( model ) ).to.equal(
+					'<p>' +
+						'f[<$text linkHref="url">oo</$text>' +
+						'<img linkHref="url"><caption>xxx</caption></img>' +
+						'<$text linkHref="url">ba</$text>]r' +
+					'</p>'
+				);
+			} );
 		} );
 		} );
 
 
 		describe( 'collapsed selection', () => {
 		describe( 'collapsed selection', () => {

+ 18 - 0
packages/ckeditor5-link/tests/linkimage.js

@@ -0,0 +1,18 @@
+/**
+ * @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 LinkImage from '../src/linkimage';
+import LinkImageEditing from '../src/linkimageediting';
+import LinkImageUI from '../src/linkimageui';
+
+describe( 'LinkImage', () => {
+	it( 'should require LinkImageEditing and LinkImageUI', () => {
+		expect( LinkImage.requires ).to.deep.equal( [ LinkImageEditing, LinkImageUI ] );
+	} );
+
+	it( 'should be named', () => {
+		expect( LinkImage.pluginName ).to.equal( 'LinkImage' );
+	} );
+} );

+ 334 - 0
packages/ckeditor5-link/tests/linkimageediting.js

@@ -0,0 +1,334 @@
+/**
+ * @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 VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import LinkImageEditing from '../src/linkimageediting';
+import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+import normalizeHtml from '@ckeditor/ckeditor5-utils/tests/_utils/normalizehtml';
+import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
+import ImageCaptionEditing from '@ckeditor/ckeditor5-image/src/imagecaption/imagecaptionediting';
+
+describe( 'LinkImageEditing', () => {
+	let editor, model, view;
+
+	beforeEach( () => {
+		return VirtualTestEditor
+			.create( {
+				plugins: [ Paragraph, LinkImageEditing ]
+			} )
+			.then( newEditor => {
+				editor = newEditor;
+				model = editor.model;
+				view = editor.editing.view;
+			} );
+	} );
+
+	afterEach( () => {
+		return editor.destroy();
+	} );
+
+	it( 'should have pluginName', () => {
+		expect( LinkImageEditing.pluginName ).to.equal( 'LinkImageEditing' );
+	} );
+
+	it( 'should be loaded', () => {
+		expect( editor.plugins.get( LinkImageEditing ) ).to.be.instanceOf( LinkImageEditing );
+	} );
+
+	it( 'should set proper schema rules', () => {
+		expect( model.schema.checkAttribute( [ '$root', 'image' ], 'linkHref' ) ).to.be.true;
+	} );
+
+	describe( 'conversion in data pipeline', () => {
+		describe( 'model to view', () => {
+			it( 'should convert an image with a link', () => {
+				setModelData( model, '<image src="/assets/sample.png" alt="alt text" linkHref="http://ckeditor.com"></image>' );
+
+				expect( editor.getData() ).to.equal(
+					'<figure class="image"><a href="http://ckeditor.com"><img alt="alt text" src="/assets/sample.png"></a></figure>'
+				);
+			} );
+
+			it( 'should convert an image with a link and without alt attribute', () => {
+				setModelData( model, '<image src="/assets/sample.png" linkHref="http://ckeditor.com"></image>' );
+
+				expect( editor.getData() ).to.equal(
+					'<figure class="image"><a href="http://ckeditor.com"><img src="/assets/sample.png"></a></figure>'
+				);
+			} );
+
+			it( 'should convert srcset attribute to srcset and sizes attribute wrapped into a link', () => {
+				setModelData( model,
+					'<image src="/assets/sample.png" ' +
+						'linkHref="http://ckeditor.com" ' +
+						'srcset=\'{ "data": "small.png 148w, big.png 1024w" }\'>' +
+					'</image>'
+				);
+
+				expect( normalizeHtml( editor.getData() ) ).to.equal(
+					'<figure class="image">' +
+						'<a href="http://ckeditor.com">' +
+							'<img sizes="100vw" src="/assets/sample.png" srcset="small.png 148w, big.png 1024w"></img>' +
+						'</a>' +
+					'</figure>'
+				);
+			} );
+		} );
+
+		describe( 'view to model', () => {
+			describe( 'figure > a > img', () => {
+				it( 'should convert a link in an image figure', () => {
+					editor.setData(
+						'<figure class="image"><a href="http://ckeditor.com"><img src="/assets/sample.png" alt="alt text" /></a></figure>'
+					);
+
+					expect( getModelData( model, { withoutSelection: true } ) )
+						.to.equal( '<image alt="alt text" linkHref="http://ckeditor.com" src="/assets/sample.png"></image>' );
+				} );
+
+				it( 'should convert an image with a link and without alt attribute', () => {
+					editor.setData( '<figure class="image"><a href="http://ckeditor.com"><img src="/assets/sample.png" /></a></figure>' );
+
+					expect( getModelData( model, { withoutSelection: true } ) )
+						.to.equal( '<image linkHref="http://ckeditor.com" src="/assets/sample.png"></image>' );
+				} );
+
+				it( 'should not convert without src attribute', () => {
+					editor.setData( '<figure class="image"><a href="http://ckeditor.com"><img alt="alt text" /></a></figure>' );
+
+					expect( getModelData( model, { withoutSelection: true } ) )
+						.to.equal( '<paragraph></paragraph>' );
+				} );
+
+				it( 'should not convert in wrong context', () => {
+					model.schema.register( 'div', { inheritAllFrom: '$block' } );
+					model.schema.addChildCheck( ( ctx, childDef ) => {
+						if ( ctx.endsWith( '$root' ) && childDef.name == 'image' ) {
+							return false;
+						}
+					} );
+
+					editor.conversion.elementToElement( { model: 'div', view: 'div' } );
+
+					editor.setData(
+						'<div>' +
+							'<figure class="image">' +
+								'<a href="http://ckeditor.com">' +
+									'<img src="/assets/sample.png" alt="alt text" />' +
+								'</a>' +
+							'</figure>' +
+						'</div>' );
+
+					expect( getModelData( model, { withoutSelection: true } ) )
+						.to.equal( '<div></div>' );
+				} );
+
+				it( 'should not convert "a" element if is already consumed', () => {
+					editor.data.upcastDispatcher.on( 'element:a', ( evt, data, conversionApi ) => {
+						conversionApi.consumable.consume( data.viewItem, { attributes: [ 'href' ] } );
+					}, { priority: 'highest' } );
+
+					editor.setData(
+						'<figure class="image"><a href="http://ckeditor.com"><img src="/assets/sample.png" alt="alt text" /></a></figure>'
+					);
+
+					expect( editor.getData() ).to.equal( '<figure class="image"><img src="/assets/sample.png" alt="alt text"></figure>' );
+				} );
+
+				it( 'should not convert if a link misses "href" attribute', () => {
+					editor.setData(
+						'<figure class="image"><a href=""><img src="/assets/sample.png" alt="alt text" /></a></figure>'
+					);
+
+					expect( getModelData( model, { withoutSelection: true } ) )
+						.to.equal( '<image alt="alt text" src="/assets/sample.png"></image>' );
+				} );
+
+				it( 'should convert a link without an image to a paragraph with the link', () => {
+					editor.setData(
+						'<figure class="image"><a href="http://ckeditor.com">Foo</a></figure>'
+					);
+
+					expect( getModelData( model, { withoutSelection: true } ) )
+						.to.equal( '<paragraph><$text linkHref="http://ckeditor.com">Foo</$text></paragraph>' );
+				} );
+			} );
+
+			describe( 'a > img', () => {
+				it( 'should convert a link in an image figure', () => {
+					editor.setData(
+						'<a href="http://ckeditor.com"><img src="/assets/sample.png" alt="alt text" /></a>'
+					);
+
+					expect( getModelData( model, { withoutSelection: true } ) )
+						.to.equal( '<image alt="alt text" linkHref="http://ckeditor.com" src="/assets/sample.png"></image>' );
+				} );
+
+				it( 'should convert an image with a link and without alt attribute', () => {
+					editor.setData( '<a href="http://ckeditor.com"><img src="/assets/sample.png" /></a>' );
+
+					expect( getModelData( model, { withoutSelection: true } ) )
+						.to.equal( '<image linkHref="http://ckeditor.com" src="/assets/sample.png"></image>' );
+				} );
+
+				it( 'should not convert without src attribute', () => {
+					editor.setData( '<a href="http://ckeditor.com"><img alt="alt text" /></a>' );
+
+					expect( getModelData( model, { withoutSelection: true } ) )
+						.to.equal( '<paragraph></paragraph>' );
+				} );
+
+				it( 'should not convert in wrong context', () => {
+					model.schema.register( 'div', { inheritAllFrom: '$block' } );
+					model.schema.addChildCheck( ( ctx, childDef ) => {
+						if ( ctx.endsWith( '$root' ) && childDef.name == 'image' ) {
+							return false;
+						}
+					} );
+
+					editor.conversion.elementToElement( { model: 'div', view: 'div' } );
+
+					editor.setData(
+						'<div>' +
+							'<a href="http://ckeditor.com">' +
+								'<img src="/assets/sample.png" alt="alt text" />' +
+							'</a>' +
+						'</div>' );
+
+					expect( getModelData( model, { withoutSelection: true } ) )
+						.to.equal( '<div></div>' );
+				} );
+
+				it( 'should not convert "a" element if is already consumed', () => {
+					editor.data.upcastDispatcher.on( 'element:a', ( evt, data, conversionApi ) => {
+						conversionApi.consumable.consume( data.viewItem, { attributes: [ 'href' ] } );
+					}, { priority: 'highest' } );
+
+					editor.setData(
+						'<a href="http://ckeditor.com"><img src="/assets/sample.png" alt="alt text" /></a>'
+					);
+
+					expect( editor.getData() ).to.equal( '<figure class="image"><img src="/assets/sample.png" alt="alt text"></figure>' );
+				} );
+
+				it( 'should not convert if a link misses "href" attribute', () => {
+					editor.setData(
+						'<a href=""><img src="/assets/sample.png" alt="alt text" /></a>'
+					);
+
+					expect( getModelData( model, { withoutSelection: true } ) )
+						.to.equal( '<image alt="alt text" src="/assets/sample.png"></image>' );
+				} );
+			} );
+
+			describe( 'figure > a > img + figcaption', () => {
+				it( 'should convert a link and the caption element', () => {
+					return VirtualTestEditor
+						.create( {
+							plugins: [ Paragraph, LinkImageEditing, ImageCaptionEditing ]
+						} )
+						.then( editor => {
+							editor.setData(
+								'<figure class="image">' +
+									'<a href="http://ckeditor.com">' +
+										'<img src="/assets/sample.png" alt="alt text" />' +
+									'</a>' +
+									'<figcaption>' +
+										'Foo Bar.' +
+									'</figcaption>' +
+								'</figure>'
+							);
+
+							expect( getModelData( editor.model, { withoutSelection: true } ) ).to.equal(
+								'<image alt="alt text" linkHref="http://ckeditor.com" src="/assets/sample.png">' +
+									'<caption>Foo Bar.</caption>' +
+								'</image>'
+							);
+
+							return editor.destroy();
+						} );
+				} );
+			} );
+		} );
+	} );
+
+	describe( 'conversion in editing pipeline', () => {
+		describe( 'model to view', () => {
+			it( 'should convert the image element', () => {
+				setModelData( model, '<image linkHref="http://ckeditor.com" src="/assets/sample.png" alt="alt text"></image>' );
+
+				expect( getViewData( view, { withoutSelection: true } ) ).to.equal(
+					'<figure class="ck-widget image" contenteditable="false">' +
+						'<a href="http://ckeditor.com">' +
+							'<img alt="alt text" src="/assets/sample.png"></img>' +
+						'</a>' +
+					'</figure>'
+				);
+			} );
+
+			it( 'should convert attribute change', () => {
+				setModelData( model, '<image linkHref="http://ckeditor.com" src="/assets/sample.png" alt="alt text"></image>' );
+				const image = model.document.getRoot().getChild( 0 );
+
+				model.change( writer => {
+					writer.setAttribute( 'linkHref', 'https://ckeditor.com/why-ckeditor/', image );
+				} );
+
+				expect( getViewData( view, { withoutSelection: true } ) ).to.equal(
+					'<figure class="ck-widget image" contenteditable="false">' +
+						'<a href="https://ckeditor.com/why-ckeditor/">' +
+							'<img alt="alt text" src="/assets/sample.png"></img>' +
+						'</a>' +
+					'</figure>'
+				);
+			} );
+
+			it( 'should convert attribute removal', () => {
+				setModelData( model, '<image linkHref="http://ckeditor.com" src="/assets/sample.png" alt="alt text"></image>' );
+				const image = model.document.getRoot().getChild( 0 );
+
+				model.change( writer => {
+					writer.removeAttribute( 'linkHref', image );
+				} );
+
+				expect( getViewData( view, { withoutSelection: true } ) ).to.equal(
+					'<figure class="ck-widget image" contenteditable="false">' +
+						'<img alt="alt text" src="/assets/sample.png"></img>' +
+					'</figure>'
+				);
+			} );
+		} );
+
+		describe( 'figure > a > img + figcaption', () => {
+			it( 'should convert a link and the caption element', () => {
+				return VirtualTestEditor
+					.create( {
+						plugins: [ Paragraph, LinkImageEditing, ImageCaptionEditing ]
+					} )
+					.then( editor => {
+						setModelData( editor.model,
+							'<image linkHref="http://ckeditor.com" src="/assets/sample.png" alt="alt text">' +
+								'<caption>Foo Bar.</caption>' +
+							'</image>'
+						);
+
+						expect( getViewData( editor.editing.view, { withoutSelection: true } ) ).to.equal(
+							'<figure class="ck-widget image" contenteditable="false">' +
+								'<a href="http://ckeditor.com">' +
+									'<img alt="alt text" src="/assets/sample.png"></img>' +
+								'</a>' +
+								'<figcaption class="ck-editor__editable ck-editor__nested-editable" ' +
+									'contenteditable="true" data-placeholder="Enter image caption">' +
+										'Foo Bar.' +
+								'</figcaption>' +
+							'</figure>'
+						);
+						return editor.destroy();
+					} );
+			} );
+		} );
+	} );
+} );

+ 41 - 0
packages/ckeditor5-link/tests/linkimageui.js

@@ -0,0 +1,41 @@
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/* globals document */
+
+import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+import LinkImageUI from '../src/linkimageui';
+
+describe( 'LinkImageUI', () => {
+	let editor, editorElement;
+
+	testUtils.createSinonSandbox();
+
+	beforeEach( () => {
+		editorElement = document.createElement( 'div' );
+		document.body.appendChild( editorElement );
+
+		return ClassicTestEditor
+			.create( editorElement, {
+				plugins: [ LinkImageUI ]
+			} )
+			.then( newEditor => {
+				editor = newEditor;
+			} );
+	} );
+
+	afterEach( () => {
+		editorElement.remove();
+
+		return editor.destroy();
+	} );
+
+	describe( 'init()', () => {
+		it( 'does nothing', () => {
+			expect( editor.plugins.get( LinkImageUI ).init() ).to.equal( undefined );
+		} );
+	} );
+} );

+ 145 - 1
packages/ckeditor5-link/tests/linkui.js

@@ -8,7 +8,7 @@
 import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
 import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
 import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
-import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+import { setData as setModelData, getData as getModelData } 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 Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
@@ -891,6 +891,27 @@ describe( 'LinkUI', () => {
 	describe( 'link form view', () => {
 	describe( 'link form view', () => {
 		let focusEditableSpy;
 		let focusEditableSpy;
 
 
+		const createEditorWithDefaultProtocol = defaultProtocol => {
+			return ClassicTestEditor
+				.create( editorElement, {
+					plugins: [ LinkEditing, LinkUI, Paragraph, BlockQuote ],
+					link: { defaultProtocol }
+				} )
+				.then( editor => {
+					const linkUIFeature = editor.plugins.get( LinkUI );
+					const formView = linkUIFeature.formView;
+
+					formView.render();
+
+					editor.model.schema.extend( '$text', {
+						allowIn: '$root',
+						allowAttributes: 'linkHref'
+					} );
+
+					return { editor, formView };
+				} );
+		};
+
 		beforeEach( () => {
 		beforeEach( () => {
 			focusEditableSpy = testUtils.sinon.spy( editor.editing.view, 'focus' );
 			focusEditableSpy = testUtils.sinon.spy( editor.editing.view, 'focus' );
 		} );
 		} );
@@ -905,6 +926,129 @@ describe( 'LinkUI', () => {
 			expect( editor.ui.focusTracker.isFocused ).to.be.true;
 			expect( editor.ui.focusTracker.isFocused ).to.be.true;
 		} );
 		} );
 
 
+		describe( 'link protocol', () => {
+			it( 'should use a default link protocol from the `config.link.defaultProtocol` when provided', () => {
+				return ClassicTestEditor
+					.create( editorElement, {
+						link: {
+							defaultProtocol: 'https://'
+						}
+					} )
+					.then( editor => {
+						const defaultProtocol = editor.config.get( 'link.defaultProtocol' );
+
+						expect( defaultProtocol ).to.equal( 'https://' );
+
+						editor.destroy();
+					} );
+			} );
+
+			it( 'should not add a protocol without the configuration', () => {
+				formView.urlInputView.fieldView.value = 'ckeditor.com';
+				formView.fire( 'submit' );
+
+				expect( formView.urlInputView.fieldView.value ).to.equal( 'ckeditor.com' );
+			} );
+
+			it( 'should not add a protocol to the local links even when `config.link.defaultProtocol` configured', () => {
+				return createEditorWithDefaultProtocol( 'http://' ).then( ( { editor, formView } ) => {
+					formView.urlInputView.fieldView.value = '#test';
+					formView.fire( 'submit' );
+
+					expect( formView.urlInputView.fieldView.value ).to.equal( '#test' );
+
+					editor.destroy();
+				} );
+			} );
+
+			it( 'should not add a protocol to the relative links even when `config.link.defaultProtocol` configured', () => {
+				return createEditorWithDefaultProtocol( 'http://' ).then( ( { editor, formView } ) => {
+					formView.urlInputView.fieldView.value = '/test.html';
+					formView.fire( 'submit' );
+
+					expect( formView.urlInputView.fieldView.value ).to.equal( '/test.html' );
+
+					editor.destroy();
+				} );
+			} );
+
+			it( 'should not add a protocol when given provided within the value even when `config.link.defaultProtocol` configured', () => {
+				return createEditorWithDefaultProtocol( 'http://' ).then( ( { editor, formView } ) => {
+					formView.urlInputView.fieldView.value = 'http://example.com';
+					formView.fire( 'submit' );
+
+					expect( formView.urlInputView.fieldView.value ).to.equal( 'http://example.com' );
+
+					editor.destroy();
+				} );
+			} );
+
+			it( 'should use the "http://" protocol when it\'s configured', () => {
+				return createEditorWithDefaultProtocol( 'http://' ).then( ( { editor, formView } ) => {
+					formView.urlInputView.fieldView.value = 'ckeditor.com';
+					formView.fire( 'submit' );
+
+					expect( formView.urlInputView.fieldView.value ).to.equal( 'http://ckeditor.com' );
+
+					editor.destroy();
+				} );
+			} );
+
+			it( 'should use the "http://" protocol when it\'s configured and form input value contains "www."', () => {
+				return createEditorWithDefaultProtocol( 'http://' ).then( ( { editor, formView } ) => {
+					formView.urlInputView.fieldView.value = 'www.ckeditor.com';
+					formView.fire( 'submit' );
+
+					expect( formView.urlInputView.fieldView.value ).to.equal( 'http://www.ckeditor.com' );
+
+					editor.destroy();
+				} );
+			} );
+
+			it( 'should propagate the protocol to the link\'s `linkHref` attribute in model', () => {
+				return createEditorWithDefaultProtocol( 'http://' ).then( ( { editor, formView } ) => {
+					setModelData( editor.model, '[ckeditor.com]' );
+
+					formView.urlInputView.fieldView.value = 'ckeditor.com';
+					formView.fire( 'submit' );
+
+					expect( getModelData( editor.model ) ).to.equal(
+						'[<$text linkHref="http://ckeditor.com">ckeditor.com</$text>]'
+					);
+
+					editor.destroy();
+				} );
+			} );
+
+			it( 'should detect an email on submitting the form and add "mailto:" protocol automatically to the provided value', () => {
+				return createEditorWithDefaultProtocol( 'http://' ).then( ( { editor, formView } ) => {
+					setModelData( editor.model, '[email@example.com]' );
+
+					formView.urlInputView.fieldView.value = 'email@example.com';
+					formView.fire( 'submit' );
+
+					expect( formView.urlInputView.fieldView.value ).to.equal( 'mailto:email@example.com' );
+					expect( getModelData( editor.model ) ).to.equal(
+						'[<$text linkHref="mailto:email@example.com">email@example.com</$text>]'
+					);
+
+					editor.destroy();
+				} );
+			} );
+
+			it( 'should not add an email protocol when given provided within the value' +
+				'even when `config.link.defaultProtocol` configured', () => {
+				return createEditorWithDefaultProtocol( 'mailto:' ).then( ( { editor, formView } ) => {
+					formView.urlInputView.fieldView.value = 'mailto:test@example.com';
+					formView.fire( 'submit' );
+
+					expect( formView.urlInputView.fieldView.value ).to.equal( 'mailto:test@example.com' );
+
+					editor.destroy();
+				} );
+			} );
+		} );
+
 		describe( 'binding', () => {
 		describe( 'binding', () => {
 			beforeEach( () => {
 			beforeEach( () => {
 				setModelData( editor.model, '<paragraph>f[o]o</paragraph>' );
 				setModelData( editor.model, '<paragraph>f[o]o</paragraph>' );

+ 11 - 0
packages/ckeditor5-link/tests/manual/linkimage.html

@@ -0,0 +1,11 @@
+<div id="editor">
+	<figure class="image">
+		<a href="https://cksource.com">
+			<img alt="bar" src="sample.jpg">
+		</a>
+		<figcaption>CKEditor logo - caption</figcaption>
+	</figure>
+	<a href="https://cksource.com">
+		<img alt="bar" src="sample.jpg">
+	</a>
+</div>

+ 49 - 0
packages/ckeditor5-link/tests/manual/linkimage.js

@@ -0,0 +1,49 @@
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/* globals console, window, document */
+
+import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
+import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
+import LinkImage from '../../src/linkimage';
+
+ClassicEditor
+	.create( document.querySelector( '#editor' ), {
+		plugins: [ ArticlePluginSet, LinkImage ],
+		toolbar: [
+			'heading',
+			'|',
+			'bold',
+			'italic',
+			'link',
+			'bulletedList',
+			'numberedList',
+			'|',
+			'outdent',
+			'indent',
+			'|',
+			'blockQuote',
+			'insertTable',
+			'mediaEmbed',
+			'undo',
+			'redo'
+		],
+		image: {
+			toolbar: [ 'imageStyle:full', 'imageStyle:side', '|', 'imageTextAlternative' ]
+		},
+		table: {
+			contentToolbar: [
+				'tableColumn',
+				'tableRow',
+				'mergeTableCells'
+			]
+		}
+	} )
+	.then( editor => {
+		window.editor = editor;
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );

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

@@ -0,0 +1,3 @@
+# Linking Image
+
+Both images should have defined the `linkHref` attribute.

+ 59 - 0
packages/ckeditor5-link/tests/manual/protocol.html

@@ -0,0 +1,59 @@
+<style>
+	code {
+		background: hsl(0, 0%, 90%);
+	}
+
+	sup {
+		position: relative;
+	}
+
+	.ck-editor__editable sup:after {
+		content: '';
+		display: inline-block;
+		position: absolute;
+		width: 14px;
+		height: 14px;
+		left: 1px;
+		top: 1px;
+		background: hsla(207, 87%, 55%, 0.5);
+		border-radius: 50px;
+		animation: pulse 2s linear infinite;
+	}
+
+	@keyframes pulse {
+		0% {
+			transform: scale(1);
+		}
+
+		50% {
+			transform: scale(2);
+		}
+
+		100% {
+			transform: scale(1);
+		}
+	}
+</style>
+
+
+<h2><code>Feature is disabled</code></h2>
+<div id="editor0">
+	<p>This is <a href="http://ckeditor.com">CKEditor5</a> from <a href="http://cksource.com">CKSource</a>. If you need more information please contact us at support@example.com.</p>
+</div>
+
+<h2><code>http://</code></h2>
+<div id="editor1">
+	<p>This is <a href="http://ckeditor.com">CKEditor5</a> from <a href="http://cksource.com">CKSource</a>. If you need more information please contact us at support@example.com <sup class="indicator">[1]</sup>.</p>
+</div>
+
+<p><sup>[1]</sup><strong>When feature enabled:</strong> copy the email address and create a link with it (<code>mailto:</code> protocol will be added automatically).</p>
+
+<h2><code>https://</code></h2>
+<div id="editor2">
+	<p>This is <a href="http://ckeditor.com">CKEditor5</a> from <a href="http://cksource.com">CKSource</a>. If you need more information please contact us at support@example.com.</p>
+</div>
+
+<h2><code>mailto:</code></h2>
+<div id="editor3">
+	<p>This is <a href="http://ckeditor.com">CKEditor5</a> from <a href="http://cksource.com">CKSource</a>. If you need more information please contact us at support@example.com.</p>
+</div>

+ 37 - 0
packages/ckeditor5-link/tests/manual/protocol.js

@@ -0,0 +1,37 @@
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/* globals console:false, window, document */
+
+import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
+import Enter from '@ckeditor/ckeditor5-enter/src/enter';
+import Typing from '@ckeditor/ckeditor5-typing/src/typing';
+import Link from '../../src/link';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import Undo from '@ckeditor/ckeditor5-undo/src/undo';
+import Superscript from '@ckeditor/ckeditor5-basic-styles/src/superscript';
+
+createEditorWithDefaultProtocol( '#editor0' );
+createEditorWithDefaultProtocol( '#editor1', 'http://' );
+createEditorWithDefaultProtocol( '#editor2', 'https://' );
+createEditorWithDefaultProtocol( '#editor3', 'mailto:' );
+
+function createEditorWithDefaultProtocol( editor, defaultProtocol ) {
+	return ClassicEditor
+		.create( document.querySelector( editor ), {
+			plugins: [ Link, Typing, Paragraph, Undo, Enter, Superscript ],
+			toolbar: [ 'link', 'undo', 'redo' ],
+			link: {
+				addTargetToExternalLinks: true,
+				...defaultProtocol && { defaultProtocol }
+			}
+		} )
+		.then( editor => {
+			window.editor = editor;
+		} )
+		.catch( err => {
+			console.error( err.stack );
+		} );
+}

+ 6 - 0
packages/ckeditor5-link/tests/manual/protocol.md

@@ -0,0 +1,6 @@
+## Link protocol
+
+This test checks whether:
+- `config.link.defaultProtocol` applies.
+- when input value starts with a protocol-like syntax (like `http://` etc.) or any non-word (like `#` or `/`) then `defaultProtocol` won't be applied.
+- the plugin dynamically change link protocol to `mailto:` when email address was detected.

BIN
packages/ckeditor5-link/tests/manual/sample.jpg


+ 20 - 2
packages/ckeditor5-media-embed/tests/mediaembedtoolbar.js

@@ -3,7 +3,7 @@
  * 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
  */
  */
 
 
-/* global document */
+/* global document, console */
 
 
 import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
 import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
 import BalloonEditor from '@ckeditor/ckeditor5-editor-balloon/src/ballooneditor';
 import BalloonEditor from '@ckeditor/ckeditor5-editor-balloon/src/ballooneditor';
@@ -186,7 +186,7 @@ describe( 'MediaEmbedToolbar - integration with BalloonEditor', () => {
 		return BalloonEditor.create( element, {
 		return BalloonEditor.create( element, {
 			plugins: [ Paragraph, MediaEmbed, MediaEmbedToolbar, FakeButton, Bold ],
 			plugins: [ Paragraph, MediaEmbed, MediaEmbedToolbar, FakeButton, Bold ],
 			balloonToolbar: [ 'bold' ],
 			balloonToolbar: [ 'bold' ],
-			media: {
+			mediaEmbed: {
 				toolbar: [ 'fake_button' ]
 				toolbar: [ 'fake_button' ]
 			}
 			}
 		} ).then( _editor => {
 		} ).then( _editor => {
@@ -239,6 +239,24 @@ describe( 'MediaEmbedToolbar - integration with BalloonEditor', () => {
 
 
 		expect( balloon.visibleView ).to.equal( balloonToolbar.toolbarView );
 		expect( balloon.visibleView ).to.equal( balloonToolbar.toolbarView );
 	} );
 	} );
+
+	it( 'does not create the toolbar if its items are not specified', () => {
+		const consoleWarnStub = sinon.stub( console, 'warn' );
+		const element = document.createElement( 'div' );
+
+		return BalloonEditor.create( element, {
+			plugins: [ Paragraph, MediaEmbed, MediaEmbedToolbar, Bold ]
+		} ).then( editor => {
+			widgetToolbarRepository = editor.plugins.get( 'WidgetToolbarRepository' );
+
+			expect( widgetToolbarRepository._toolbarDefinitions.get( 'mediaEmbed' ) ).to.be.undefined;
+			expect( consoleWarnStub.calledOnce ).to.equal( true );
+			expect( consoleWarnStub.firstCall.args[ 0 ] ).to.match( /^widget-toolbar-no-items:/ );
+
+			element.remove();
+			return editor.destroy();
+		} );
+	} );
 } );
 } );
 
 
 // Plugin that adds fake_button to editor's component factory.
 // Plugin that adds fake_button to editor's component factory.

+ 18 - 5
packages/ckeditor5-paragraph/src/insertparagraphcommand.js

@@ -18,6 +18,10 @@ import Command from '@ckeditor/ckeditor5-core/src/command';
  *			position: editor.model.createPositionBefore( element )
  *			position: editor.model.createPositionBefore( element )
  *		} );
  *		} );
  *
  *
+ * If a paragraph is disallowed in the context of the specific position, the command
+ * will attempt to split position ancestors to find a place where it is possible
+ * to insert a paragraph.
+ *
  * **Note**: This command moves the selection to the inserted paragraph.
  * **Note**: This command moves the selection to the inserted paragraph.
  *
  *
  * @extends module:core/command~Command
  * @extends module:core/command~Command
@@ -33,15 +37,24 @@ export default class InsertParagraphCommand extends Command {
 	 */
 	 */
 	execute( options ) {
 	execute( options ) {
 		const model = this.editor.model;
 		const model = this.editor.model;
-
-		if ( !model.schema.checkChild( options.position, 'paragraph' ) ) {
-			return;
-		}
+		let position = options.position;
 
 
 		model.change( writer => {
 		model.change( writer => {
 			const paragraph = writer.createElement( 'paragraph' );
 			const paragraph = writer.createElement( 'paragraph' );
 
 
-			model.insertContent( paragraph, options.position );
+			if ( !model.schema.checkChild( position.parent, paragraph ) ) {
+				const allowedParent = model.schema.findAllowedParent( position, paragraph );
+
+				// It could be there's no ancestor limit that would allow paragraph.
+				// In theory, "paragraph" could be disallowed even in the "$root".
+				if ( !allowedParent ) {
+					return;
+				}
+
+				position = writer.split( position, allowedParent ).position;
+			}
+
+			model.insertContent( paragraph, position );
 
 
 			writer.setSelection( paragraph, 'in' );
 			writer.setSelection( paragraph, 'in' );
 		} );
 		} );

+ 28 - 6
packages/ckeditor5-paragraph/tests/insertparagraphcommand.js

@@ -23,7 +23,9 @@ describe( 'InsertParagraphCommand', () => {
 			editor.commands.add( 'insertParagraph', command );
 			editor.commands.add( 'insertParagraph', command );
 			schema.register( 'paragraph', { inheritAllFrom: '$block' } );
 			schema.register( 'paragraph', { inheritAllFrom: '$block' } );
 			schema.register( 'heading1', { inheritAllFrom: '$block', allowIn: 'headersOnly' } );
 			schema.register( 'heading1', { inheritAllFrom: '$block', allowIn: 'headersOnly' } );
-			schema.register( 'headersOnly', { inheritAllFrom: '$block' } );
+			schema.register( 'allowP', { inheritAllFrom: '$block' } );
+			schema.register( 'disallowP', { inheritAllFrom: '$block', allowIn: [ 'allowP' ] } );
+			model.schema.extend( 'paragraph', { allowIn: [ 'allowP' ] } );
 		} );
 		} );
 	} );
 	} );
 
 
@@ -42,18 +44,38 @@ describe( 'InsertParagraphCommand', () => {
 			expect( getData( model ) ).to.equal( '<paragraph>[]</paragraph><heading1>foo</heading1>' );
 			expect( getData( model ) ).to.equal( '<paragraph>[]</paragraph><heading1>foo</heading1>' );
 		} );
 		} );
 
 
-		it( 'should do nothing if the paragraph is not allowed at the provided position', () => {
-			setData( model, '<headersOnly><heading1>foo[]</heading1></headersOnly>' );
+		it( 'should split ancestors down to a limit where a paragraph is allowed', () => {
+			setData( model, '<allowP><disallowP>foo</disallowP></allowP>' );
 
 
 			command.execute( {
 			command.execute( {
-				position: model.createPositionBefore( root.getChild( 0 ).getChild( 0 ) )
+				// fo[]o
+				position: model.createPositionAt( root.getChild( 0 ).getChild( 0 ), 2 )
 			} );
 			} );
 
 
+			expect( getData( model ) ).to.equal(
+				'<allowP>' +
+					'<disallowP>fo</disallowP>' +
+					'<paragraph>[]</paragraph>' +
+					'<disallowP>o</disallowP>' +
+				'</allowP>'
+			);
+		} );
+
+		it( 'should do nothing if the paragraph is not allowed at the provided position', () => {
+			// Create a situation where "paragraph" is disallowed even in the "root".
+			schema.addChildCheck( ( context, childDefinition ) => {
+				if ( context.endsWith( '$root' ) && childDefinition.name == 'paragraph' ) {
+					return false;
+				}
+			} );
+
+			setData( model, '<heading1>foo[]</heading1>' );
+
 			command.execute( {
 			command.execute( {
-				position: model.createPositionAfter( root.getChild( 0 ).getChild( 0 ) )
+				position: model.createPositionBefore( root.getChild( 0 ) )
 			} );
 			} );
 
 
-			expect( getData( model ) ).to.equal( '<headersOnly><heading1>foo[]</heading1></headersOnly>' );
+			expect( getData( model ) ).to.equal( '<heading1>foo[]</heading1>' );
 		} );
 		} );
 
 
 		describe( 'interation with existing paragraphs in the content', () => {
 		describe( 'interation with existing paragraphs in the content', () => {

+ 38 - 30
packages/ckeditor5-table/src/converters/downcast.js

@@ -8,7 +8,7 @@
  */
  */
 
 
 import TableWalker from './../tablewalker';
 import TableWalker from './../tablewalker';
-import { toWidget, toWidgetEditable } from '@ckeditor/ckeditor5-widget/src/utils';
+import { toWidget, toWidgetEditable, setHighlightHandling } from '@ckeditor/ckeditor5-widget/src/utils';
 
 
 /**
 /**
  * Model table element to view table element conversion helper.
  * Model table element to view table element conversion helper.
@@ -84,7 +84,7 @@ export function downcastInsertTable( options = {} ) {
  *
  *
  * @returns {Function} Conversion helper.
  * @returns {Function} Conversion helper.
  */
  */
-export function downcastInsertRow( options = {} ) {
+export function downcastInsertRow() {
 	return dispatcher => dispatcher.on( 'insert:tableRow', ( evt, data, conversionApi ) => {
 	return dispatcher => dispatcher.on( 'insert:tableRow', ( evt, data, conversionApi ) => {
 		const tableRow = data.item;
 		const tableRow = data.item;
 
 
@@ -120,7 +120,7 @@ export function downcastInsertRow( options = {} ) {
 
 
 			const insertPosition = conversionApi.writer.createPositionAt( trElement, 'end' );
 			const insertPosition = conversionApi.writer.createPositionAt( trElement, 'end' );
 
 
-			createViewTableCellElement( tableSlot, tableAttributes, insertPosition, conversionApi, options );
+			createViewTableCellElement( tableSlot, tableAttributes, insertPosition, conversionApi, { asWidget: true } );
 		}
 		}
 	} );
 	} );
 }
 }
@@ -133,7 +133,7 @@ export function downcastInsertRow( options = {} ) {
  *
  *
  * @returns {Function} Conversion helper.
  * @returns {Function} Conversion helper.
  */
  */
-export function downcastInsertCell( options = {} ) {
+export function downcastInsertCell() {
 	return dispatcher => dispatcher.on( 'insert:tableCell', ( evt, data, conversionApi ) => {
 	return dispatcher => dispatcher.on( 'insert:tableCell', ( evt, data, conversionApi ) => {
 		const tableCell = data.item;
 		const tableCell = data.item;
 
 
@@ -158,7 +158,7 @@ export function downcastInsertCell( options = {} ) {
 				const trElement = conversionApi.mapper.toViewElement( tableRow );
 				const trElement = conversionApi.mapper.toViewElement( tableRow );
 				const insertPosition = conversionApi.writer.createPositionAt( trElement, tableRow.getChildIndex( tableCell ) );
 				const insertPosition = conversionApi.writer.createPositionAt( trElement, tableRow.getChildIndex( tableCell ) );
 
 
-				createViewTableCellElement( tableSlot, tableAttributes, insertPosition, conversionApi, options );
+				createViewTableCellElement( tableSlot, tableAttributes, insertPosition, conversionApi, { asWidget: true } );
 
 
 				// No need to iterate further.
 				// No need to iterate further.
 				return;
 				return;
@@ -178,9 +178,7 @@ export function downcastInsertCell( options = {} ) {
  *
  *
  * @returns {Function} Conversion helper.
  * @returns {Function} Conversion helper.
  */
  */
-export function downcastTableHeadingRowsChange( options = {} ) {
-	const asWidget = !!options.asWidget;
-
+export function downcastTableHeadingRowsChange() {
 	return dispatcher => dispatcher.on( 'attribute:headingRows:table', ( evt, data, conversionApi ) => {
 	return dispatcher => dispatcher.on( 'attribute:headingRows:table', ( evt, data, conversionApi ) => {
 		const table = data.item;
 		const table = data.item;
 
 
@@ -205,7 +203,7 @@ export function downcastTableHeadingRowsChange( options = {} ) {
 			// Rename all table cells from moved rows to 'th' as they lands in <thead>.
 			// Rename all table cells from moved rows to 'th' as they lands in <thead>.
 			for ( const tableRow of rowsToMove ) {
 			for ( const tableRow of rowsToMove ) {
 				for ( const tableCell of tableRow.getChildren() ) {
 				for ( const tableCell of tableRow.getChildren() ) {
-					renameViewTableCell( tableCell, 'th', conversionApi, asWidget );
+					renameViewTableCell( tableCell, 'th', conversionApi );
 				}
 				}
 			}
 			}
 		}
 		}
@@ -228,7 +226,7 @@ export function downcastTableHeadingRowsChange( options = {} ) {
 			};
 			};
 
 
 			for ( const tableSlot of tableWalker ) {
 			for ( const tableSlot of tableWalker ) {
-				renameViewTableCellIfRequired( tableSlot, tableAttributes, conversionApi, asWidget );
+				renameViewTableCellIfRequired( tableSlot, tableAttributes, conversionApi );
 			}
 			}
 		}
 		}
 
 
@@ -249,9 +247,7 @@ export function downcastTableHeadingRowsChange( options = {} ) {
  *
  *
  * @returns {Function} Conversion helper.
  * @returns {Function} Conversion helper.
  */
  */
-export function downcastTableHeadingColumnsChange( options = {} ) {
-	const asWidget = !!options.asWidget;
-
+export function downcastTableHeadingColumnsChange() {
 	return dispatcher => dispatcher.on( 'attribute:headingColumns:table', ( evt, data, conversionApi ) => {
 	return dispatcher => dispatcher.on( 'attribute:headingColumns:table', ( evt, data, conversionApi ) => {
 		const table = data.item;
 		const table = data.item;
 
 
@@ -270,7 +266,7 @@ export function downcastTableHeadingColumnsChange( options = {} ) {
 		const lastColumnToCheck = ( oldColumns > newColumns ? oldColumns : newColumns ) - 1;
 		const lastColumnToCheck = ( oldColumns > newColumns ? oldColumns : newColumns ) - 1;
 
 
 		for ( const tableSlot of new TableWalker( table, { endColumn: lastColumnToCheck } ) ) {
 		for ( const tableSlot of new TableWalker( table, { endColumn: lastColumnToCheck } ) ) {
-			renameViewTableCellIfRequired( tableSlot, tableAttributes, conversionApi, asWidget );
+			renameViewTableCellIfRequired( tableSlot, tableAttributes, conversionApi );
 		}
 		}
 	} );
 	} );
 }
 }
@@ -327,8 +323,7 @@ function toTableWidget( viewElement, writer ) {
 // @param {module:engine/model/element~Element} tableCell
 // @param {module:engine/model/element~Element} tableCell
 // @param {String} desiredCellElementName
 // @param {String} desiredCellElementName
 // @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi
 // @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi
-// @param {Boolean} asWidget
-function renameViewTableCell( tableCell, desiredCellElementName, conversionApi, asWidget ) {
+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 );
 
 
@@ -337,18 +332,19 @@ function renameViewTableCell( tableCell, desiredCellElementName, conversionApi,
 		return;
 		return;
 	}
 	}
 
 
-	let renamedCell;
+	const editable = viewWriter.createEditableElement( desiredCellElementName, viewCell.getAttributes() );
+	const renamedCell = toWidgetEditable( editable, viewWriter );
 
 
-	if ( asWidget ) {
-		const editable = viewWriter.createEditableElement( desiredCellElementName, viewCell.getAttributes() );
-		renamedCell = toWidgetEditable( editable, viewWriter );
+	setHighlightHandling(
+		renamedCell,
+		viewWriter,
+		( element, descriptor, writer ) => writer.addClass( normalizeToArray( descriptor.classes ), element ),
+		( element, descriptor, writer ) => writer.removeClass( normalizeToArray( descriptor.classes ), element )
+	);
 
 
-		viewWriter.insert( viewWriter.createPositionAfter( viewCell ), renamedCell );
-		viewWriter.move( viewWriter.createRangeIn( viewCell ), viewWriter.createPositionAt( renamedCell, 0 ) );
-		viewWriter.remove( viewWriter.createRangeOn( viewCell ) );
-	} else {
-		renamedCell = viewWriter.rename( desiredCellElementName, viewCell );
-	}
+	viewWriter.insert( viewWriter.createPositionAfter( viewCell ), renamedCell );
+	viewWriter.move( viewWriter.createRangeIn( viewCell ), viewWriter.createPositionAt( renamedCell, 0 ) );
+	viewWriter.remove( viewWriter.createRangeOn( viewCell ) );
 
 
 	conversionApi.mapper.unbindViewElement( viewCell );
 	conversionApi.mapper.unbindViewElement( viewCell );
 	conversionApi.mapper.bindElements( tableCell, renamedCell );
 	conversionApi.mapper.bindElements( tableCell, renamedCell );
@@ -359,8 +355,7 @@ function renameViewTableCell( tableCell, desiredCellElementName, conversionApi,
 // @param {module:table/tablewalker~TableSlot} tableSlot
 // @param {module:table/tablewalker~TableSlot} tableSlot
 // @param {{headingColumns, headingRows}} tableAttributes
 // @param {{headingColumns, headingRows}} tableAttributes
 // @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi
 // @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi
-// @param {Boolean} asWidget
-function renameViewTableCellIfRequired( tableSlot, tableAttributes, conversionApi, asWidget ) {
+function renameViewTableCellIfRequired( tableSlot, tableAttributes, conversionApi ) {
 	const { cell } = tableSlot;
 	const { cell } = tableSlot;
 
 
 	// Check whether current columnIndex is overlapped by table cells from previous rows.
 	// Check whether current columnIndex is overlapped by table cells from previous rows.
@@ -371,7 +366,7 @@ function renameViewTableCellIfRequired( tableSlot, tableAttributes, conversionAp
 	// If in single change we're converting attribute changes and inserting cell the table cell might not be inserted into view
 	// If in single change we're converting attribute changes and inserting cell the table cell might not be inserted into view
 	// because of child conversion is done after parent.
 	// because of child conversion is done after parent.
 	if ( viewCell && viewCell.name !== desiredCellElementName ) {
 	if ( viewCell && viewCell.name !== desiredCellElementName ) {
-		renameViewTableCell( cell, desiredCellElementName, conversionApi, asWidget );
+		renameViewTableCell( cell, desiredCellElementName, conversionApi );
 	}
 	}
 }
 }
 
 
@@ -388,6 +383,15 @@ function createViewTableCellElement( tableSlot, tableAttributes, insertPosition,
 		toWidgetEditable( conversionApi.writer.createEditableElement( cellElementName ), conversionApi.writer ) :
 		toWidgetEditable( conversionApi.writer.createEditableElement( cellElementName ), conversionApi.writer ) :
 		conversionApi.writer.createContainerElement( cellElementName );
 		conversionApi.writer.createContainerElement( cellElementName );
 
 
+	if ( asWidget ) {
+		setHighlightHandling(
+			cellElement,
+			conversionApi.writer,
+			( element, descriptor, writer ) => writer.addClass( normalizeToArray( descriptor.classes ), element ),
+			( element, descriptor, writer ) => writer.removeClass( normalizeToArray( descriptor.classes ), element )
+		);
+	}
+
 	const tableCell = tableSlot.cell;
 	const tableCell = tableSlot.cell;
 
 
 	const firstChild = tableCell.getChild( 0 );
 	const firstChild = tableCell.getChild( 0 );
@@ -401,7 +405,7 @@ function createViewTableCellElement( tableSlot, tableAttributes, insertPosition,
 
 
 		conversionApi.consumable.consume( innerParagraph, 'insert' );
 		conversionApi.consumable.consume( innerParagraph, 'insert' );
 
 
-		if ( options.asWidget ) {
+		if ( asWidget ) {
 			// Use display:inline-block to force Chrome/Safari to limit text mutations to this element.
 			// Use display:inline-block to force Chrome/Safari to limit text mutations to this element.
 			// See #6062.
 			// See #6062.
 			const fakeParagraph = conversionApi.writer.createContainerElement( 'span', { style: 'display:inline-block' } );
 			const fakeParagraph = conversionApi.writer.createContainerElement( 'span', { style: 'display:inline-block' } );
@@ -569,3 +573,7 @@ function getViewTable( viewFigure ) {
 function hasAnyAttribute( element ) {
 function hasAnyAttribute( element ) {
 	return !![ ...element.getAttributeKeys() ].length;
 	return !![ ...element.getAttributeKeys() ].length;
 }
 }
+
+function normalizeToArray( classes ) {
+	return Array.isArray( classes ) ? classes : [ classes ];
+}

+ 5 - 9
packages/ckeditor5-table/src/tableediting.js

@@ -100,26 +100,22 @@ export default class TableEditing extends Plugin {
 		conversion.for( 'upcast' ).elementToElement( { model: 'tableRow', view: 'tr' } );
 		conversion.for( 'upcast' ).elementToElement( { model: 'tableRow', view: 'tr' } );
 		conversion.for( 'upcast' ).add( skipEmptyTableRow() );
 		conversion.for( 'upcast' ).add( skipEmptyTableRow() );
 
 
-		conversion.for( 'editingDowncast' ).add( downcastInsertRow( { asWidget: true } ) );
-		conversion.for( 'dataDowncast' ).add( downcastInsertRow() );
-		conversion.for( 'downcast' ).add( downcastRemoveRow() );
+		conversion.for( 'editingDowncast' ).add( downcastInsertRow() );
+		conversion.for( 'editingDowncast' ).add( downcastRemoveRow() );
 
 
 		// Table cell conversion.
 		// Table cell conversion.
 		conversion.for( 'upcast' ).add( upcastTableCell( 'td' ) );
 		conversion.for( 'upcast' ).add( upcastTableCell( 'td' ) );
 		conversion.for( 'upcast' ).add( upcastTableCell( 'th' ) );
 		conversion.for( 'upcast' ).add( upcastTableCell( 'th' ) );
 
 
-		conversion.for( 'editingDowncast' ).add( downcastInsertCell( { asWidget: true } ) );
-		conversion.for( 'dataDowncast' ).add( downcastInsertCell() );
+		conversion.for( 'editingDowncast' ).add( downcastInsertCell() );
 
 
 		// Table attributes conversion.
 		// Table attributes conversion.
 		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 rows and columns conversion.
-		conversion.for( 'editingDowncast' ).add( downcastTableHeadingColumnsChange( { asWidget: true } ) );
-		conversion.for( 'dataDowncast' ).add( downcastTableHeadingColumnsChange() );
-		conversion.for( 'editingDowncast' ).add( downcastTableHeadingRowsChange( { asWidget: true } ) );
-		conversion.for( 'dataDowncast' ).add( downcastTableHeadingRowsChange() );
+		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 ) );

+ 10 - 76
packages/ckeditor5-table/src/tablekeyboard.js

@@ -13,7 +13,10 @@ import TableWalker from './tablewalker';
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect';
 import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect';
 import priorities from '@ckeditor/ckeditor5-utils/src/priorities';
 import priorities from '@ckeditor/ckeditor5-utils/src/priorities';
-import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
+import {
+	isArrowKeyCode,
+	getLocalizedArrowKeyCodeDirection
+} from '@ckeditor/ckeditor5-utils/src/keyboard';
 import { getSelectedTableCells, getTableCellsContainingSelection } from './utils/selection';
 import { getSelectedTableCells, getTableCellsContainingSelection } from './utils/selection';
 import { findAncestor } from './utils/common';
 import { findAncestor } from './utils/common';
 
 
@@ -50,11 +53,10 @@ export default class TableKeyboard extends Plugin {
 		this.editor.keystrokes.set( 'Tab', this._getTabHandler( true ), { priority: 'low' } );
 		this.editor.keystrokes.set( 'Tab', this._getTabHandler( true ), { priority: 'low' } );
 		this.editor.keystrokes.set( 'Shift+Tab', this._getTabHandler( false ), { priority: 'low' } );
 		this.editor.keystrokes.set( 'Shift+Tab', this._getTabHandler( false ), { priority: 'low' } );
 
 
-		// Note: This listener has the "high+1" priority because we would like to avoid collisions with other features
-		// (like Widgets), which take over the keydown events with the "high" priority. Table navigation takes precedence
-		// over Widgets in that matter (widget arrow handler stops propagation of event if object element was selected
-		// but getNearestSelectionRange didn't returned any range).
-		this.listenTo( viewDocument, 'keydown', ( ...args ) => this._onKeydown( ...args ), { priority: priorities.get( 'high' ) + 1 } );
+		// Note: This listener has the "high-10" priority because it should allow the Widget plugin to handle the default
+		// behavior first ("high") but it should not be "prevent–defaulted" by the Widget plugin ("high-20") because of
+		// the fake selection retention on the fully selected widget.
+		this.listenTo( viewDocument, 'keydown', ( ...args ) => this._onKeydown( ...args ), { priority: priorities.get( 'high' ) - 10 } );
 	}
 	}
 
 
 	/**
 	/**
@@ -163,13 +165,14 @@ export default class TableKeyboard extends Plugin {
 	 * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData
 	 * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData
 	 */
 	 */
 	_onKeydown( eventInfo, domEventData ) {
 	_onKeydown( eventInfo, domEventData ) {
+		const editor = this.editor;
 		const keyCode = domEventData.keyCode;
 		const keyCode = domEventData.keyCode;
 
 
 		if ( !isArrowKeyCode( keyCode ) ) {
 		if ( !isArrowKeyCode( keyCode ) ) {
 			return;
 			return;
 		}
 		}
 
 
-		const direction = getDirectionFromKeyCode( keyCode, this.editor.locale.contentLanguageDirection );
+		const direction = getLocalizedArrowKeyCodeDirection( keyCode, editor.locale.contentLanguageDirection );
 		const wasHandled = this._handleArrowKeys( direction, domEventData.shiftKey );
 		const wasHandled = this._handleArrowKeys( direction, domEventData.shiftKey );
 
 
 		if ( wasHandled ) {
 		if ( wasHandled ) {
@@ -226,19 +229,6 @@ export default class TableKeyboard extends Plugin {
 			return true;
 			return true;
 		}
 		}
 
 
-		// If this is an object selected and it's not at the start or the end of cell content
-		// then let's allow widget handler to take care of it.
-		const objectElement = selection.getSelectedElement();
-
-		if ( objectElement && model.schema.isObject( objectElement ) ) {
-			return false;
-		}
-
-		// If next to the selection there is an object then this is not the cell boundary (widget handler should handle this).
-		if ( this._isObjectElementNextToSelection( selection, isForward ) ) {
-			return false;
-		}
-
 		// If there isn't any $text position between cell edge and selection then we shall move the selection to next cell.
 		// If there isn't any $text position between cell edge and selection then we shall move the selection to next cell.
 		const textRange = this._findTextRangeFromSelection( cellRange, selection, isForward );
 		const textRange = this._findTextRangeFromSelection( cellRange, selection, isForward );
 
 
@@ -303,27 +293,6 @@ export default class TableKeyboard extends Plugin {
 		return focus.isEqual( probe.focus );
 		return focus.isEqual( probe.focus );
 	}
 	}
 
 
-	/**
-	 * Checks if there is an {@link module:engine/model/element~Element element} next to the current
-	 * {@link module:engine/model/selection~Selection model selection} marked in the
-	 * {@link module:engine/model/schema~Schema schema} as an `object`.
-	 *
-	 * @private
-	 * @param {module:engine/model/selection~Selection} modelSelection The selection.
-	 * @param {Boolean} isForward The direction of checking.
-	 * @returns {Boolean}
-	 */
-	_isObjectElementNextToSelection( modelSelection, isForward ) {
-		const model = this.editor.model;
-		const schema = model.schema;
-
-		const probe = model.createSelection( modelSelection );
-		model.modifySelection( probe, { direction: isForward ? 'forward' : 'backward' } );
-		const objectElement = isForward ? probe.focus.nodeBefore : probe.focus.nodeAfter;
-
-		return objectElement && schema.isObject( objectElement );
-	}
-
 	/**
 	/**
 	 * Truncates the range so that it spans from the last selection position
 	 * Truncates the range so that it spans from the last selection position
 	 * to the last allowed `$text` position (mirrored if `isForward` is false).
 	 * to the last allowed `$text` position (mirrored if `isForward` is false).
@@ -515,38 +484,3 @@ export default class TableKeyboard extends Plugin {
 	}
 	}
 }
 }
 
 
-// Returns `true` if the provided key code represents one of the arrow keys.
-//
-// @private
-// @param {Number} keyCode
-// @returns {Boolean}
-function isArrowKeyCode( keyCode ) {
-	return keyCode == keyCodes.arrowright ||
-		keyCode == keyCodes.arrowleft ||
-		keyCode == keyCodes.arrowup ||
-		keyCode == keyCodes.arrowdown;
-}
-
-// Returns the direction name from `keyCode`.
-//
-// @private
-// @param {Number} keyCode
-// @param {String} contentLanguageDirection The content language direction.
-// @returns {'left'|'up'|'right'|'down'} Arrow direction.
-function getDirectionFromKeyCode( keyCode, contentLanguageDirection ) {
-	const isLtrContent = contentLanguageDirection === 'ltr';
-
-	switch ( keyCode ) {
-		case keyCodes.arrowleft:
-			return isLtrContent ? 'left' : 'right';
-
-		case keyCodes.arrowright:
-			return isLtrContent ? 'right' : 'left';
-
-		case keyCodes.arrowup:
-			return 'up';
-
-		case keyCodes.arrowdown:
-			return 'down';
-	}
-}

+ 12 - 94
packages/ckeditor5-table/tests/_utils/utils.js

@@ -3,21 +3,11 @@
  * 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 {
-	downcastInsertCell,
-	downcastInsertRow,
-	downcastInsertTable,
-	downcastRemoveRow,
-	downcastTableHeadingColumnsChange,
-	downcastTableHeadingRowsChange
-} from '../../src/converters/downcast';
-import upcastTable, { upcastTableCell } from '../../src/converters/upcasttable';
 import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import TableWalker from '../../src/tablewalker';
 import TableWalker from '../../src/tablewalker';
 
 
 const WIDGET_TABLE_CELL_CLASS = 'ck-editor__editable ck-editor__nested-editable';
 const WIDGET_TABLE_CELL_CLASS = 'ck-editor__editable ck-editor__nested-editable';
-const BORDER_REG_EXP = /[\s\S]+/;
 
 
 /**
 /**
  * Returns a model representation of a table shorthand notation:
  * Returns a model representation of a table shorthand notation:
@@ -180,85 +170,6 @@ export function viewTable( tableData, attributes = {} ) {
 	return `<figure ${ figureAttributes }>${ asWidget ? widgetHandler : '' }<table>${ thead }${ tbody }</table></figure>`;
 	return `<figure ${ figureAttributes }>${ asWidget ? widgetHandler : '' }<table>${ thead }${ tbody }</table></figure>`;
 }
 }
 
 
-export function defaultSchema( schema, registerParagraph = true ) {
-	schema.register( 'table', {
-		allowWhere: '$block',
-		allowAttributes: [ 'headingRows', 'headingColumns' ],
-		isLimit: true,
-		isObject: true,
-		isBlock: true
-	} );
-
-	schema.register( 'tableRow', {
-		allowIn: 'table',
-		isLimit: true
-	} );
-
-	schema.register( 'tableCell', {
-		allowIn: 'tableRow',
-		allowAttributes: [ 'colspan', 'rowspan' ],
-		isObject: true
-	} );
-
-	// Allow all $block content inside table cell.
-	schema.extend( '$block', { allowIn: 'tableCell' } );
-
-	// Disallow table in table.
-	schema.addChildCheck( ( context, childDefinition ) => {
-		if ( childDefinition.name == 'table' && Array.from( context.getNames() ).includes( 'table' ) ) {
-			return false;
-		}
-	} );
-
-	if ( registerParagraph ) {
-		schema.register( 'paragraph', { inheritAllFrom: '$block' } );
-	}
-
-	// Styles
-	schema.extend( 'tableCell', {
-		allowAttributes: [ 'border' ]
-	} );
-}
-
-export function defaultConversion( conversion, asWidget = false ) {
-	conversion.elementToElement( { model: 'paragraph', view: 'p' } );
-
-	// Table conversion.
-	conversion.for( 'upcast' ).add( upcastTable() );
-	conversion.for( 'downcast' ).add( downcastInsertTable( { asWidget } ) );
-
-	// Table row conversion.
-	conversion.for( 'upcast' ).elementToElement( { model: 'tableRow', view: 'tr' } );
-	conversion.for( 'downcast' ).add( downcastInsertRow( { asWidget } ) );
-	conversion.for( 'downcast' ).add( downcastRemoveRow( { asWidget } ) );
-
-	// Table cell conversion.
-	conversion.for( 'upcast' ).add( upcastTableCell( 'td' ) );
-	conversion.for( 'upcast' ).add( upcastTableCell( 'th' ) );
-	conversion.for( 'downcast' ).add( downcastInsertCell( { asWidget } ) );
-
-	// Table attributes conversion.
-	conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } );
-	conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } );
-
-	conversion.for( 'downcast' ).add( downcastTableHeadingColumnsChange( { asWidget } ) );
-	conversion.for( 'downcast' ).add( downcastTableHeadingRowsChange( { asWidget } ) );
-
-	// Styles
-	conversion.for( 'upcast' ).attributeToAttribute( {
-		view: {
-			name: 'td',
-			styles: {
-				border: BORDER_REG_EXP
-			}
-		},
-		model: {
-			key: 'border',
-			value: viewElement => viewElement.getStyle( 'border' )
-		}
-	} );
-}
-
 /**
 /**
  * An assertion helper for top-right-bottom-left attribute object.
  * An assertion helper for top-right-bottom-left attribute object.
  *
  *
@@ -420,10 +331,10 @@ function formatAttributes( attributes ) {
 	let attributesString = '';
 	let attributesString = '';
 
 
 	if ( attributes ) {
 	if ( attributes ) {
-		const entries = Object.entries( attributes );
+		const sortedKeys = Object.keys( attributes ).sort();
 
 
-		if ( entries.length ) {
-			attributesString = ' ' + entries.map( entry => `${ entry[ 0 ] }="${ entry[ 1 ] }"` ).join( ' ' );
+		if ( sortedKeys.length ) {
+			attributesString = ' ' + sortedKeys.map( key => `${ key }="${ attributes[ key ] }"` ).join( ' ' );
 		}
 		}
 	}
 	}
 
 
@@ -456,17 +367,24 @@ function makeRows( tableData, options ) {
 					delete tableCellData.isSelected;
 					delete tableCellData.isSelected;
 				}
 				}
 
 
-				const attributes = isObject ? tableCellData : {};
+				let attributes = {};
 
 
 				if ( asWidget ) {
 				if ( asWidget ) {
 					attributes.class = getClassToSet( attributes );
 					attributes.class = getClassToSet( attributes );
 					attributes.contenteditable = 'true';
 					attributes.contenteditable = 'true';
 				}
 				}
 
 
+				if ( isObject ) {
+					attributes = {
+						...attributes,
+						...tableCellData
+					};
+				}
+
 				if ( !( contents.replace( '[', '' ).replace( ']', '' ).startsWith( '<' ) ) && enforceWrapping ) {
 				if ( !( contents.replace( '[', '' ).replace( ']', '' ).startsWith( '<' ) ) && enforceWrapping ) {
 					contents =
 					contents =
 						`<${ wrappingElement == 'span' ? 'span style="display:inline-block"' : wrappingElement }>` +
 						`<${ wrappingElement == 'span' ? 'span style="display:inline-block"' : wrappingElement }>` +
-							contents +
+						contents +
 						`</${ wrappingElement }>`;
 						`</${ wrappingElement }>`;
 				}
 				}
 
 

+ 51 - 20
packages/ckeditor5-table/tests/commands/insertcolumncommand.js

@@ -5,13 +5,15 @@
 
 
 import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
 import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
 import HorizontalLineEditing from '@ckeditor/ckeditor5-horizontal-line/src/horizontallineediting';
 import HorizontalLineEditing from '@ckeditor/ckeditor5-horizontal-line/src/horizontallineediting';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 
-import InsertColumnCommand from '../../src/commands/insertcolumncommand';
 import TableSelection from '../../src/tableselection';
 import TableSelection from '../../src/tableselection';
-import { assertSelectedCells, defaultConversion, defaultSchema, modelTable } from '../_utils/utils';
-import TableUtils from '../../src/tableutils';
-import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
+import TableEditing from '../../src/tableediting';
+import { assertSelectedCells, modelTable } from '../_utils/utils';
+
+import InsertColumnCommand from '../../src/commands/insertcolumncommand';
 
 
 describe( 'InsertColumnCommand', () => {
 describe( 'InsertColumnCommand', () => {
 	let editor, model, command;
 	let editor, model, command;
@@ -19,14 +21,11 @@ describe( 'InsertColumnCommand', () => {
 	beforeEach( () => {
 	beforeEach( () => {
 		return ModelTestEditor
 		return ModelTestEditor
 			.create( {
 			.create( {
-				plugins: [ TableUtils, TableSelection, HorizontalLineEditing ]
+				plugins: [ Paragraph, TableEditing, TableSelection, HorizontalLineEditing ]
 			} )
 			} )
 			.then( newEditor => {
 			.then( newEditor => {
 				editor = newEditor;
 				editor = newEditor;
 				model = editor.model;
 				model = editor.model;
-
-				defaultSchema( model.schema );
-				defaultConversion( editor.conversion );
 			} );
 			} );
 	} );
 	} );
 
 
@@ -168,18 +167,34 @@ describe( 'InsertColumnCommand', () => {
 			} );
 			} );
 
 
 			it( 'should skip wide spanned columns', () => {
 			it( 'should skip wide spanned columns', () => {
+				// +----+----+----+----+----+----+
+				// | 00 | 01 | 02 | 03 | 04 | 05 |
+				// +----+----+----+----+----+----+
+				// | 10 | 11 | 12      | 14 | 15 |
+				// +----+----+----+----+----+----+
+				// | 20                | 24      |
+				// +----+----+----+----+----+----+
+				//                     ^-- heading columns
 				setData( model, modelTable( [
 				setData( model, modelTable( [
-					[ '11', '12[]', '13', '14', '15' ],
-					[ '21', '22', { colspan: 2, contents: '23' }, '25' ],
-					[ { colspan: 4, contents: '31' }, { colspan: 2, contents: '34' } ]
+					[ '00', '01[]', '02', '03', '04', '05' ],
+					[ '10', '11', { contents: '12', colspan: 2 }, '14', '15' ],
+					[ { contents: '20', colspan: 4 }, { contents: '24', colspan: 2 } ]
 				], { headingColumns: 4 } ) );
 				], { headingColumns: 4 } ) );
 
 
 				command.execute();
 				command.execute();
 
 
+				// +----+----+----+----+----+----+----+
+				// | 00 | 01 |    | 02 | 03 | 04 | 05 |
+				// +----+----+----+----+----+----+----+
+				// | 10 | 11 |    | 12      | 14 | 15 |
+				// +----+----+----+----+----+----+----+
+				// | 20                     | 24      |
+				// +----+----+----+----+----+----+----+
+				//                          ^-- heading columns
 				assertEqualMarkup( getData( model ), modelTable( [
 				assertEqualMarkup( getData( model ), modelTable( [
-					[ '11', '12[]', '', '13', '14', '15' ],
-					[ '21', '22', '', { colspan: 2, contents: '23' }, '25' ],
-					[ { colspan: 5, contents: '31' }, { colspan: 2, contents: '34' } ]
+					[ '00', '01[]', '', '02', '03', '04', '05' ],
+					[ '10', '11', '', { contents: '12', colspan: 2 }, '14', '15' ],
+					[ { contents: '20', colspan: 5 }, { contents: '24', colspan: 2 } ]
 				], { headingColumns: 5 } ) );
 				], { headingColumns: 5 } ) );
 			} );
 			} );
 
 
@@ -335,18 +350,34 @@ describe( 'InsertColumnCommand', () => {
 			} );
 			} );
 
 
 			it( 'should skip wide spanned columns', () => {
 			it( 'should skip wide spanned columns', () => {
+				// +----+----+----+----+----+----+
+				// | 00 | 01 | 02 | 03 | 04 | 05 |
+				// +----+----+----+----+----+----+
+				// | 10 | 11 | 12      | 14 | 15 |
+				// +----+----+----+----+----+----+
+				// | 20                | 24      |
+				// +----+----+----+----+----+----+
+				//                     ^-- heading columns
 				setData( model, modelTable( [
 				setData( model, modelTable( [
-					[ '11', '12', '13[]', '14', '15' ],
-					[ '21', '22', { colspan: 2, contents: '23' }, '25' ],
-					[ { colspan: 4, contents: '31' }, { colspan: 2, contents: '34' } ]
+					[ '00', '01', '[]02', '03', '04', '05' ],
+					[ '10', '11', { contents: '12', colspan: 2 }, '14', '15' ],
+					[ { contents: '20', colspan: 4 }, { contents: '24', colspan: 2 } ]
 				], { headingColumns: 4 } ) );
 				], { headingColumns: 4 } ) );
 
 
 				command.execute();
 				command.execute();
 
 
+				// +----+----+----+----+----+----+----+
+				// | 00 | 01 |    | 02 | 03 | 04 | 05 |
+				// +----+----+----+----+----+----+----+
+				// | 10 | 11 |    | 12      | 14 | 15 |
+				// +----+----+----+----+----+----+----+
+				// | 20                     | 24      |
+				// +----+----+----+----+----+----+----+
+				//                          ^-- heading columns
 				assertEqualMarkup( getData( model ), modelTable( [
 				assertEqualMarkup( getData( model ), modelTable( [
-					[ '11', '12', '', '13[]', '14', '15' ],
-					[ '21', '22', '', { colspan: 2, contents: '23' }, '25' ],
-					[ { colspan: 5, contents: '31' }, { colspan: 2, contents: '34' } ]
+					[ '00', '01', '', '[]02', '03', '04', '05' ],
+					[ '10', '11', '', { contents: '12', colspan: 2 }, '14', '15' ],
+					[ { contents: '20', colspan: 5 }, { contents: '24', colspan: 2 } ]
 				], { headingColumns: 5 } ) );
 				], { headingColumns: 5 } ) );
 			} );
 			} );
 		} );
 		} );

+ 71 - 22
packages/ckeditor5-table/tests/commands/insertrowcommand.js

@@ -5,13 +5,15 @@
 
 
 import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
 import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
 import HorizontalLineEditing from '@ckeditor/ckeditor5-horizontal-line/src/horizontallineediting';
 import HorizontalLineEditing from '@ckeditor/ckeditor5-horizontal-line/src/horizontallineediting';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 
-import InsertRowCommand from '../../src/commands/insertrowcommand';
+import TableEditing from '../../src/tableediting';
 import TableSelection from '../../src/tableselection';
 import TableSelection from '../../src/tableselection';
-import { assertSelectedCells, defaultConversion, defaultSchema, modelTable } from '../_utils/utils';
-import TableUtils from '../../src/tableutils';
-import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
+import { assertSelectedCells, modelTable } from '../_utils/utils';
+
+import InsertRowCommand from '../../src/commands/insertrowcommand';
 
 
 describe( 'InsertRowCommand', () => {
 describe( 'InsertRowCommand', () => {
 	let editor, model, command;
 	let editor, model, command;
@@ -19,14 +21,11 @@ describe( 'InsertRowCommand', () => {
 	beforeEach( () => {
 	beforeEach( () => {
 		return ModelTestEditor
 		return ModelTestEditor
 			.create( {
 			.create( {
-				plugins: [ TableUtils, TableSelection, HorizontalLineEditing ]
+				plugins: [ Paragraph, TableEditing, TableSelection, HorizontalLineEditing ]
 			} )
 			} )
 			.then( newEditor => {
 			.then( newEditor => {
 				editor = newEditor;
 				editor = newEditor;
 				model = editor.model;
 				model = editor.model;
-
-				defaultSchema( model.schema );
-				defaultConversion( editor.conversion );
 			} );
 			} );
 	} );
 	} );
 
 
@@ -119,54 +118,104 @@ describe( 'InsertRowCommand', () => {
 			} );
 			} );
 
 
 			it( 'should expand rowspan of a cell that overlaps inserted rows', () => {
 			it( 'should expand rowspan of a cell that overlaps inserted rows', () => {
+				// +----+----+----+----+
+				// | 00      | 02 | 03 |
+				// +----+----+----+----+ <-- heading rows
+				// | 10      | 12 | 13 |
+				// +         +----+----+
+				// |         | 22 | 23 |
+				// +----+----+----+----+
+				//                     ^-- heading columns
 				setData( model, modelTable( [
 				setData( model, modelTable( [
-					[ { colspan: 2, contents: '00' }, '02', '03' ],
-					[ { colspan: 2, rowspan: 4, contents: '10[]' }, '12', '13' ],
+					[ { contents: '00', colspan: 2 }, '02', '03' ],
+					[ { contents: '10[]', colspan: 2, rowspan: 2 }, '12', '13' ],
 					[ '22', '23' ]
 					[ '22', '23' ]
 				], { headingColumns: 3, headingRows: 1 } ) );
 				], { headingColumns: 3, headingRows: 1 } ) );
 
 
 				command.execute();
 				command.execute();
 
 
+				// +----+----+----+----+
+				// | 00      | 02 | 03 |
+				// +----+----+----+----+ <-- heading rows
+				// | 10      | 12 | 13 |
+				// +         +----+----+
+				// |         |    |    |
+				// +         +----+----+
+				// |         | 22 | 23 |
+				// +----+----+----+----+
+				//                     ^-- heading columns
 				assertEqualMarkup( getData( model ), modelTable( [
 				assertEqualMarkup( getData( model ), modelTable( [
-					[ { colspan: 2, contents: '00' }, '02', '03' ],
-					[ { colspan: 2, rowspan: 5, contents: '10[]' }, '12', '13' ],
+					[ { contents: '00', colspan: 2 }, '02', '03' ],
+					[ { contents: '10[]', colspan: 2, rowspan: 3 }, '12', '13' ],
 					[ '', '' ],
 					[ '', '' ],
 					[ '22', '23' ]
 					[ '22', '23' ]
 				], { headingColumns: 3, headingRows: 1 } ) );
 				], { headingColumns: 3, headingRows: 1 } ) );
 			} );
 			} );
 
 
 			it( 'should not expand rowspan of a cell that does not overlaps inserted rows', () => {
 			it( 'should not expand rowspan of a cell that does not overlaps inserted rows', () => {
+				// +----+----+----+
+				// | 00 | 01 | 02 |
+				// +    +----+----+
+				// |    | 11 | 12 |
+				// +----+----+----+ <-- heading rows
+				// | 20 | 21 | 22 |
+				// +----+----+----+
 				setData( model, modelTable( [
 				setData( model, modelTable( [
-					[ { rowspan: 2, contents: '00' }, '01', '02' ],
+					[ { contents: '00', rowspan: 2 }, '01', '02' ],
 					[ '11[]', '12' ],
 					[ '11[]', '12' ],
 					[ '20', '21', '22' ]
 					[ '20', '21', '22' ]
-				], { headingColumns: 3, headingRows: 1 } ) );
+				], { headingRows: 2 } ) );
 
 
 				command.execute();
 				command.execute();
 
 
+				// +----+----+----+
+				// | 00 | 01 | 02 |
+				// +    +----+----+
+				// |    | 11 | 12 |
+				// +----+----+----+ <-- heading rows
+				// |    |    |    |
+				// +----+----+----+
+				// | 20 | 21 | 22 |
+				// +----+----+----+
 				assertEqualMarkup( getData( model ), modelTable( [
 				assertEqualMarkup( getData( model ), modelTable( [
-					[ { rowspan: 2, contents: '00' }, '01', '02' ],
+					[ { contents: '00', rowspan: 2 }, '01', '02' ],
 					[ '11[]', '12' ],
 					[ '11[]', '12' ],
 					[ '', '', '' ],
 					[ '', '', '' ],
 					[ '20', '21', '22' ]
 					[ '20', '21', '22' ]
-				], { headingColumns: 3, headingRows: 1 } ) );
+				], { headingRows: 2 } ) );
 			} );
 			} );
 
 
 			it( 'should properly calculate columns if next row has colspans', () => {
 			it( 'should properly calculate columns if next row has colspans', () => {
+				// +----+----+----+
+				// | 00 | 01 | 02 |
+				// +    +----+----+
+				// |    | 11 | 12 |
+				// +----+----+----+ <-- heading rows
+				// | 20           |
+				// +----+----+----+
 				setData( model, modelTable( [
 				setData( model, modelTable( [
-					[ { rowspan: 2, contents: '00' }, '01', '02' ],
+					[ { contents: '00', rowspan: 2 }, '01', '02' ],
 					[ '11[]', '12' ],
 					[ '11[]', '12' ],
-					[ { colspan: 3, contents: '20' } ]
-				], { headingColumns: 3, headingRows: 1 } ) );
+					[ { contents: '20', colspan: 3 } ]
+				], { headingRows: 2 } ) );
 
 
 				command.execute();
 				command.execute();
 
 
+				// +----+----+----+
+				// | 00 | 01 | 02 |
+				// +    +----+----+
+				// |    | 11 | 12 |
+				// +----+----+----+ <-- heading rows
+				// |    |    |    |
+				// +----+----+----+
+				// | 20           |
+				// +----+----+----+
 				assertEqualMarkup( getData( model ), modelTable( [
 				assertEqualMarkup( getData( model ), modelTable( [
-					[ { rowspan: 2, contents: '00' }, '01', '02' ],
+					[ { contents: '00', rowspan: 2 }, '01', '02' ],
 					[ '11[]', '12' ],
 					[ '11[]', '12' ],
 					[ '', '', '' ],
 					[ '', '', '' ],
-					[ { colspan: 3, contents: '20' } ]
-				], { headingColumns: 3, headingRows: 1 } ) );
+					[ { contents: '20', colspan: 3 } ]
+				], { headingRows: 2 } ) );
 			} );
 			} );
 
 
 			it( 'should insert rows at the end of a table', () => {
 			it( 'should insert rows at the end of a table', () => {

+ 6 - 8
packages/ckeditor5-table/tests/commands/inserttablecommand.js

@@ -4,13 +4,14 @@
  */
  */
 
 
 import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
 import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 
-import InsertTableCommand from '../../src/commands/inserttablecommand';
-import TableUtils from '../../src/tableutils';
+import TableEditing from '../../src/tableediting';
+import { modelTable } from '../_utils/utils';
 
 
-import { defaultConversion, defaultSchema, modelTable } from '../_utils/utils';
-import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
+import InsertTableCommand from '../../src/commands/inserttablecommand';
 
 
 describe( 'InsertTableCommand', () => {
 describe( 'InsertTableCommand', () => {
 	let editor, model, command;
 	let editor, model, command;
@@ -18,15 +19,12 @@ describe( 'InsertTableCommand', () => {
 	beforeEach( () => {
 	beforeEach( () => {
 		return ModelTestEditor
 		return ModelTestEditor
 			.create( {
 			.create( {
-				plugins: [ TableUtils ]
+				plugins: [ Paragraph, TableEditing ]
 			} )
 			} )
 			.then( newEditor => {
 			.then( newEditor => {
 				editor = newEditor;
 				editor = newEditor;
 				model = editor.model;
 				model = editor.model;
 				command = new InsertTableCommand( editor );
 				command = new InsertTableCommand( editor );
-
-				defaultSchema( model.schema );
-				defaultConversion( editor.conversion );
 			} );
 			} );
 	} );
 	} );
 
 

+ 18 - 11
packages/ckeditor5-table/tests/commands/mergecellcommand.js

@@ -4,12 +4,14 @@
  */
  */
 
 
 import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
 import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
+
+import TableEditing from '../../src/tableediting';
+import { modelTable } from '../_utils/utils';
 
 
 import MergeCellCommand from '../../src/commands/mergecellcommand';
 import MergeCellCommand from '../../src/commands/mergecellcommand';
-import { defaultConversion, defaultSchema, modelTable } from '../_utils/utils';
-import TableUtils from '../../src/tableutils';
-import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 
 describe( 'MergeCellCommand', () => {
 describe( 'MergeCellCommand', () => {
 	let editor, model, command, root;
 	let editor, model, command, root;
@@ -17,15 +19,12 @@ describe( 'MergeCellCommand', () => {
 	beforeEach( () => {
 	beforeEach( () => {
 		return ModelTestEditor
 		return ModelTestEditor
 			.create( {
 			.create( {
-				plugins: [ TableUtils ]
+				plugins: [ Paragraph, TableEditing ]
 			} )
 			} )
 			.then( newEditor => {
 			.then( newEditor => {
 				editor = newEditor;
 				editor = newEditor;
 				model = editor.model;
 				model = editor.model;
 				root = model.document.getRoot( 'main' );
 				root = model.document.getRoot( 'main' );
-
-				defaultSchema( model.schema );
-				defaultConversion( editor.conversion );
 			} );
 			} );
 	} );
 	} );
 
 
@@ -65,7 +64,9 @@ describe( 'MergeCellCommand', () => {
 
 
 			it( 'should be false if in a cell that has sibling but with different rowspan', () => {
 			it( 'should be false if in a cell that has sibling but with different rowspan', () => {
 				setData( model, modelTable( [
 				setData( model, modelTable( [
-					[ { rowspan: 2, contents: '00[]' }, { rowspan: 3, contents: '01' } ]
+					[ { rowspan: 2, contents: '00[]' }, { rowspan: 3, contents: '01' }, '02' ],
+					[ '12' ],
+					[ '20', '22' ]
 				] ) );
 				] ) );
 
 
 				expect( command.isEnabled ).to.be.false;
 				expect( command.isEnabled ).to.be.false;
@@ -173,7 +174,9 @@ describe( 'MergeCellCommand', () => {
 
 
 			it( 'should be undefined if in a cell that has sibling but with different rowspan', () => {
 			it( 'should be undefined if in a cell that has sibling but with different rowspan', () => {
 				setData( model, modelTable( [
 				setData( model, modelTable( [
-					[ { rowspan: 2, contents: '00[]' }, { rowspan: 3, contents: '01' } ]
+					[ { rowspan: 2, contents: '00[]' }, { rowspan: 3, contents: '01' }, '02' ],
+					[ '12' ],
+					[ '20', '22' ]
 				] ) );
 				] ) );
 
 
 				expect( command.value ).to.be.undefined;
 				expect( command.value ).to.be.undefined;
@@ -309,7 +312,9 @@ describe( 'MergeCellCommand', () => {
 
 
 			it( 'should be false if in a cell that has sibling but with different rowspan', () => {
 			it( 'should be false if in a cell that has sibling but with different rowspan', () => {
 				setData( model, modelTable( [
 				setData( model, modelTable( [
-					[ { rowspan: 2, contents: '00' }, { rowspan: 3, contents: '01[]' } ]
+					[ { rowspan: 2, contents: '00' }, { rowspan: 3, contents: '01[]' } ],
+					[ '12' ],
+					[ '20', '22' ]
 				] ) );
 				] ) );
 
 
 				expect( command.isEnabled ).to.be.false;
 				expect( command.isEnabled ).to.be.false;
@@ -409,7 +414,9 @@ describe( 'MergeCellCommand', () => {
 
 
 			it( 'should be undefined if in a cell that has sibling but with different rowspan', () => {
 			it( 'should be undefined if in a cell that has sibling but with different rowspan', () => {
 				setData( model, modelTable( [
 				setData( model, modelTable( [
-					[ { rowspan: 2, contents: '00' }, { rowspan: 3, contents: '01[]' } ]
+					[ { rowspan: 2, contents: '00' }, { rowspan: 3, contents: '01[]' } ],
+					[ '12' ],
+					[ '20', '22' ]
 				] ) );
 				] ) );
 
 
 				expect( command.value ).to.be.undefined;
 				expect( command.value ).to.be.undefined;

+ 7 - 8
packages/ckeditor5-table/tests/commands/removecolumncommand.js

@@ -4,13 +4,15 @@
  */
  */
 
 
 import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
 import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 
-import RemoveColumnCommand from '../../src/commands/removecolumncommand';
+import TableEditing from '../../src/tableediting';
 import TableSelection from '../../src/tableselection';
 import TableSelection from '../../src/tableselection';
-import { defaultConversion, defaultSchema, modelTable } from '../_utils/utils';
-import TableUtils from '../../src/tableutils';
-import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
+import { modelTable } from '../_utils/utils';
+
+import RemoveColumnCommand from '../../src/commands/removecolumncommand';
 
 
 describe( 'RemoveColumnCommand', () => {
 describe( 'RemoveColumnCommand', () => {
 	let editor, model, command;
 	let editor, model, command;
@@ -18,15 +20,12 @@ describe( 'RemoveColumnCommand', () => {
 	beforeEach( () => {
 	beforeEach( () => {
 		return ModelTestEditor
 		return ModelTestEditor
 			.create( {
 			.create( {
-				plugins: [ TableUtils, TableSelection ]
+				plugins: [ Paragraph, TableEditing, TableSelection ]
 			} )
 			} )
 			.then( newEditor => {
 			.then( newEditor => {
 				editor = newEditor;
 				editor = newEditor;
 				model = editor.model;
 				model = editor.model;
 				command = new RemoveColumnCommand( editor );
 				command = new RemoveColumnCommand( editor );
-
-				defaultSchema( model.schema );
-				defaultConversion( editor.conversion );
 			} );
 			} );
 	} );
 	} );
 
 

+ 6 - 6
packages/ckeditor5-table/tests/commands/selectcolumncommand.js

@@ -4,26 +4,26 @@
  */
  */
 
 
 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 { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
 
-import SelectColumnCommand from '../../src/commands/selectcolumncommand';
+import TableEditing from '../../src/tableediting';
 import TableSelection from '../../src/tableselection';
 import TableSelection from '../../src/tableselection';
-import { assertSelectedCells, defaultConversion, defaultSchema, modelTable } from '../_utils/utils';
+import { assertSelectedCells, modelTable } from '../_utils/utils';
+
+import SelectColumnCommand from '../../src/commands/selectcolumncommand';
 
 
 describe( 'SelectColumnCommand', () => {
 describe( 'SelectColumnCommand', () => {
 	let editor, model, modelRoot, command, tableSelection;
 	let editor, model, modelRoot, command, tableSelection;
 
 
 	beforeEach( () => {
 	beforeEach( () => {
-		return VirtualTestEditor.create( { plugins: [ TableSelection ] } )
+		return VirtualTestEditor.create( { plugins: [ Paragraph, TableEditing, TableSelection ] } )
 			.then( newEditor => {
 			.then( newEditor => {
 				editor = newEditor;
 				editor = newEditor;
 				model = editor.model;
 				model = editor.model;
 				modelRoot = model.document.getRoot();
 				modelRoot = model.document.getRoot();
 				command = new SelectColumnCommand( editor );
 				command = new SelectColumnCommand( editor );
 				tableSelection = editor.plugins.get( TableSelection );
 				tableSelection = editor.plugins.get( TableSelection );
-
-				defaultSchema( model.schema );
-				defaultConversion( editor.conversion );
 			} );
 			} );
 	} );
 	} );
 
 

+ 6 - 6
packages/ckeditor5-table/tests/commands/selectrowcommand.js

@@ -4,26 +4,26 @@
  */
  */
 
 
 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 { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
 
-import SelectRowCommand from '../../src/commands/selectrowcommand';
+import TableEditing from '../../src/tableediting';
 import TableSelection from '../../src/tableselection';
 import TableSelection from '../../src/tableselection';
-import { assertSelectedCells, defaultConversion, defaultSchema, modelTable } from '../_utils/utils';
+import { assertSelectedCells, modelTable } from '../_utils/utils';
+
+import SelectRowCommand from '../../src/commands/selectrowcommand';
 
 
 describe( 'SelectRowCommand', () => {
 describe( 'SelectRowCommand', () => {
 	let editor, model, modelRoot, command, tableSelection;
 	let editor, model, modelRoot, command, tableSelection;
 
 
 	beforeEach( () => {
 	beforeEach( () => {
-		return VirtualTestEditor.create( { plugins: [ TableSelection ] } )
+		return VirtualTestEditor.create( { plugins: [ Paragraph, TableEditing, TableSelection ] } )
 			.then( newEditor => {
 			.then( newEditor => {
 				editor = newEditor;
 				editor = newEditor;
 				model = editor.model;
 				model = editor.model;
 				modelRoot = model.document.getRoot();
 				modelRoot = model.document.getRoot();
 				command = new SelectRowCommand( editor );
 				command = new SelectRowCommand( editor );
 				tableSelection = editor.plugins.get( TableSelection );
 				tableSelection = editor.plugins.get( TableSelection );
-
-				defaultSchema( model.schema );
-				defaultConversion( editor.conversion );
 			} );
 			} );
 	} );
 	} );
 
 

+ 7 - 8
packages/ckeditor5-table/tests/commands/setheadercolumncommand.js

@@ -4,13 +4,15 @@
  */
  */
 
 
 import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
 import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 
-import SetHeaderColumnCommand from '../../src/commands/setheadercolumncommand';
-import { assertSelectedCells, defaultConversion, defaultSchema, modelTable } from '../_utils/utils';
 import TableSelection from '../../src/tableselection';
 import TableSelection from '../../src/tableselection';
-import TableUtils from '../../src/tableutils';
-import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
+import TableEditing from '../../src/tableediting';
+import { assertSelectedCells, modelTable } from '../_utils/utils';
+
+import SetHeaderColumnCommand from '../../src/commands/setheadercolumncommand';
 
 
 describe( 'SetHeaderColumnCommand', () => {
 describe( 'SetHeaderColumnCommand', () => {
 	let editor, model, command;
 	let editor, model, command;
@@ -18,15 +20,12 @@ describe( 'SetHeaderColumnCommand', () => {
 	beforeEach( () => {
 	beforeEach( () => {
 		return ModelTestEditor
 		return ModelTestEditor
 			.create( {
 			.create( {
-				plugins: [ TableUtils, TableSelection ]
+				plugins: [ Paragraph, TableEditing, TableSelection ]
 			} )
 			} )
 			.then( newEditor => {
 			.then( newEditor => {
 				editor = newEditor;
 				editor = newEditor;
 				model = editor.model;
 				model = editor.model;
 				command = new SetHeaderColumnCommand( editor );
 				command = new SetHeaderColumnCommand( editor );
-
-				defaultSchema( model.schema );
-				defaultConversion( editor.conversion );
 			} );
 			} );
 	} );
 	} );
 
 

+ 8 - 8
packages/ckeditor5-table/tests/commands/setheaderrowcommand.js

@@ -4,28 +4,28 @@
  */
  */
 
 
 import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
 import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
-import SetHeaderRowCommand from '../../src/commands/setheaderrowcommand';
-import { assertSelectedCells, defaultConversion, defaultSchema, modelTable } from '../_utils/utils';
-import TableSelection from '../../src/tableselection';
-import TableUtils from '../../src/tableutils';
 import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 
+import TableEditing from '../../src/tableediting';
+import TableSelection from '../../src/tableselection';
+import { assertSelectedCells, modelTable } from '../_utils/utils';
+
+import SetHeaderRowCommand from '../../src/commands/setheaderrowcommand';
+
 describe( 'SetHeaderRowCommand', () => {
 describe( 'SetHeaderRowCommand', () => {
 	let editor, model, command;
 	let editor, model, command;
 
 
 	beforeEach( () => {
 	beforeEach( () => {
 		return ModelTestEditor
 		return ModelTestEditor
 			.create( {
 			.create( {
-				plugins: [ TableUtils, TableSelection ]
+				plugins: [ Paragraph, TableEditing, TableSelection ]
 			} )
 			} )
 			.then( newEditor => {
 			.then( newEditor => {
 				editor = newEditor;
 				editor = newEditor;
 				model = editor.model;
 				model = editor.model;
 				command = new SetHeaderRowCommand( editor );
 				command = new SetHeaderRowCommand( editor );
-
-				defaultSchema( model.schema );
-				defaultConversion( editor.conversion );
 			} );
 			} );
 	} );
 	} );
 
 

+ 7 - 8
packages/ckeditor5-table/tests/commands/splitcellcommand.js

@@ -4,13 +4,15 @@
  */
  */
 
 
 import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
 import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 
-import SplitCellCommand from '../../src/commands/splitcellcommand';
-import { defaultConversion, defaultSchema, modelTable } from '../_utils/utils';
+import TableEditing from '../../src/tableediting';
 import TableSelection from '../../src/tableselection';
 import TableSelection from '../../src/tableselection';
-import TableUtils from '../../src/tableutils';
-import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
+import { modelTable } from '../_utils/utils';
+
+import SplitCellCommand from '../../src/commands/splitcellcommand';
 
 
 describe( 'SplitCellCommand', () => {
 describe( 'SplitCellCommand', () => {
 	let editor, model, command;
 	let editor, model, command;
@@ -18,15 +20,12 @@ describe( 'SplitCellCommand', () => {
 	beforeEach( () => {
 	beforeEach( () => {
 		return ModelTestEditor
 		return ModelTestEditor
 			.create( {
 			.create( {
-				plugins: [ TableUtils, TableSelection ]
+				plugins: [ Paragraph, TableEditing, TableSelection ]
 			} )
 			} )
 			.then( newEditor => {
 			.then( newEditor => {
 				editor = newEditor;
 				editor = newEditor;
 				model = editor.model;
 				model = editor.model;
 				command = new SplitCellCommand( editor );
 				command = new SplitCellCommand( editor );
-
-				defaultSchema( model.schema );
-				defaultConversion( editor.conversion );
 			} );
 			} );
 	} );
 	} );
 
 

File diff suppressed because it is too large
+ 443 - 537
packages/ckeditor5-table/tests/converters/downcast.js


+ 7 - 12
packages/ckeditor5-table/tests/converters/table-cell-refresh-post-fixer.js

@@ -5,16 +5,15 @@
 
 
 /* globals document */
 /* globals document */
 
 
-import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
-
-import { defaultConversion, defaultSchema, viewTable } from '../_utils/utils';
-import injectTableCellRefreshPostFixer from '../../src/converters/table-cell-refresh-post-fixer';
-
-import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
 import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
-
 import Delete from '@ckeditor/ckeditor5-typing/src/delete';
 import Delete from '@ckeditor/ckeditor5-typing/src/delete';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
 import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+
+import TableEditing from '../../src/tableediting';
+import { viewTable } from '../_utils/utils';
 
 
 describe( 'Table cell refresh post-fixer', () => {
 describe( 'Table cell refresh post-fixer', () => {
 	let editor, model, doc, root, view, refreshItemSpy, element;
 	let editor, model, doc, root, view, refreshItemSpy, element;
@@ -25,7 +24,7 @@ describe( 'Table cell refresh post-fixer', () => {
 		element = document.createElement( 'div' );
 		element = document.createElement( 'div' );
 		document.body.appendChild( element );
 		document.body.appendChild( element );
 
 
-		return ClassicTestEditor.create( element, { extraPlugins: [ Delete ] } )
+		return ClassicTestEditor.create( element, { plugins: [ Paragraph, TableEditing, Delete ] } )
 			.then( newEditor => {
 			.then( newEditor => {
 				editor = newEditor;
 				editor = newEditor;
 				model = editor.model;
 				model = editor.model;
@@ -33,9 +32,6 @@ describe( 'Table cell refresh post-fixer', () => {
 				root = doc.getRoot( 'main' );
 				root = doc.getRoot( 'main' );
 				view = editor.editing.view;
 				view = editor.editing.view;
 
 
-				defaultSchema( model.schema );
-				defaultConversion( editor.conversion, true );
-
 				editor.model.schema.register( 'block', {
 				editor.model.schema.register( 'block', {
 					inheritAllFrom: '$block'
 					inheritAllFrom: '$block'
 				} );
 				} );
@@ -45,7 +41,6 @@ describe( 'Table cell refresh post-fixer', () => {
 				editor.conversion.attributeToAttribute( { model: 'foo', view: 'foo' } );
 				editor.conversion.attributeToAttribute( { model: 'foo', view: 'foo' } );
 				editor.conversion.attributeToAttribute( { model: 'bar', view: 'bar' } );
 				editor.conversion.attributeToAttribute( { model: 'bar', view: 'bar' } );
 
 
-				injectTableCellRefreshPostFixer( model );
 				refreshItemSpy = sinon.spy( model.document.differ, 'refreshItem' );
 				refreshItemSpy = sinon.spy( model.document.differ, 'refreshItem' );
 			} );
 			} );
 	} );
 	} );

+ 7 - 3
packages/ckeditor5-table/tests/converters/upcasttable.js

@@ -3,7 +3,7 @@
  * 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 VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
+import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
 import { getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import ImageEditing from '@ckeditor/ckeditor5-image/src/image/imageediting';
 import ImageEditing from '@ckeditor/ckeditor5-image/src/image/imageediting';
@@ -17,8 +17,8 @@ describe( 'upcastTable()', () => {
 	let editor, model;
 	let editor, model;
 
 
 	beforeEach( () => {
 	beforeEach( () => {
-		return VirtualTestEditor
-			.create( {
+		return ClassicTestEditor
+			.create( '', {
 				plugins: [ TableEditing, Paragraph, ImageEditing, Widget ]
 				plugins: [ TableEditing, Paragraph, ImageEditing, Widget ]
 			} )
 			} )
 			.then( newEditor => {
 			.then( newEditor => {
@@ -31,6 +31,10 @@ describe( 'upcastTable()', () => {
 			} );
 			} );
 	} );
 	} );
 
 
+	afterEach( () => {
+		editor.destroy();
+	} );
+
 	function expectModel( data ) {
 	function expectModel( data ) {
 		assertEqualMarkup( getModelData( model, { withoutSelection: true } ), data );
 		assertEqualMarkup( getModelData( model, { withoutSelection: true } ), data );
 	}
 	}

+ 19 - 7
packages/ckeditor5-table/tests/table-integration.js

@@ -11,7 +11,7 @@ import UndoEditing from '@ckeditor/ckeditor5-undo/src/undoediting';
 import ListEditing from '@ckeditor/ckeditor5-list/src/listediting';
 import ListEditing from '@ckeditor/ckeditor5-list/src/listediting';
 import BlockQuoteEditing from '@ckeditor/ckeditor5-block-quote/src/blockquoteediting';
 import BlockQuoteEditing from '@ckeditor/ckeditor5-block-quote/src/blockquoteediting';
 import Typing from '@ckeditor/ckeditor5-typing/src/typing';
 import Typing from '@ckeditor/ckeditor5-typing/src/typing';
-import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
+import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
 import {
 import {
 	getData as getModelData,
 	getData as getModelData,
 	setData as setModelData
 	setData as setModelData
@@ -27,14 +27,18 @@ describe( 'Table feature – integration', () => {
 		let editor, clipboard;
 		let editor, clipboard;
 
 
 		beforeEach( () => {
 		beforeEach( () => {
-			return VirtualTestEditor
-				.create( { plugins: [ Paragraph, TableEditing, ListEditing, BlockQuoteEditing, Widget, Clipboard ] } )
+			return ClassicTestEditor
+				.create( '', { plugins: [ Paragraph, TableEditing, ListEditing, BlockQuoteEditing, Widget, Clipboard ] } )
 				.then( newEditor => {
 				.then( newEditor => {
 					editor = newEditor;
 					editor = newEditor;
 					clipboard = editor.plugins.get( 'Clipboard' );
 					clipboard = editor.plugins.get( 'Clipboard' );
 				} );
 				} );
 		} );
 		} );
 
 
+		afterEach( () => {
+			editor.destroy();
+		} );
+
 		it( 'pastes td as p when pasting into the table', () => {
 		it( 'pastes td as p when pasting into the table', () => {
 			setModelData( editor.model, modelTable( [ [ 'foo[]' ] ] ) );
 			setModelData( editor.model, modelTable( [ [ 'foo[]' ] ] ) );
 
 
@@ -86,8 +90,8 @@ describe( 'Table feature – integration', () => {
 		let editor, doc, root;
 		let editor, doc, root;
 
 
 		beforeEach( () => {
 		beforeEach( () => {
-			return VirtualTestEditor
-				.create( { plugins: [ Paragraph, TableEditing, Widget, UndoEditing ] } )
+			return ClassicTestEditor
+				.create( '', { plugins: [ Paragraph, TableEditing, Widget, UndoEditing ] } )
 				.then( newEditor => {
 				.then( newEditor => {
 					editor = newEditor;
 					editor = newEditor;
 					doc = editor.model.document;
 					doc = editor.model.document;
@@ -95,6 +99,10 @@ describe( 'Table feature – integration', () => {
 				} );
 				} );
 		} );
 		} );
 
 
+		afterEach( () => {
+			editor.destroy();
+		} );
+
 		it( 'fixing empty roots should be transparent to undo', () => {
 		it( 'fixing empty roots should be transparent to undo', () => {
 			expect( editor.getData( { trim: 'none' } ) ).to.equal( '<p>&nbsp;</p>' );
 			expect( editor.getData( { trim: 'none' } ) ).to.equal( '<p>&nbsp;</p>' );
 			expect( editor.commands.get( 'undo' ).isEnabled ).to.be.false;
 			expect( editor.commands.get( 'undo' ).isEnabled ).to.be.false;
@@ -155,13 +163,17 @@ describe( 'Table feature – integration', () => {
 		let editor;
 		let editor;
 
 
 		beforeEach( () => {
 		beforeEach( () => {
-			return VirtualTestEditor
-				.create( { plugins: [ Paragraph, TableEditing, ListEditing, BlockQuoteEditing, Widget, Typing ] } )
+			return ClassicTestEditor
+				.create( '', { plugins: [ Paragraph, TableEditing, ListEditing, BlockQuoteEditing, Widget, Typing ] } )
 				.then( newEditor => {
 				.then( newEditor => {
 					editor = newEditor;
 					editor = newEditor;
 				} );
 				} );
 		} );
 		} );
 
 
+		afterEach( () => {
+			editor.destroy();
+		} );
+
 		it( 'merges elements without throwing errors', () => {
 		it( 'merges elements without throwing errors', () => {
 			setModelData( editor.model, modelTable( [
 			setModelData( editor.model, modelTable( [
 				[ '<blockQuote><paragraph>Foo</paragraph></blockQuote><paragraph>[]Bar</paragraph>' ]
 				[ '<blockQuote><paragraph>Foo</paragraph></blockQuote><paragraph>[]Bar</paragraph>' ]

+ 3 - 0
packages/ckeditor5-table/tests/tablekeyboard.js

@@ -2769,6 +2769,9 @@ describe( 'TableKeyboard', () => {
 							[ '20', '21', '22' ]
 							[ '20', '21', '22' ]
 						] ) );
 						] ) );
 
 
+						// Note: Two keydowns are necessary because the first one is handled by the WidgetTypeAround plugin
+						// to activate the "fake caret".
+						editor.editing.view.document.fire( 'keydown', downArrowDomEvtDataStub );
 						editor.editing.view.document.fire( 'keydown', downArrowDomEvtDataStub );
 						editor.editing.view.document.fire( 'keydown', downArrowDomEvtDataStub );
 
 
 						assertEqualMarkup( getModelData( model ), modelTable( [
 						assertEqualMarkup( getModelData( model ), modelTable( [

+ 123 - 171
packages/ckeditor5-table/tests/tableutils.js

@@ -4,57 +4,40 @@
  */
  */
 
 
 import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
 import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
-
-import { defaultConversion, defaultSchema, modelTable } from './_utils/utils';
+import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 
 import TableEditing from '../src/tableediting';
 import TableEditing from '../src/tableediting';
+import { modelTable } from './_utils/utils';
+
 import TableUtils from '../src/tableutils';
 import TableUtils from '../src/tableutils';
-import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
-import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 
 
 describe( 'TableUtils', () => {
 describe( 'TableUtils', () => {
 	let editor, model, root, tableUtils;
 	let editor, model, root, tableUtils;
 
 
+	beforeEach( () => {
+		return ModelTestEditor.create( {
+			plugins: [ Paragraph, TableEditing, TableUtils ]
+		} ).then( newEditor => {
+			editor = newEditor;
+			model = editor.model;
+			root = model.document.getRoot( 'main' );
+			tableUtils = editor.plugins.get( TableUtils );
+		} );
+	} );
+
 	afterEach( () => {
 	afterEach( () => {
 		return editor.destroy();
 		return editor.destroy();
 	} );
 	} );
 
 
 	describe( '#pluginName', () => {
 	describe( '#pluginName', () => {
-		beforeEach( () => {
-			return ModelTestEditor.create( {
-				plugins: [ TableUtils ]
-			} ).then( newEditor => {
-				editor = newEditor;
-				model = editor.model;
-				root = model.document.getRoot( 'main' );
-				tableUtils = editor.plugins.get( TableUtils );
-
-				defaultSchema( model.schema );
-				defaultConversion( editor.conversion );
-			} );
-		} );
-
 		it( 'should provide plugin name', () => {
 		it( 'should provide plugin name', () => {
 			expect( TableUtils.pluginName ).to.equal( 'TableUtils' );
 			expect( TableUtils.pluginName ).to.equal( 'TableUtils' );
 		} );
 		} );
 	} );
 	} );
 
 
 	describe( 'getCellLocation()', () => {
 	describe( 'getCellLocation()', () => {
-		beforeEach( () => {
-			return ModelTestEditor.create( {
-				plugins: [ TableUtils ]
-			} ).then( newEditor => {
-				editor = newEditor;
-				model = editor.model;
-				root = model.document.getRoot( 'main' );
-				tableUtils = editor.plugins.get( TableUtils );
-
-				defaultSchema( model.schema );
-				defaultConversion( editor.conversion );
-			} );
-		} );
-
 		it( 'should return proper table cell location', () => {
 		it( 'should return proper table cell location', () => {
 			setData( model, modelTable( [
 			setData( model, modelTable( [
 				[ { rowspan: 2, colspan: 2, contents: '00[]' }, '02' ],
 				[ { rowspan: 2, colspan: 2, contents: '00[]' }, '02' ],
@@ -68,20 +51,6 @@ describe( 'TableUtils', () => {
 	} );
 	} );
 
 
 	describe( 'insertRows()', () => {
 	describe( 'insertRows()', () => {
-		beforeEach( () => {
-			return ModelTestEditor.create( {
-				plugins: [ TableUtils ]
-			} ).then( newEditor => {
-				editor = newEditor;
-				model = editor.model;
-				root = model.document.getRoot( 'main' );
-				tableUtils = editor.plugins.get( TableUtils );
-
-				defaultSchema( model.schema );
-				defaultConversion( editor.conversion );
-			} );
-		} );
-
 		it( 'should insert row in given table at given index', () => {
 		it( 'should insert row in given table at given index', () => {
 			setData( model, modelTable( [
 			setData( model, modelTable( [
 				[ '11[]', '12' ],
 				[ '11[]', '12' ],
@@ -147,60 +116,122 @@ describe( 'TableUtils', () => {
 		} );
 		} );
 
 
 		it( 'should expand rowspan of a cell that overlaps inserted rows', () => {
 		it( 'should expand rowspan of a cell that overlaps inserted rows', () => {
+			// +----+----+----+----+
+			// | 00      | 02 | 03 |
+			// +----+----+----+----+ <-- heading rows
+			// | 10      | 12 | 13 |
+			// +         +----+----+
+			// |         | 22 | 23 |
+			// +----+----+----+----+
+			//                     ^-- heading columns
 			setData( model, modelTable( [
 			setData( model, modelTable( [
-				[ { colspan: 2, contents: '11[]' }, '13', '14' ],
-				[ { colspan: 2, rowspan: 4, contents: '21' }, '23', '24' ],
-				[ '33', '34' ]
+				[ { contents: '00', colspan: 2 }, '02', '03' ],
+				[ { contents: '10[]', colspan: 2, rowspan: 2 }, '12', '13' ],
+				[ '22', '23' ]
 			], { headingColumns: 3, headingRows: 1 } ) );
 			], { headingColumns: 3, headingRows: 1 } ) );
 
 
 			tableUtils.insertRows( root.getNodeByPath( [ 0 ] ), { at: 2, rows: 3 } );
 			tableUtils.insertRows( root.getNodeByPath( [ 0 ] ), { at: 2, rows: 3 } );
 
 
+			// +----+----+----+----+
+			// | 00      | 02 | 03 |
+			// +----+----+----+----+ <-- heading rows
+			// | 10      | 12 | 13 |
+			// +         +----+----+
+			// |         |    |    |
+			// +         +----+----+
+			// |         |    |    |
+			// +         +----+----+
+			// |         |    |    |
+			// +         +----+----+
+			// |         | 22 | 23 |
+			// +----+----+----+----+
+			//                     ^-- heading columns
 			assertEqualMarkup( getData( model ), modelTable( [
 			assertEqualMarkup( getData( model ), modelTable( [
-				[ { colspan: 2, contents: '11[]' }, '13', '14' ],
-				[ { colspan: 2, rowspan: 7, contents: '21' }, '23', '24' ],
+				[ { contents: '00', colspan: 2 }, '02', '03' ],
+				[ { contents: '10[]', colspan: 2, rowspan: 5 }, '12', '13' ],
 				[ '', '' ],
 				[ '', '' ],
 				[ '', '' ],
 				[ '', '' ],
 				[ '', '' ],
 				[ '', '' ],
-				[ '33', '34' ]
+				[ '22', '23' ]
 			], { headingColumns: 3, headingRows: 1 } ) );
 			], { headingColumns: 3, headingRows: 1 } ) );
 		} );
 		} );
 
 
 		it( 'should not expand rowspan of a cell that does not overlaps inserted rows', () => {
 		it( 'should not expand rowspan of a cell that does not overlaps inserted rows', () => {
+			// +----+----+----+
+			// | 00 | 01 | 02 |
+			// +    +----+----+
+			// |    | 11 | 12 |
+			// +----+----+----+ <-- heading rows
+			// | 20 | 21 | 22 |
+			// +----+----+----+
 			setData( model, modelTable( [
 			setData( model, modelTable( [
-				[ { rowspan: 2, contents: '11[]' }, '12', '13' ],
-				[ '22', '23' ],
-				[ '31', '32', '33' ]
-			], { headingColumns: 3, headingRows: 1 } ) );
+				[ { contents: '00', rowspan: 2 }, '01', '02' ],
+				[ '11[]', '12' ],
+				[ '20', '21', '22' ]
+			], { headingRows: 2 } ) );
 
 
 			tableUtils.insertRows( root.getNodeByPath( [ 0 ] ), { at: 2, rows: 3 } );
 			tableUtils.insertRows( root.getNodeByPath( [ 0 ] ), { at: 2, rows: 3 } );
 
 
+			// +----+----+----+
+			// | 00 | 01 | 02 |
+			// +    +----+----+
+			// |    | 11 | 12 |
+			// +----+----+----+ <-- heading rows
+			// |    |    |    |
+			// +----+----+----+
+			// |    |    |    |
+			// +----+----+----+
+			// |    |    |    |
+			// +----+----+----+
+			// | 20 | 21 | 22 |
+			// +----+----+----+
 			assertEqualMarkup( getData( model ), modelTable( [
 			assertEqualMarkup( getData( model ), modelTable( [
-				[ { rowspan: 2, contents: '11[]' }, '12', '13' ],
-				[ '22', '23' ],
+				[ { contents: '00', rowspan: 2 }, '01', '02' ],
+				[ '11[]', '12' ],
 				[ '', '', '' ],
 				[ '', '', '' ],
 				[ '', '', '' ],
 				[ '', '', '' ],
 				[ '', '', '' ],
 				[ '', '', '' ],
-				[ '31', '32', '33' ]
-			], { headingColumns: 3, headingRows: 1 } ) );
+				[ '20', '21', '22' ]
+			], { headingRows: 2 } ) );
 		} );
 		} );
 
 
 		it( 'should properly calculate columns if next row has colspans', () => {
 		it( 'should properly calculate columns if next row has colspans', () => {
+			// +----+----+----+
+			// | 00 | 01 | 02 |
+			// +    +----+----+
+			// |    | 11 | 12 |
+			// +----+----+----+ <-- heading rows
+			// | 20           |
+			// +----+----+----+
 			setData( model, modelTable( [
 			setData( model, modelTable( [
-				[ { rowspan: 2, contents: '11[]' }, '12', '13' ],
-				[ '22', '23' ],
-				[ { colspan: 3, contents: '31' } ]
-			], { headingColumns: 3, headingRows: 1 } ) );
+				[ { contents: '00', rowspan: 2 }, '01', '02' ],
+				[ '11[]', '12' ],
+				[ { contents: '20', colspan: 3 } ]
+			], { headingRows: 2 } ) );
 
 
 			tableUtils.insertRows( root.getNodeByPath( [ 0 ] ), { at: 2, rows: 3 } );
 			tableUtils.insertRows( root.getNodeByPath( [ 0 ] ), { at: 2, rows: 3 } );
 
 
+			// +----+----+----+
+			// | 00 | 01 | 02 |
+			// +    +----+----+
+			// |    | 11 | 12 |
+			// +----+----+----+ <-- heading rows
+			// |    |    |    |
+			// +----+----+----+
+			// |    |    |    |
+			// +----+----+----+
+			// |    |    |    |
+			// +----+----+----+
+			// | 20           |
+			// +----+----+----+
 			assertEqualMarkup( getData( model ), modelTable( [
 			assertEqualMarkup( getData( model ), modelTable( [
-				[ { rowspan: 2, contents: '11[]' }, '12', '13' ],
-				[ '22', '23' ],
+				[ { contents: '00', rowspan: 2 }, '01', '02' ],
+				[ '11[]', '12' ],
 				[ '', '', '' ],
 				[ '', '', '' ],
 				[ '', '', '' ],
 				[ '', '', '' ],
 				[ '', '', '' ],
 				[ '', '', '' ],
-				[ { colspan: 3, contents: '31' } ]
-			], { headingColumns: 3, headingRows: 1 } ) );
+				[ { contents: '20', colspan: 3 } ]
+			], { headingRows: 2 } ) );
 		} );
 		} );
 
 
 		it( 'should insert rows at the end of a table', () => {
 		it( 'should insert rows at the end of a table', () => {
@@ -304,20 +335,6 @@ describe( 'TableUtils', () => {
 	} );
 	} );
 
 
 	describe( 'insertColumns()', () => {
 	describe( 'insertColumns()', () => {
-		beforeEach( () => {
-			return ModelTestEditor.create( {
-				plugins: [ TableUtils ]
-			} ).then( newEditor => {
-				editor = newEditor;
-				model = editor.model;
-				root = model.document.getRoot( 'main' );
-				tableUtils = editor.plugins.get( TableUtils );
-
-				defaultSchema( model.schema );
-				defaultConversion( editor.conversion );
-			} );
-		} );
-
 		it( 'should insert column in given table at given index', () => {
 		it( 'should insert column in given table at given index', () => {
 			setData( model, modelTable( [
 			setData( model, modelTable( [
 				[ '11[]', '12' ],
 				[ '11[]', '12' ],
@@ -465,18 +482,34 @@ describe( 'TableUtils', () => {
 		} );
 		} );
 
 
 		it( 'should skip wide spanned columns', () => {
 		it( 'should skip wide spanned columns', () => {
+			// +----+----+----+----+----+----+
+			// | 00 | 01 | 02 | 03 | 04 | 05 |
+			// +----+----+----+----+----+----+
+			// | 10 | 11 | 12      | 14 | 15 |
+			// +----+----+----+----+----+----+
+			// | 20                | 24      |
+			// +----+----+----+----+----+----+
+			//                     ^-- heading columns
 			setData( model, modelTable( [
 			setData( model, modelTable( [
-				[ '11[]', '12', '13', '14', '15' ],
-				[ '21', '22', { colspan: 2, contents: '23' }, '25' ],
-				[ { colspan: 4, contents: '31' }, { colspan: 2, contents: '34' } ]
+				[ '00', '01[]', '02', '03', '04', '05' ],
+				[ '10', '11', { contents: '12', colspan: 2 }, '14', '15' ],
+				[ { contents: '20', colspan: 4 }, { contents: '24', colspan: 2 } ]
 			], { headingColumns: 4 } ) );
 			], { headingColumns: 4 } ) );
 
 
 			tableUtils.insertColumns( root.getNodeByPath( [ 0 ] ), { at: 2, columns: 2 } );
 			tableUtils.insertColumns( root.getNodeByPath( [ 0 ] ), { at: 2, columns: 2 } );
 
 
+			// +----+----+----+----+----+----+----+----+
+			// | 00 | 01 |    |    | 02 | 03 | 04 | 05 |
+			// +----+----+----+----+----+----+----+----+
+			// | 10 | 11 |    |    | 12      | 14 | 15 |
+			// +----+----+----+----+----+----+----+----+
+			// | 20                          | 24      |
+			// +----+----+----+----+----+----+----+----+
+			//                               ^-- heading columns
 			assertEqualMarkup( getData( model ), modelTable( [
 			assertEqualMarkup( getData( model ), modelTable( [
-				[ '11[]', '12', '', '', '13', '14', '15' ],
-				[ '21', '22', '', '', { colspan: 2, contents: '23' }, '25' ],
-				[ { colspan: 6, contents: '31' }, { colspan: 2, contents: '34' } ]
+				[ '00', '01[]', '', '', '02', '03', '04', '05' ],
+				[ '10', '11', '', '', { contents: '12', colspan: 2 }, '14', '15' ],
+				[ { contents: '20', colspan: 6 }, { contents: '24', colspan: 2 } ]
 			], { headingColumns: 6 } ) );
 			], { headingColumns: 6 } ) );
 		} );
 		} );
 
 
@@ -523,20 +556,6 @@ describe( 'TableUtils', () => {
 	} );
 	} );
 
 
 	describe( 'splitCellVertically()', () => {
 	describe( 'splitCellVertically()', () => {
-		beforeEach( () => {
-			return ModelTestEditor.create( {
-				plugins: [ TableUtils ]
-			} ).then( newEditor => {
-				editor = newEditor;
-				model = editor.model;
-				root = model.document.getRoot( 'main' );
-				tableUtils = editor.plugins.get( TableUtils );
-
-				defaultSchema( model.schema );
-				defaultConversion( editor.conversion );
-			} );
-		} );
-
 		it( 'should split table cell to given table cells number', () => {
 		it( 'should split table cell to given table cells number', () => {
 			setData( model, modelTable( [
 			setData( model, modelTable( [
 				[ '00', '01', '02' ],
 				[ '00', '01', '02' ],
@@ -681,20 +700,6 @@ describe( 'TableUtils', () => {
 	} );
 	} );
 
 
 	describe( 'splitCellHorizontally()', () => {
 	describe( 'splitCellHorizontally()', () => {
-		beforeEach( () => {
-			return ModelTestEditor.create( {
-				plugins: [ TableUtils ]
-			} ).then( newEditor => {
-				editor = newEditor;
-				model = editor.model;
-				root = model.document.getRoot( 'main' );
-				tableUtils = editor.plugins.get( TableUtils );
-
-				defaultSchema( model.schema );
-				defaultConversion( editor.conversion );
-			} );
-		} );
-
 		it( 'should split table cell to default table cells number', () => {
 		it( 'should split table cell to default table cells number', () => {
 			setData( model, modelTable( [
 			setData( model, modelTable( [
 				[ '00', '01', '02' ],
 				[ '00', '01', '02' ],
@@ -835,7 +840,7 @@ describe( 'TableUtils', () => {
 		it( 'should split row-spanned & col-spanned cell', () => {
 		it( 'should split row-spanned & col-spanned cell', () => {
 			setData( model, modelTable( [
 			setData( model, modelTable( [
 				[ '00', { colspan: 2, contents: '01[]' } ],
 				[ '00', { colspan: 2, contents: '01[]' } ],
-				[ '10', '11' ]
+				[ '10', '11', '12' ]
 			] ) );
 			] ) );
 
 
 			const tableCell = root.getNodeByPath( [ 0, 0, 1 ] );
 			const tableCell = root.getNodeByPath( [ 0, 0, 1 ] );
@@ -846,7 +851,7 @@ describe( 'TableUtils', () => {
 				[ { rowspan: 3, contents: '00' }, { colspan: 2, contents: '01[]' } ],
 				[ { rowspan: 3, contents: '00' }, { colspan: 2, contents: '01[]' } ],
 				[ { colspan: 2, contents: '' } ],
 				[ { colspan: 2, contents: '' } ],
 				[ { colspan: 2, contents: '' } ],
 				[ { colspan: 2, contents: '' } ],
-				[ '10', '11' ]
+				[ '10', '11', '12' ]
 			] ) );
 			] ) );
 		} );
 		} );
 
 
@@ -870,20 +875,6 @@ describe( 'TableUtils', () => {
 	} );
 	} );
 
 
 	describe( 'getColumns()', () => {
 	describe( 'getColumns()', () => {
-		beforeEach( () => {
-			return ModelTestEditor.create( {
-				plugins: [ TableUtils ]
-			} ).then( newEditor => {
-				editor = newEditor;
-				model = editor.model;
-				root = model.document.getRoot( 'main' );
-				tableUtils = editor.plugins.get( TableUtils );
-
-				defaultSchema( model.schema );
-				defaultConversion( editor.conversion );
-			} );
-		} );
-
 		it( 'should return proper number of columns', () => {
 		it( 'should return proper number of columns', () => {
 			setData( model, modelTable( [
 			setData( model, modelTable( [
 				[ '00', { colspan: 3, contents: '01' }, '04' ]
 				[ '00', { colspan: 3, contents: '01' }, '04' ]
@@ -894,20 +885,6 @@ describe( 'TableUtils', () => {
 	} );
 	} );
 
 
 	describe( 'getRows()', () => {
 	describe( 'getRows()', () => {
-		beforeEach( () => {
-			return ModelTestEditor.create( {
-				plugins: [ TableUtils ]
-			} ).then( newEditor => {
-				editor = newEditor;
-				model = editor.model;
-				root = model.document.getRoot( 'main' );
-				tableUtils = editor.plugins.get( TableUtils );
-
-				defaultSchema( model.schema );
-				defaultConversion( editor.conversion );
-			} );
-		} );
-
 		it( 'should return proper number of columns for simple table', () => {
 		it( 'should return proper number of columns for simple table', () => {
 			setData( model, modelTable( [
 			setData( model, modelTable( [
 				[ '00', '01' ],
 				[ '00', '01' ],
@@ -938,17 +915,6 @@ describe( 'TableUtils', () => {
 	} );
 	} );
 
 
 	describe( 'removeRows()', () => {
 	describe( 'removeRows()', () => {
-		beforeEach( () => {
-			return ModelTestEditor.create( {
-				plugins: [ Paragraph, TableEditing, TableUtils ]
-			} ).then( newEditor => {
-				editor = newEditor;
-				model = editor.model;
-				root = model.document.getRoot( 'main' );
-				tableUtils = editor.plugins.get( TableUtils );
-			} );
-		} );
-
 		describe( 'single row', () => {
 		describe( 'single row', () => {
 			it( 'should remove a given row from a table start', () => {
 			it( 'should remove a given row from a table start', () => {
 				setData( model, modelTable( [
 				setData( model, modelTable( [
@@ -1289,20 +1255,6 @@ describe( 'TableUtils', () => {
 	} );
 	} );
 
 
 	describe( 'removeColumns()', () => {
 	describe( 'removeColumns()', () => {
-		beforeEach( () => {
-			return ModelTestEditor.create( {
-				plugins: [ TableUtils ]
-			} ).then( newEditor => {
-				editor = newEditor;
-				model = editor.model;
-				root = model.document.getRoot( 'main' );
-				tableUtils = editor.plugins.get( TableUtils );
-
-				defaultSchema( model.schema );
-				defaultConversion( editor.conversion );
-			} );
-		} );
-
 		describe( 'single row', () => {
 		describe( 'single row', () => {
 			it( 'should remove a given column', () => {
 			it( 'should remove a given column', () => {
 				setData( model, modelTable( [
 				setData( model, modelTable( [

+ 5 - 6
packages/ckeditor5-table/tests/tablewalker.js

@@ -3,11 +3,13 @@
  * 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 { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
 import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
 
-import { defaultConversion, defaultSchema, modelTable } from './_utils/utils';
+import TableEditing from '../src/tableediting';
+import { modelTable } from './_utils/utils';
 
 
 import TableWalker from '../src/tablewalker';
 import TableWalker from '../src/tablewalker';
 
 
@@ -15,15 +17,12 @@ describe( 'TableWalker', () => {
 	let editor, model, doc, root;
 	let editor, model, doc, root;
 
 
 	beforeEach( () => {
 	beforeEach( () => {
-		return ModelTestEditor.create()
+		return ModelTestEditor.create( { plugins: [ Paragraph, TableEditing ] } )
 			.then( newEditor => {
 			.then( newEditor => {
 				editor = newEditor;
 				editor = newEditor;
 				model = editor.model;
 				model = editor.model;
 				doc = model.document;
 				doc = model.document;
 				root = doc.getRoot( 'main' );
 				root = doc.getRoot( 'main' );
-
-				defaultSchema( model.schema );
-				defaultConversion( editor.conversion );
 			} );
 			} );
 	} );
 	} );
 
 

+ 7 - 7
packages/ckeditor5-table/tests/utils/common.js

@@ -4,9 +4,12 @@
  */
  */
 
 
 import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
 import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
-import TableUtils from '../../src/tableutils';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
-import { defaultConversion, defaultSchema, modelTable } from '../_utils/utils';
+
+import TableEditing from '../../src/tableediting';
+import { modelTable } from '../_utils/utils';
+
 import { findAncestor, isHeadingColumnCell } from '../../src/utils/common';
 import { findAncestor, isHeadingColumnCell } from '../../src/utils/common';
 
 
 describe( 'table utils', () => {
 describe( 'table utils', () => {
@@ -14,16 +17,13 @@ describe( 'table utils', () => {
 	beforeEach( () => {
 	beforeEach( () => {
 		return ModelTestEditor
 		return ModelTestEditor
 			.create( {
 			.create( {
-				plugins: [ TableUtils ]
+				plugins: [ Paragraph, TableEditing ]
 			} )
 			} )
 			.then( newEditor => {
 			.then( newEditor => {
 				editor = newEditor;
 				editor = newEditor;
 				model = editor.model;
 				model = editor.model;
 				modelRoot = model.document.getRoot();
 				modelRoot = model.document.getRoot();
-				tableUtils = editor.plugins.get( TableUtils );
-
-				defaultSchema( model.schema );
-				defaultConversion( editor.conversion );
+				tableUtils = editor.plugins.get( 'TableUtils' );
 			} );
 			} );
 	} );
 	} );
 
 

+ 101 - 7
packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widgettypearound.css

@@ -13,6 +13,16 @@
 	--ck-color-widget-type-around-button-icon: var(--ck-color-base-background);
 	--ck-color-widget-type-around-button-icon: var(--ck-color-base-background);
 }
 }
 
 
+@define-mixin ck-widget-type-around-button-visible {
+	opacity: 1;
+	pointer-events: auto;
+}
+
+@define-mixin ck-widget-type-around-button-hidden {
+	opacity: 0;
+	pointer-events: none;
+}
+
 .ck .ck-widget {
 .ck .ck-widget {
 	/*
 	/*
 	 * Styles of the type around buttons
 	 * Styles of the type around buttons
@@ -22,11 +32,10 @@
 		height: var(--ck-widget-type-around-button-size);
 		height: var(--ck-widget-type-around-button-size);
 		background: var(--ck-color-widget-type-around-button);
 		background: var(--ck-color-widget-type-around-button);
 		border-radius: 100px;
 		border-radius: 100px;
-
-		pointer-events: none;
-		opacity: 0;
 		transition: opacity var(--ck-widget-handler-animation-duration) var(--ck-widget-handler-animation-curve), background var(--ck-widget-handler-animation-duration) var(--ck-widget-handler-animation-curve);
 		transition: opacity var(--ck-widget-handler-animation-duration) var(--ck-widget-handler-animation-curve), background var(--ck-widget-handler-animation-duration) var(--ck-widget-handler-animation-curve);
 
 
+		@mixin ck-widget-type-around-button-hidden;
+
 		& svg {
 		& svg {
 			width: 10px;
 			width: 10px;
 			height: 8px;
 			height: 8px;
@@ -77,8 +86,7 @@
 	&.ck-widget_selected,
 	&.ck-widget_selected,
 	&:hover {
 	&:hover {
 		& > .ck-widget__type-around > .ck-widget__type-around__button {
 		& > .ck-widget__type-around > .ck-widget__type-around__button {
-			pointer-events: auto;
-			opacity: 1;
+			@mixin ck-widget-type-around-button-visible;
 		}
 		}
 	}
 	}
 
 
@@ -114,6 +122,75 @@
 	&.ck-widget_with-selection-handle > .ck-widget__type-around > .ck-widget__type-around__button_before {
 	&.ck-widget_with-selection-handle > .ck-widget__type-around > .ck-widget__type-around__button_before {
 		margin-left: 20px;
 		margin-left: 20px;
 	}
 	}
+
+	/*
+	 * Styles for the horizontal "fake caret" which is displayed when the user navigates using the keyboard.
+	 */
+	& .ck-widget__type-around__fake-caret {
+		pointer-events: none;
+		height: 1px;
+		animation: ck-widget-type-around-fake-caret-pulse linear 1s infinite normal forwards;
+
+		/*
+		 * The semit-transparent-outline+background combo improves the contrast
+		 * when the background underneath the fake caret is dark.
+		 */
+		outline: solid 1px hsla(0, 0%, 100%, .5);
+		background: var(--ck-color-base-text);
+	}
+
+	/*
+	 * Styles of the widget when the "fake caret" is blinking (e.g. upon keyboard navigation).
+	 * Despite the widget being physically selected in the model, its outline should disappear.
+	 */
+	&.ck-widget_selected {
+		&.ck-widget_type-around_show-fake-caret_before,
+		&.ck-widget_type-around_show-fake-caret_after {
+			outline-color: transparent;
+		}
+	}
+
+	&.ck-widget_type-around_show-fake-caret_before,
+	&.ck-widget_type-around_show-fake-caret_after {
+		/*
+		 * When the "fake caret" is visible we simulate that the widget is not selected
+		 * (despite being physically selected), so the outline color should be for the
+		 * unselected widget.
+		 */
+		&.ck-widget_selected:hover {
+			outline-color: var(--ck-color-widget-hover-border);
+		}
+
+		/*
+		 * Styles of the type around buttons when the "fake caret" is blinking (e.g. upon keyboard navigation).
+		 * In this state, the type around buttons would collide with the fake carets so they should disappear.
+		 */
+		& > .ck-widget__type-around > .ck-widget__type-around__button {
+			@mixin ck-widget-type-around-button-hidden;
+		}
+
+		/*
+		 * Fake horizontal caret integration with the selection handle. When the caret is visible, simply
+		 * hide the handle because it intersects with the caret (and does not make much sense anyway).
+		 */
+		&.ck-widget_with-selection-handle {
+			&.ck-widget_selected,
+			&.ck-widget_selected:hover {
+				& > .ck-widget__selection-handle {
+					opacity: 0
+				}
+			}
+		}
+
+		/*
+		 * Fake horizontal caret integration with the resize UI. When the caret is visible, simply
+		 * hide the resize UI because it creates too much noise. It can be visible when the user
+		 * hovers the widget, though.
+		 */
+		&.ck-widget_selected.ck-widget_with-resizer > .ck-widget__resizer {
+			opacity: 0
+		}
+	}
 }
 }
 
 
 /*
 /*
@@ -127,8 +204,7 @@
 		&.ck-widget_selected,
 		&.ck-widget_selected,
 		&:hover {
 		&:hover {
 			& > .ck-widget__type-around > .ck-widget__type-around__button {
 			& > .ck-widget__type-around > .ck-widget__type-around__button {
-				pointer-events: none;
-				opacity: 0;
+				@mixin ck-widget-type-around-button-hidden;
 			}
 			}
 		}
 		}
 	}
 	}
@@ -174,3 +250,21 @@
 		box-shadow: 0 0 0 5px hsla(var(--ck-color-focus-border-coordinates), var(--ck-color-widget-type-around-button-radar-start-alpha));
 		box-shadow: 0 0 0 5px hsla(var(--ck-color-focus-border-coordinates), var(--ck-color-widget-type-around-button-radar-start-alpha));
 	}
 	}
 }
 }
+
+@keyframes ck-widget-type-around-fake-caret-pulse {
+	0% {
+		opacity: 1;
+	}
+	49% {
+		opacity: 1;
+	}
+	50% {
+		opacity: 0;
+	}
+	99% {
+		opacity: 0;
+	}
+	100% {
+		opacity: 1;
+	}
+}

+ 15 - 9
packages/ckeditor5-typing/src/utils/injectunsafekeystrokeshandling.js

@@ -65,7 +65,7 @@ export default function injectUnsafeKeystrokesHandling( editor ) {
 			return;
 			return;
 		}
 		}
 
 
-		if ( isSafeKeystroke( evtData ) || doc.selection.isCollapsed ) {
+		if ( isNonTypingKeystroke( evtData ) || doc.selection.isCollapsed ) {
 			return;
 			return;
 		}
 		}
 
 
@@ -154,14 +154,20 @@ for ( let code = 112; code <= 135; code++ ) {
 	safeKeycodes.push( code );
 	safeKeycodes.push( code );
 }
 }
 
 
-// Returns `true` if a keystroke should not cause any content change caused by "typing".
-//
-// Note: This implementation is very simple and will need to be refined with time.
-//
-// @private
-// @param {engine.view.observer.keyObserver.KeyEventData} keyData
-// @returns {Boolean}
-function isSafeKeystroke( keyData ) {
+/**
+ * Returns `true` if a keystroke will **not** result in "typing".
+ *
+ * For instance, keystrokes that result in typing are letters "a-zA-Z", numbers "0-9", delete, backspace, etc.
+ *
+ * Keystrokes that do not cause typing are, for instance, Fn keys (F5, F8, etc.), arrow keys (←, →, ↑, ↓),
+ * Tab (↹), "Windows logo key" (⊞ Win), etc.
+ *
+ * Note: This implementation is very simple and will need to be refined with time.
+ *
+ * @param {module:engine/view/observer/keyobserver~KeyEventData} keyData
+ * @returns {Boolean}
+ */
+export function isNonTypingKeystroke( keyData ) {
 	// Keystrokes which contain Ctrl don't represent typing.
 	// Keystrokes which contain Ctrl don't represent typing.
 	if ( keyData.ctrlKey ) {
 	if ( keyData.ctrlKey ) {
 		return true;
 		return true;

+ 85 - 0
packages/ckeditor5-typing/tests/utils/injectunsafekeystrokeshandling.js

@@ -0,0 +1,85 @@
+/**
+ * @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 {
+	keyCodes
+} from '@ckeditor/ckeditor5-utils/src/keyboard';
+import { isNonTypingKeystroke } from '../../src/utils/injectunsafekeystrokeshandling';
+
+describe( 'unsafe keystroke handling utils', () => {
+	describe( 'isNonTypingKeystroke()', () => {
+		it( 'should return "true" for any keystroke with the Ctrl key', () => {
+			expect( isNonTypingKeystroke( { keyCode: keyCodes.a, ctrlKey: true } ), 'Ctrl+a' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: keyCodes[ 0 ], ctrlKey: true } ), 'Ctrl+0' ).to.be.true;
+		} );
+
+		it( 'should return "true" for all arrow keys', () => {
+			expect( isNonTypingKeystroke( { keyCode: keyCodes.arrowup } ), 'arrow up' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: keyCodes.arrowdown } ), 'arrow down' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: keyCodes.arrowleft } ), 'arrow left' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: keyCodes.arrowright } ), 'arrow right' ).to.be.true;
+		} );
+
+		it( 'should return "true" for function (Fn) keystrokes', () => {
+			expect( isNonTypingKeystroke( { keyCode: keyCodes.f1 } ), 'F1' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: keyCodes.f2 } ), 'F2' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: keyCodes.f3 } ), 'F3' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: keyCodes.f4 } ), 'F4' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: keyCodes.f5 } ), 'F5' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: keyCodes.f6 } ), 'F6' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: keyCodes.f7 } ), 'F7' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: keyCodes.f8 } ), 'F8' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: keyCodes.f9 } ), 'F9' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: keyCodes.f10 } ), 'F10' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: keyCodes.f11 } ), 'F11' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: keyCodes.f12 } ), 'F12' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 124 } ), 'F13' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 125 } ), 'F14' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 126 } ), 'F15' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 127 } ), 'F16' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 128 } ), 'F17' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 129 } ), 'F18' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 130 } ), 'F19' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 131 } ), 'F20' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 132 } ), 'F21' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 133 } ), 'F22' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 134 } ), 'F23' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 135 } ), 'F24' ).to.be.true;
+		} );
+
+		it( 'should return "true" for other safe keystrokes', () => {
+			expect( isNonTypingKeystroke( { keyCode: keyCodes.tab } ), 'Tab' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 16 } ), 'Shift' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 17 } ), 'Ctrl' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 18 } ), 'Alt' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 19 } ), 'Pause' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 20 } ), 'CapsLock' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: keyCodes.esc } ), 'Escape' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 33 } ), 'PageUp' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 34 } ), 'PageDown' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 35 } ), 'Home' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 36 } ), 'End' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 45 } ), 'Insert' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 91 } ), 'Windows' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 93 } ), 'Menu key' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 144 } ), 'NumLock' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 145 } ), 'ScrollLock' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 173 } ), 'Mute/Unmute' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 174 } ), 'Volume up' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 175 } ), 'Volume down' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 176 } ), 'Next song' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 177 } ), 'Previous song' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 178 } ), 'Stop' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 179 } ), 'Play/Pause' ).to.be.true;
+			expect( isNonTypingKeystroke( { keyCode: 255 } ), 'Display brightness (increase and decrease)' ).to.be.true;
+		} );
+
+		it( 'should return "false" for the keystrokes that result in typing', () => {
+			expect( isNonTypingKeystroke( { keyCode: keyCodes.a } ), 'a' ).to.be.false;
+			expect( isNonTypingKeystroke( { keyCode: keyCodes[ 0 ] } ), '0' ).to.be.false;
+			expect( isNonTypingKeystroke( { keyCode: keyCodes.a, altKey: true } ), 'Alt+a' ).to.be.false;
+		} );
+	} );
+} );

+ 61 - 0
packages/ckeditor5-utils/src/keyboard.js

@@ -129,6 +129,67 @@ export function getEnvKeystrokeText( keystroke ) {
 		} );
 		} );
 }
 }
 
 
+/**
+ * Returns `true` if the provided key code represents one of the arrow keys.
+ *
+ * @param {Number} keyCode A key code as in {@link module:utils/keyboard~KeystrokeInfo#keyCode}.
+ * @returns {Boolean}
+ */
+export function isArrowKeyCode( keyCode ) {
+	return keyCode == keyCodes.arrowright ||
+		keyCode == keyCodes.arrowleft ||
+		keyCode == keyCodes.arrowup ||
+		keyCode == keyCodes.arrowdown;
+}
+
+/**
+ * Returns the direction in which the {@link module:engine/model/documentselection~DocumentSelection selection}
+ * will move when a provided arrow key code is pressed considering the language direction of the editor content.
+ *
+ * For instance, in right–to–left (RTL) content languages, pressing the left arrow means moving selection right (forward)
+ * in the model structure. Similarly, pressing the right arrow moves the selection left (backward).
+ *
+ * @param {Number} keyCode A key code as in {@link module:utils/keyboard~KeystrokeInfo#keyCode}.
+ * @param {'ltr'|'rtl'} contentLanguageDirection The content language direction, corresponding to
+ * {@link module:utils/locale~Locale#contentLanguageDirection}.
+ * @returns {'left'|'up'|'right'|'down'} Localized arrow direction.
+ */
+export function getLocalizedArrowKeyCodeDirection( keyCode, contentLanguageDirection ) {
+	const isLtrContent = contentLanguageDirection === 'ltr';
+
+	switch ( keyCode ) {
+		case keyCodes.arrowleft:
+			return isLtrContent ? 'left' : 'right';
+
+		case keyCodes.arrowright:
+			return isLtrContent ? 'right' : 'left';
+
+		case keyCodes.arrowup:
+			return 'up';
+
+		case keyCodes.arrowdown:
+			return 'down';
+	}
+}
+
+/**
+ * Determines if the provided key code moves the {@link module:engine/model/documentselection~DocumentSelection selection}
+ * forward or backward considering the language direction of the editor content.
+ *
+ * For instance, in right–to–left (RTL) languages, pressing the left arrow means moving forward
+ * in the model structure. Similarly, pressing the right arrow moves the selection backward.
+ *
+ * @param {Number} keyCode A key code as in {@link module:utils/keyboard~KeystrokeInfo#keyCode}.
+ * @param {'ltr'|'rtl'} contentLanguageDirection The content language direction, corresponding to
+ * {@link module:utils/locale~Locale#contentLanguageDirection}.
+ * @returns {Boolean}
+ */
+export function isForwardArrowKeyCode( keyCode, contentLanguageDirection ) {
+	const localizedKeyCodeDirection = getLocalizedArrowKeyCodeDirection( keyCode, contentLanguageDirection );
+
+	return localizedKeyCodeDirection === 'down' || localizedKeyCodeDirection === 'right';
+}
+
 function generateKnownKeyCodes() {
 function generateKnownKeyCodes() {
 	const keyCodes = {
 	const keyCodes = {
 		arrowleft: 37,
 		arrowleft: 37,

+ 108 - 1
packages/ckeditor5-utils/tests/keyboard.js

@@ -4,7 +4,15 @@
  */
  */
 
 
 import env from '../src/env';
 import env from '../src/env';
-import { keyCodes, getCode, parseKeystroke, getEnvKeystrokeText } from '../src/keyboard';
+import {
+	keyCodes,
+	getCode,
+	parseKeystroke,
+	getEnvKeystrokeText,
+	isArrowKeyCode,
+	isForwardArrowKeyCode,
+	getLocalizedArrowKeyCodeDirection
+} from '../src/keyboard';
 import { expectToThrowCKEditorError } from './_utils/utils';
 import { expectToThrowCKEditorError } from './_utils/utils';
 
 
 describe( 'Keyboard', () => {
 describe( 'Keyboard', () => {
@@ -160,4 +168,103 @@ describe( 'Keyboard', () => {
 			} );
 			} );
 		} );
 		} );
 	} );
 	} );
+
+	describe( 'isArrowKeyCode()', () => {
+		it( 'should return "true" for right arrow', () => {
+			expect( isArrowKeyCode( keyCodes.arrowright ) ).to.be.true;
+		} );
+
+		it( 'should return "true" for left arrow', () => {
+			expect( isArrowKeyCode( keyCodes.arrowleft ) ).to.be.true;
+		} );
+
+		it( 'should return "true" for up arrow', () => {
+			expect( isArrowKeyCode( keyCodes.arrowup ) ).to.be.true;
+		} );
+
+		it( 'should return "true" for down arrow', () => {
+			expect( isArrowKeyCode( keyCodes.arrowdown ) ).to.be.true;
+		} );
+
+		it( 'should return "false" for non-arrow keystrokes', () => {
+			expect( isArrowKeyCode( keyCodes.a ) ).to.be.false;
+			expect( isArrowKeyCode( keyCodes.ctrl ) ).to.be.false;
+		} );
+	} );
+
+	describe( 'getLocalizedArrowKeyCodeDirection()', () => {
+		describe( 'for a left–to–right content language direction', () => {
+			it( 'should return "left" for left arrow', () => {
+				expect( getLocalizedArrowKeyCodeDirection( keyCodes.arrowleft, 'ltr' ) ).to.equal( 'left' );
+			} );
+
+			it( 'should return "right" for right arrow', () => {
+				expect( getLocalizedArrowKeyCodeDirection( keyCodes.arrowright, 'ltr' ) ).to.equal( 'right' );
+			} );
+
+			it( 'should return "up" for up arrow', () => {
+				expect( getLocalizedArrowKeyCodeDirection( keyCodes.arrowup, 'ltr' ) ).to.equal( 'up' );
+			} );
+
+			it( 'should return "down" for down arrow', () => {
+				expect( getLocalizedArrowKeyCodeDirection( keyCodes.arrowdown, 'ltr' ) ).to.equal( 'down' );
+			} );
+		} );
+
+		describe( 'for a right-to-left content language direction', () => {
+			it( 'should return "right" for left arrow', () => {
+				expect( getLocalizedArrowKeyCodeDirection( keyCodes.arrowleft, 'rtl' ) ).to.equal( 'right' );
+			} );
+
+			it( 'should return "left" for right arrow', () => {
+				expect( getLocalizedArrowKeyCodeDirection( keyCodes.arrowright, 'rtl' ) ).to.equal( 'left' );
+			} );
+
+			it( 'should return "up" for up arrow', () => {
+				expect( getLocalizedArrowKeyCodeDirection( keyCodes.arrowup, 'rtl' ) ).to.equal( 'up' );
+			} );
+
+			it( 'should return "down" for down arrow', () => {
+				expect( getLocalizedArrowKeyCodeDirection( keyCodes.arrowdown, 'rtl' ) ).to.equal( 'down' );
+			} );
+		} );
+	} );
+
+	describe( 'isForwardArrowKeyCode()', () => {
+		describe( 'for a left–to–right content language direction', () => {
+			it( 'should return "true" for down arrow', () => {
+				expect( isForwardArrowKeyCode( keyCodes.arrowdown, 'ltr' ) ).to.be.true;
+			} );
+
+			it( 'should return "true" for right arrow', () => {
+				expect( isForwardArrowKeyCode( keyCodes.arrowright, 'ltr' ) ).to.be.true;
+			} );
+
+			it( 'should return "false" for up arrow', () => {
+				expect( isForwardArrowKeyCode( keyCodes.arrowup, 'ltr' ) ).to.be.false;
+			} );
+
+			it( 'should return "false" for left arrow', () => {
+				expect( isForwardArrowKeyCode( keyCodes.arrowleft, 'ltr' ) ).to.be.false;
+			} );
+		} );
+
+		describe( 'for a right-to-left content language direction', () => {
+			it( 'should return "true" for down arrow', () => {
+				expect( isForwardArrowKeyCode( keyCodes.arrowdown, 'rtl' ) ).to.be.true;
+			} );
+
+			it( 'should return "true" for left arrow', () => {
+				expect( isForwardArrowKeyCode( keyCodes.arrowleft, 'rtl' ) ).to.be.true;
+			} );
+
+			it( 'should return "false" for up arrow', () => {
+				expect( isForwardArrowKeyCode( keyCodes.arrowup, 'rtl' ) ).to.be.false;
+			} );
+
+			it( 'should return "false" for right arrow', () => {
+				expect( isForwardArrowKeyCode( keyCodes.arrowright, 'rtl' ) ).to.be.false;
+			} );
+		} );
+	} );
 } );
 } );

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

@@ -11,6 +11,7 @@
   "dependencies": {
   "dependencies": {
     "@ckeditor/ckeditor5-core": "^19.0.1",
     "@ckeditor/ckeditor5-core": "^19.0.1",
     "@ckeditor/ckeditor5-engine": "^19.0.1",
     "@ckeditor/ckeditor5-engine": "^19.0.1",
+    "@ckeditor/ckeditor5-typing": "^19.0.1",
     "@ckeditor/ckeditor5-ui": "^19.0.1",
     "@ckeditor/ckeditor5-ui": "^19.0.1",
     "@ckeditor/ckeditor5-utils": "^19.0.1",
     "@ckeditor/ckeditor5-utils": "^19.0.1",
     "lodash-es": "^4.17.15"
     "lodash-es": "^4.17.15"
@@ -25,6 +26,7 @@
     "@ckeditor/ckeditor5-essentials": "^19.0.1",
     "@ckeditor/ckeditor5-essentials": "^19.0.1",
     "@ckeditor/ckeditor5-heading": "^19.0.1",
     "@ckeditor/ckeditor5-heading": "^19.0.1",
     "@ckeditor/ckeditor5-horizontal-line": "^19.0.1",
     "@ckeditor/ckeditor5-horizontal-line": "^19.0.1",
+    "@ckeditor/ckeditor5-image": "^19.0.1",
     "@ckeditor/ckeditor5-media-embed": "^19.1.0",
     "@ckeditor/ckeditor5-media-embed": "^19.1.0",
     "@ckeditor/ckeditor5-paragraph": "^19.1.0",
     "@ckeditor/ckeditor5-paragraph": "^19.1.0",
     "@ckeditor/ckeditor5-table": "^19.1.0",
     "@ckeditor/ckeditor5-table": "^19.1.0",

+ 100 - 114
packages/ckeditor5-widget/src/widget.js

@@ -11,10 +11,14 @@ 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 WidgetTypeAround from './widgettypearound/widgettypearound';
 import WidgetTypeAround from './widgettypearound/widgettypearound';
 import { getLabel, isWidget, WIDGET_SELECTED_CLASS_NAME } from './utils';
 import { getLabel, isWidget, WIDGET_SELECTED_CLASS_NAME } from './utils';
-import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
+import {
+	isArrowKeyCode,
+	isForwardArrowKeyCode
+} from '@ckeditor/ckeditor5-utils/src/keyboard';
 import env from '@ckeditor/ckeditor5-utils/src/env';
 import env from '@ckeditor/ckeditor5-utils/src/env';
 
 
 import '../theme/widget.css';
 import '../theme/widget.css';
+import priorities from '@ckeditor/ckeditor5-utils/src/priorities';
 
 
 /**
 /**
  * The widget plugin. It enables base support for widgets.
  * The widget plugin. It enables base support for widgets.
@@ -96,8 +100,24 @@ export default class Widget extends Plugin {
 		view.addObserver( MouseObserver );
 		view.addObserver( MouseObserver );
 		this.listenTo( viewDocument, 'mousedown', ( ...args ) => this._onMousedown( ...args ) );
 		this.listenTo( viewDocument, 'mousedown', ( ...args ) => this._onMousedown( ...args ) );
 
 
-		// Handle custom keydown behaviour.
-		this.listenTo( viewDocument, 'keydown', ( ...args ) => this._onKeydown( ...args ), { priority: 'high' } );
+		// There are two keydown listeners working on different priorities. This allows other
+		// features such as WidgetTypeAround or TableKeyboard to attach their listeners in between
+		// and customize the behavior even further in different content/selection scenarios.
+		//
+		// * The first listener handles changing the selection on arrow key press
+		// if the widget is selected or if the selection is next to a widget and the widget
+		// should become selected upon the arrow key press.
+		//
+		// * The second (late) listener makes sure the default browser action on arrow key press is
+		// prevented when a widget is selected. This prevents the selection from being moved
+		// from a fake selection container.
+		this.listenTo( viewDocument, 'keydown', ( ...args ) => {
+			this._handleSelectionChangeOnArrowKeyPress( ...args );
+		}, { priority: 'high' } );
+
+		this.listenTo( viewDocument, 'keydown', ( ...args ) => {
+			this._preventDefaultOnArrowKeyPress( ...args );
+		}, { priority: priorities.get( 'high' ) - 20 } );
 
 
 		// Handle custom delete behaviour.
 		// Handle custom delete behaviour.
 		this.listenTo( viewDocument, 'delete', ( evt, data ) => {
 		this.listenTo( viewDocument, 'delete', ( evt, data ) => {
@@ -162,27 +182,92 @@ export default class Widget extends Plugin {
 	}
 	}
 
 
 	/**
 	/**
-	 * Handles {@link module:engine/view/document~Document#event:keydown keydown} events.
+	 * Handles {@link module:engine/view/document~Document#event:keydown keydown} events and changes
+	 * the model selection when:
+	 *
+	 * * arrow key is pressed when the widget is selected,
+	 * * the selection is next to a widget and the widget should become selected upon the arrow key press.
+	 *
+	 * See {@link #_preventDefaultOnArrowKeyPress}.
+	 *
+	 * @private
+	 * @param {module:utils/eventinfo~EventInfo} eventInfo
+	 * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData
+	 */
+	_handleSelectionChangeOnArrowKeyPress( eventInfo, domEventData ) {
+		const keyCode = domEventData.keyCode;
+
+		// Checks if the keys were handled and then prevents the default event behaviour and stops
+		// the propagation.
+		if ( !isArrowKeyCode( keyCode ) ) {
+			return;
+		}
+
+		const model = this.editor.model;
+		const schema = model.schema;
+		const modelSelection = model.document.selection;
+		const objectElement = modelSelection.getSelectedElement();
+		const isForward = isForwardArrowKeyCode( keyCode, this.editor.locale.contentLanguageDirection );
+
+		// If object element is selected.
+		if ( objectElement && schema.isObject( objectElement ) ) {
+			const position = isForward ? modelSelection.getLastPosition() : modelSelection.getFirstPosition();
+			const newRange = schema.getNearestSelectionRange( position, isForward ? 'forward' : 'backward' );
+
+			if ( newRange ) {
+				model.change( writer => {
+					writer.setSelection( newRange );
+				} );
+
+				domEventData.preventDefault();
+				eventInfo.stop();
+			}
+
+			return;
+		}
+
+		// If selection is next to object element.
+		// Return if not collapsed.
+		if ( !modelSelection.isCollapsed ) {
+			return;
+		}
+
+		const objectElementNextToSelection = this._getObjectElementNextToSelection( isForward );
+
+		if ( objectElementNextToSelection && schema.isObject( objectElementNextToSelection ) ) {
+			this._setSelectionOverElement( objectElementNextToSelection );
+
+			domEventData.preventDefault();
+			eventInfo.stop();
+		}
+	}
+
+	/**
+	 * Handles {@link module:engine/view/document~Document#event:keydown keydown} events and prevents
+	 * the default browser behavior to make sure the fake selection is not being moved from a fake selection
+	 * container.
+	 *
+	 * See {@link #_handleSelectionChangeOnArrowKeyPress}.
 	 *
 	 *
 	 * @private
 	 * @private
 	 * @param {module:utils/eventinfo~EventInfo} eventInfo
 	 * @param {module:utils/eventinfo~EventInfo} eventInfo
 	 * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData
 	 * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData
 	 */
 	 */
-	_onKeydown( eventInfo, domEventData ) {
+	_preventDefaultOnArrowKeyPress( eventInfo, domEventData ) {
 		const keyCode = domEventData.keyCode;
 		const keyCode = domEventData.keyCode;
-		const isLtrContent = this.editor.locale.contentLanguageDirection === 'ltr';
-		const isForward = keyCode == keyCodes.arrowdown || keyCode == keyCodes[ isLtrContent ? 'arrowright' : 'arrowleft' ];
-		let wasHandled = false;
 
 
 		// Checks if the keys were handled and then prevents the default event behaviour and stops
 		// Checks if the keys were handled and then prevents the default event behaviour and stops
 		// the propagation.
 		// the propagation.
-		if ( isArrowKeyCode( keyCode ) ) {
-			wasHandled = this._handleArrowKeys( isForward );
-		} else if ( keyCode === keyCodes.enter ) {
-			wasHandled = this._handleEnterKey( domEventData.shiftKey );
+		if ( !isArrowKeyCode( keyCode ) ) {
+			return;
 		}
 		}
 
 
-		if ( wasHandled ) {
+		const model = this.editor.model;
+		const schema = model.schema;
+		const objectElement = model.document.selection.getSelectedElement();
+
+		// If object element is selected.
+		if ( objectElement && schema.isObject( objectElement ) ) {
 			domEventData.preventDefault();
 			domEventData.preventDefault();
 			eventInfo.stop();
 			eventInfo.stop();
 		}
 		}
@@ -230,90 +315,10 @@ export default class Widget extends Plugin {
 		}
 		}
 	}
 	}
 
 
-	/**
-	 * Handles arrow keys.
-	 *
-	 * @private
-	 * @param {Boolean} isForward Set to true if arrow key should be handled in forward direction.
-	 * @returns {Boolean|undefined} Returns `true` if keys were handled correctly.
-	 */
-	_handleArrowKeys( isForward ) {
-		const model = this.editor.model;
-		const schema = model.schema;
-		const modelDocument = model.document;
-		const modelSelection = modelDocument.selection;
-		const objectElement = modelSelection.getSelectedElement();
-
-		// If object element is selected.
-		if ( objectElement && schema.isObject( objectElement ) ) {
-			const position = isForward ? modelSelection.getLastPosition() : modelSelection.getFirstPosition();
-			const newRange = schema.getNearestSelectionRange( position, isForward ? 'forward' : 'backward' );
-
-			if ( newRange ) {
-				model.change( writer => {
-					writer.setSelection( newRange );
-				} );
-			}
-
-			return true;
-		}
-
-		// If selection is next to object element.
-		// Return if not collapsed.
-		if ( !modelSelection.isCollapsed ) {
-			return;
-		}
-
-		const objectElement2 = this._getObjectElementNextToSelection( isForward );
-
-		if ( !!objectElement2 && schema.isObject( objectElement2 ) ) {
-			this._setSelectionOverElement( objectElement2 );
-
-			return true;
-		}
-	}
-
-	/**
-	 * Handles the enter key, giving users and access to positions in the editable directly before
-	 * (<kbd>Shift</kbd>+<kbd>Enter</kbd>) or after (<kbd>Enter</kbd>) the selected widget.
-	 * It improves the UX, mainly when the widget is the first or last child of the root editable
-	 * and there's no other way to type after or before it.
-	 *
-	 * @private
-	 * @param {Boolean} isBackwards Set to true if the new paragraph is to be inserted before
-	 * the selected widget (<kbd>Shift</kbd>+<kbd>Enter</kbd>).
-	 * @returns {Boolean|undefined} Returns `true` if keys were handled correctly.
-	 */
-	_handleEnterKey( isBackwards ) {
-		const model = this.editor.model;
-		const modelSelection = model.document.selection;
-		const selectedElement = modelSelection.getSelectedElement();
-
-		if ( shouldInsertParagraph( selectedElement, model.schema ) ) {
-			model.change( writer => {
-				let position = writer.createPositionAt( selectedElement, isBackwards ? 'before' : 'after' );
-				const paragraph = writer.createElement( 'paragraph' );
-
-				// Split the parent when inside a block element.
-				// https://github.com/ckeditor/ckeditor5/issues/1529
-				if ( model.schema.isBlock( selectedElement.parent ) ) {
-					const paragraphLimit = model.schema.findAllowedParent( position, paragraph );
-
-					position = writer.split( position, paragraphLimit ).position;
-				}
-
-				writer.insert( paragraph, position );
-				writer.setSelection( paragraph, 'in' );
-			} );
-
-			return true;
-		}
-	}
-
 	/**
 	/**
 	 * Sets {@link module:engine/model/selection~Selection document's selection} over given element.
 	 * Sets {@link module:engine/model/selection~Selection document's selection} over given element.
 	 *
 	 *
-	 * @private
+	 * @protected
 	 * @param {module:engine/model/element~Element} element
 	 * @param {module:engine/model/element~Element} element
 	 */
 	 */
 	_setSelectionOverElement( element ) {
 	_setSelectionOverElement( element ) {
@@ -327,7 +332,7 @@ export default class Widget extends Plugin {
 	 * {@link module:engine/model/selection~Selection model selection} exists and is marked in
 	 * {@link module:engine/model/selection~Selection model selection} exists and is marked in
 	 * {@link module:engine/model/schema~Schema schema} as `object`.
 	 * {@link module:engine/model/schema~Schema schema} as `object`.
 	 *
 	 *
-	 * @private
+	 * @protected
 	 * @param {Boolean} forward Direction of checking.
 	 * @param {Boolean} forward Direction of checking.
 	 * @returns {module:engine/model/element~Element|null}
 	 * @returns {module:engine/model/element~Element|null}
 	 */
 	 */
@@ -364,17 +369,6 @@ export default class Widget extends Plugin {
 	}
 	}
 }
 }
 
 
-// Returns 'true' if provided key code represents one of the arrow keys.
-//
-// @param {Number} keyCode
-// @returns {Boolean}
-function isArrowKeyCode( keyCode ) {
-	return keyCode == keyCodes.arrowright ||
-		keyCode == keyCodes.arrowleft ||
-		keyCode == keyCodes.arrowup ||
-		keyCode == keyCodes.arrowdown;
-}
-
 // Returns `true` when element is a nested editable or is placed inside one.
 // Returns `true` when element is a nested editable or is placed inside one.
 //
 //
 // @param {module:engine/view/element~Element}
 // @param {module:engine/view/element~Element}
@@ -408,11 +402,3 @@ function isChild( element, parent ) {
 
 
 	return Array.from( element.getAncestors() ).includes( parent );
 	return Array.from( element.getAncestors() ).includes( parent );
 }
 }
-
-// Checks if enter key should insert paragraph. This should be done only on elements of type object (excluding inline objects).
-//
-// @param {module:engine/model/element~Element} element And element to check.
-// @param {module:engine/model/schema~Schema} schema
-function shouldInsertParagraph( element, schema ) {
-	return element && schema.isObject( element ) && !schema.isInline( element );
-}

+ 18 - 1
packages/ckeditor5-widget/src/widgettoolbarrepository.js

@@ -7,6 +7,8 @@
  * @module widget/widgettoolbarrepository
  * @module widget/widgettoolbarrepository
  */
  */
 
 
+/* global console */
+
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import ContextualBalloon from '@ckeditor/ckeditor5-ui/src/panel/balloon/contextualballoon';
 import ContextualBalloon from '@ckeditor/ckeditor5-ui/src/panel/balloon/contextualballoon';
 import ToolbarView from '@ckeditor/ckeditor5-ui/src/toolbar/toolbarview';
 import ToolbarView from '@ckeditor/ckeditor5-ui/src/toolbar/toolbarview';
@@ -15,7 +17,7 @@ import {
 	isWidget,
 	isWidget,
 	centeredBalloonPositionForLongWidgets
 	centeredBalloonPositionForLongWidgets
 } from './utils';
 } from './utils';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+import CKEditorError, { attachLinkToDocumentation } from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 
 /**
 /**
  * Widget toolbar repository plugin. A central point for registering widget toolbars. This plugin handles the whole
  * Widget toolbar repository plugin. A central point for registering widget toolbars. This plugin handles the whole
@@ -124,6 +126,21 @@ export default class WidgetToolbarRepository extends Plugin {
 	 * @param {String} [options.balloonClassName='ck-toolbar-container'] CSS class for the widget balloon.
 	 * @param {String} [options.balloonClassName='ck-toolbar-container'] CSS class for the widget balloon.
 	 */
 	 */
 	register( toolbarId, { ariaLabel, items, getRelatedElement, balloonClassName = 'ck-toolbar-container' } ) {
 	register( toolbarId, { ariaLabel, items, getRelatedElement, balloonClassName = 'ck-toolbar-container' } ) {
+		// Trying to register a toolbar without any item.
+		if ( !items.length ) {
+			/**
+			 * When {@link #register} a new toolbar, you need to provide a non-empty array with
+			 * the items that will be inserted into the toolbar.
+			 *
+			 * @error widget-toolbar-no-items
+			 */
+			console.warn(
+				attachLinkToDocumentation( 'widget-toolbar-no-items: Trying to register a toolbar without items.' ), { toolbarId }
+			);
+
+			return;
+		}
+
 		const editor = this.editor;
 		const editor = this.editor;
 		const t = editor.t;
 		const t = editor.t;
 		const toolbarView = new ToolbarView( editor.locale );
 		const toolbarView = new ToolbarView( editor.locale );

+ 0 - 38
packages/ckeditor5-widget/src/widgettypearound/utils.js

@@ -55,41 +55,3 @@ export function getClosestWidgetViewElement( domElement, domConverter ) {
 
 
 	return domConverter.mapDomToView( widgetDomElement );
 	return domConverter.mapDomToView( widgetDomElement );
 }
 }
-
-/**
- * For the passed widget view element, this helper returns an array of positions which
- * correspond to the "tight spots" around the widget which cannot be accessed due to
- * limitations of selection rendering in web browsers.
- *
- * @param {module:engine/view/element~Element} widgetViewElement
- * @returns {Array.<String>}
- */
-export function getWidgetTypeAroundPositions( widgetViewElement ) {
-	const positions = [];
-
-	if ( isFirstChild( widgetViewElement ) || hasPreviousWidgetSibling( widgetViewElement ) ) {
-		positions.push( 'before' );
-	}
-
-	if ( isLastChild( widgetViewElement ) || hasNextWidgetSibling( widgetViewElement ) ) {
-		positions.push( 'after' );
-	}
-
-	return positions;
-}
-
-function isFirstChild( widget ) {
-	return !widget.previousSibling;
-}
-
-function isLastChild( widget ) {
-	return !widget.nextSibling;
-}
-
-function hasPreviousWidgetSibling( widget ) {
-	return widget.previousSibling && isWidget( widget.previousSibling );
-}
-
-function hasNextWidgetSibling( widget ) {
-	return widget.nextSibling && isWidget( widget.nextSibling );
-}

+ 502 - 52
packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js

@@ -10,17 +10,25 @@
  */
  */
 
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
-import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import Template from '@ckeditor/ckeditor5-ui/src/template';
 import Template from '@ckeditor/ckeditor5-ui/src/template';
+import {
+	isArrowKeyCode,
+	isForwardArrowKeyCode,
+	keyCodes
+} from '@ckeditor/ckeditor5-utils/src/keyboard';
+import priorities from '@ckeditor/ckeditor5-utils/src/priorities';
 
 
 import {
 import {
 	isTypeAroundWidget,
 	isTypeAroundWidget,
-	getWidgetTypeAroundPositions,
 	getClosestTypeAroundDomButton,
 	getClosestTypeAroundDomButton,
 	getTypeAroundButtonPosition,
 	getTypeAroundButtonPosition,
 	getClosestWidgetViewElement
 	getClosestWidgetViewElement
 } from './utils';
 } from './utils';
 
 
+import {
+	isNonTypingKeystroke
+} from '@ckeditor/ckeditor5-typing/src/utils/injectunsafekeystrokeshandling';
+
 import returnIcon from '../../theme/icons/return-arrow.svg';
 import returnIcon from '../../theme/icons/return-arrow.svg';
 import '../../theme/widgettypearound.css';
 import '../../theme/widgettypearound.css';
 
 
@@ -29,6 +37,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 TYPE_AROUND_SELECTION_ATTRIBUTE = 'widget-type-around';
+
 /**
 /**
  * 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
@@ -43,13 +53,6 @@ const RETURN_ARROW_ICON_ELEMENT = new DOMParser().parseFromString( returnIcon, '
  * @private
  * @private
  */
  */
 export default class WidgetTypeAround extends Plugin {
 export default class WidgetTypeAround extends Plugin {
-	/**
-	 * @inheritDoc
-	 */
-	static get requires() {
-		return [ Paragraph ];
-	}
-
 	/**
 	/**
 	 * @inheritDoc
 	 * @inheritDoc
 	 */
 	 */
@@ -64,32 +67,33 @@ export default class WidgetTypeAround extends Plugin {
 		super( editor );
 		super( editor );
 
 
 		/**
 		/**
-		 * A set containing all widgets in all editor roots that have the type around UI injected in
-		 * {@link #_enableTypeAroundUIInjection}.
-		 *
-		 * Keeping track of them saves time, for instance, when updating their CSS classes.
+		 * A reference to the model widget element that has the "fake caret" active
+		 * on either side of it. It is later used to remove CSS classes associated with the "fake caret"
+		 * when the widget no longer needs it.
 		 *
 		 *
 		 * @private
 		 * @private
-		 * @readonly
-		 * @member {Set} #_widgetsWithTypeAroundUI
+		 * @member {module:engine/model/element~Element|null}
 		 */
 		 */
-		this._widgetsWithTypeAroundUI = new Set();
+		this._currentFakeCaretModelElement = null;
 	}
 	}
 
 
 	/**
 	/**
 	 * @inheritDoc
 	 * @inheritDoc
 	 */
 	 */
-	destroy() {
-		this._widgetsWithTypeAroundUI.clear();
+	init() {
+		this._enableTypeAroundUIInjection();
+		this._enableInsertingParagraphsOnButtonClick();
+		this._enableInsertingParagraphsOnEnterKeypress();
+		this._enableInsertingParagraphsOnTypingKeystroke();
+		this._enableTypeAroundFakeCaretActivationUsingKeyboardArrows();
+		this._enableDeleteIntegration();
 	}
 	}
 
 
 	/**
 	/**
 	 * @inheritDoc
 	 * @inheritDoc
 	 */
 	 */
-	init() {
-		this._enableTypeAroundUIInjection();
-		this._enableDetectionOfTypeAroundWidgets();
-		this._enableInsertingParagraphsOnButtonClick();
+	destroy() {
+		this._currentFakeCaretModelElement = null;
 	}
 	}
 
 
 	/**
 	/**
@@ -99,13 +103,12 @@ export default class WidgetTypeAround extends Plugin {
 	 * the viewport to the selection in the inserted paragraph.
 	 * the viewport to the selection in the inserted paragraph.
 	 *
 	 *
 	 * @protected
 	 * @protected
-	 * @param {module:engine/view/element~Element} widgetViewElement The view widget element next to which a paragraph is inserted.
+	 * @param {module:engine/model/element~Element} widgetModelElement The model widget element next to which a paragraph is inserted.
 	 * @param {'before'|'after'} position The position where the paragraph is inserted. Either `'before'` or `'after'` the widget.
 	 * @param {'before'|'after'} position The position where the paragraph is inserted. Either `'before'` or `'after'` the widget.
 	 */
 	 */
-	_insertParagraph( widgetViewElement, position ) {
+	_insertParagraph( widgetModelElement, position ) {
 		const editor = this.editor;
 		const editor = this.editor;
 		const editingView = editor.editing.view;
 		const editingView = editor.editing.view;
-		const widgetModelElement = editor.editing.mapper.toModelElement( widgetViewElement );
 		let modelPosition;
 		let modelPosition;
 
 
 		if ( position === 'before' ) {
 		if ( position === 'before' ) {
@@ -122,6 +125,35 @@ export default class WidgetTypeAround extends Plugin {
 		editingView.scrollToTheSelection();
 		editingView.scrollToTheSelection();
 	}
 	}
 
 
+	/**
+	 * 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
+	 * according to the "widget-type-around" model selection attribute value.
+	 *
+	 * Because this method requires the "widget-type-around" attribute to be set,
+	 * the insertion can only happen when the widget "fake caret" is active (e.g. activated
+	 * using the keyboard).
+	 *
+	 * @private
+	 * @returns {Boolean} Returns `true` when the paragraph was inserted (the attribute was present) and `false` otherwise.
+	 */
+	_insertParagraphAccordingToSelectionAttribute() {
+		const editor = this.editor;
+		const model = editor.model;
+		const modelSelection = model.document.selection;
+		const typeAroundSelectionAttributeValue = modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE );
+
+		if ( !typeAroundSelectionAttributeValue ) {
+			return false;
+		}
+
+		const selectedModelElement = modelSelection.getSelectedElement();
+
+		this._insertParagraph( selectedModelElement, typeAroundSelectionAttributeValue );
+
+		return true;
+	}
+
 	/**
 	/**
 	 * Creates a listener in the editing conversion pipeline that injects the type around
 	 * Creates a listener in the editing conversion pipeline that injects the type around
 	 * UI into every single widget instance created in the editor.
 	 * UI into every single widget instance created in the editor.
@@ -146,49 +178,260 @@ export default class WidgetTypeAround extends Plugin {
 			// Filter out non-widgets and inline widgets.
 			// Filter out non-widgets and inline widgets.
 			if ( isTypeAroundWidget( viewElement, data.item, schema ) ) {
 			if ( isTypeAroundWidget( viewElement, data.item, schema ) ) {
 				injectUIIntoWidget( conversionApi.writer, buttonTitles, viewElement );
 				injectUIIntoWidget( conversionApi.writer, buttonTitles, viewElement );
-
-				// Keep track of widgets that have the type around UI injected.
-				// In the #_enableDetectionOfTypeAroundWidgets() we will iterate only over these
-				// widgets instead of all children of the root. This should improve the performance.
-				this._widgetsWithTypeAroundUI.add( viewElement );
 			}
 			}
 		}, { priority: 'low' } );
 		}, { priority: 'low' } );
 	}
 	}
 
 
 	/**
 	/**
-	 * Registers an editing view post-fixer which checks all block widgets in the content
-	 * and adds CSS classes to these which should have the typing around (UI) enabled
-	 * and visible for the users.
+	 * Brings support for the "fake caret" that appears when either:
+	 *
+	 * * the selection moves from a position next to a widget (to a widget) using arrow keys,
+	 * * the arrow key is pressed when the widget is already selected.
+	 *
+	 * The "fake caret" lets the user know that they can start typing or just press
+	 * enter to insert a paragraph at the position next to a widget as suggested by the "fake caret".
+	 *
+	 * The "fake caret" disappears when the user changes the selection or the editor
+	 * gets blurred.
+	 *
+	 * The whole idea is as follows:
+	 *
+	 * 1. A user does one of the 2 scenarios described at the beginning.
+	 * 2. The "keydown" listener is executed and the decision is made whether to show or hide the "fake caret".
+	 * 3. If it should show up, the "widget-type-around" model selection attribute is set indicating
+	 *    on which side of the widget it should appear.
+	 * 4. The selection dispatcher reacts to the selection attribute and sets CSS classes responsible for the
+	 *    "fake caret" on the view widget.
+	 * 5. If the "fake caret" should disappear, the selection attribute is removed and the dispatcher
+	 *    does the CSS class clean-up in the view.
+	 * 6. Additionally, "change:range" and FocusTracker#isFocused listeners also remove the selection
+	 *    attribute (the former also removes widget CSS classes).
 	 *
 	 *
 	 * @private
 	 * @private
 	 */
 	 */
-	_enableDetectionOfTypeAroundWidgets() {
+	_enableTypeAroundFakeCaretActivationUsingKeyboardArrows() {
 		const editor = this.editor;
 		const editor = this.editor;
+		const model = editor.model;
+		const modelSelection = model.document.selection;
+		const schema = model.schema;
 		const editingView = editor.editing.view;
 		const editingView = editor.editing.view;
 
 
+		// This is the main listener responsible for the "fake caret".
+		// Note: The priority must precede the default Widget class keydown handler ("high") and the
+		// TableKeyboard keydown handler ("high-10").
+		editingView.document.on( 'keydown', ( evt, domEventData ) => {
+			if ( isArrowKeyCode( domEventData.keyCode ) ) {
+				this._handleArrowKeyPress( evt, domEventData );
+			}
+		}, { priority: priorities.get( 'high' ) + 10 } );
+
+		// This listener makes sure the widget type around selection attribute will be gone from the model
+		// 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),
+		// let's get rid of the attribute so that the selection downcast dispatcher isn't even bothered.
+		modelSelection.on( 'change:range', ( evt, data ) => {
+			// Do not reset the selection attribute when the change was indirect.
+			if ( !data.directChange ) {
+				return;
+			}
+
+			const typeAroundSelectionAttribute = modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE );
+
+			if ( !typeAroundSelectionAttribute ) {
+				return;
+			}
+
+			// Get rid of the widget type around attribute of the selection on every change:range.
+			// If the range changes, it means for sure, the user is no longer in the active ("fake horizontal caret") mode.
+			editor.model.change( writer => {
+				writer.removeSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE );
+			} );
+		} );
+
+		// 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
+		// CSS classes associated with the active ("fake horizontal caret") mode of the widget.
+		editor.editing.downcastDispatcher.on( 'selection', ( evt, data, conversionApi ) => {
+			const writer = conversionApi.writer;
+
+			if ( this._currentFakeCaretModelElement ) {
+				const selectedViewElement = conversionApi.mapper.toViewElement( this._currentFakeCaretModelElement );
+
+				if ( selectedViewElement ) {
+					// Get rid of CSS classes associated with the active ("fake horizontal caret") mode from the view widget.
+					writer.removeClass( POSSIBLE_INSERTION_POSITIONS.map( positionToWidgetCssClass ), selectedViewElement );
+
+					this._currentFakeCaretModelElement = null;
+				}
+			}
+
+			const selectedModelElement = data.selection.getSelectedElement();
+
+			if ( !selectedModelElement ) {
+				return;
+			}
+
+			const selectedViewElement = conversionApi.mapper.toViewElement( selectedModelElement );
+
+			if ( !isTypeAroundWidget( selectedViewElement, selectedModelElement, schema ) ) {
+				return;
+			}
+
+			const typeAroundSelectionAttribute = data.selection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE );
+
+			if ( !typeAroundSelectionAttribute ) {
+				return;
+			}
+
+			writer.addClass( positionToWidgetCssClass( typeAroundSelectionAttribute ), selectedViewElement );
+
+			// Remember the view widget that got the "fake-caret" CSS class. This class should be removed ASAP when the
+			// selection changes
+			this._currentFakeCaretModelElement = selectedModelElement;
+		} );
+
+		this.listenTo( editor.ui.focusTracker, 'change:isFocused', ( evt, name, isFocused ) => {
+			if ( !isFocused ) {
+				editor.model.change( writer => {
+					writer.removeSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE );
+				} );
+			}
+		} );
+
 		function positionToWidgetCssClass( position ) {
 		function positionToWidgetCssClass( position ) {
-			return `ck-widget_can-type-around_${ position }`;
+			return `ck-widget_type-around_show-fake-caret_${ position }`;
+		}
+	}
+
+	/**
+	 * A listener executed on each "keydown" in the view document, a part of
+	 * {@link #_enableTypeAroundFakeCaretActivationUsingKeyboardArrows}.
+	 *
+	 * It decides whether the arrow key press should activate the "fake caret" or not (also whether it should
+	 * be deactivated).
+	 *
+	 * The "fake caret" activation is done by setting the "widget-type-around" model selection attribute
+	 * in this listener and stopping&preventing the event that would normally be handled by the Widget
+	 * plugin that is responsible for the regular keyboard navigation near/across all widgets (that
+	 * includes inline widgets, which are ignored by the WidgetTypeAround plugin).
+	 *
+	 * @private
+	 */
+	_handleArrowKeyPress( evt, domEventData ) {
+		const editor = this.editor;
+		const model = editor.model;
+		const modelSelection = model.document.selection;
+		const schema = model.schema;
+		const editingView = editor.editing.view;
+
+		const keyCode = domEventData.keyCode;
+		const isForward = isForwardArrowKeyCode( keyCode, editor.locale.contentLanguageDirection );
+		const selectedViewElement = editingView.document.selection.getSelectedElement();
+		const selectedModelElement = editor.editing.mapper.toModelElement( selectedViewElement );
+		let shouldStopAndPreventDefault;
+
+		// Handle keyboard navigation when a type-around-compatible widget is currently selected.
+		if ( isTypeAroundWidget( selectedViewElement, selectedModelElement, schema ) ) {
+			shouldStopAndPreventDefault = this._handleArrowKeyPressOnSelectedWidget( isForward );
+		}
+		// Handle keyboard arrow navigation when the selection is next to a type-around-compatible widget
+		// and the widget is about to be selected.
+		else if ( modelSelection.isCollapsed ) {
+			shouldStopAndPreventDefault = this._handleArrowKeyPressWhenSelectionNextToAWidget( isForward );
+		}
+
+		if ( shouldStopAndPreventDefault ) {
+			domEventData.preventDefault();
+			evt.stop();
 		}
 		}
+	}
 
 
-		editingView.document.registerPostFixer( writer => {
-			for ( const widgetViewElement of this._widgetsWithTypeAroundUI ) {
-				// If the widget is no longer attached to the root (for instance, because it was removed),
-				// there is no need to update its classes and we can safely forget about it.
-				if ( !widgetViewElement.isAttached() ) {
-					this._widgetsWithTypeAroundUI.delete( widgetViewElement );
-				} else {
-					// Update widgets' classes depending on possible positions for paragraph insertion.
-					const positions = getWidgetTypeAroundPositions( widgetViewElement );
-
-					// Remove all classes. In theory we could remove only these that will not be added a few lines later,
-					// but since there are only two... KISS.
-					writer.removeClass( POSSIBLE_INSERTION_POSITIONS.map( positionToWidgetCssClass ), widgetViewElement );
-
-					// Set CSS classes related to possible positions. They are used so the UI knows which buttons to display.
-					writer.addClass( positions.map( positionToWidgetCssClass ), widgetViewElement );
+	/**
+	 * Handles the keyboard navigation on "keydown" when a widget is currently selected and activates or deactivates
+	 * the "fake caret" for that widget, depending on the current value of the "widget-type-around" model
+	 * selection attribute and the direction of the pressed arrow key.
+	 *
+	 * @private
+	 * @param {Boolean} isForward `true` when the pressed arrow key was responsible for the forward model selection movement
+	 * as in {@link module:utils/keyboard~isForwardArrowKeyCode}.
+	 * @returns {Boolean} `true` when the key press was handled and no other keydown listener of the editor should
+	 * process the event any further. `false` otherwise.
+	 */
+	_handleArrowKeyPressOnSelectedWidget( isForward ) {
+		const editor = this.editor;
+		const model = editor.model;
+		const modelSelection = model.document.selection;
+		const typeAroundSelectionAttribute = modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE );
+		let shouldStopAndPreventDefault = false;
+
+		model.change( writer => {
+			// If the selection already has the attribute...
+			if ( typeAroundSelectionAttribute ) {
+				const isLeavingWidget = typeAroundSelectionAttribute === ( isForward ? 'after' : 'before' );
+
+				// If the keyboard arrow works against the value of the selection attribute...
+				// then remove the selection attribute but prevent default DOM actions
+				// and do not let the Widget plugin listener move the selection. This brings
+				// the widget back to the state, for instance, like if was selected using the mouse.
+				//
+				// **Note**: If leaving the widget when the "fake caret" is active, then the default
+				// Widget handler will change the selection and, in turn, this will automatically discard
+				// the selection attribute.
+				if ( !isLeavingWidget ) {
+					writer.removeSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE );
+
+					shouldStopAndPreventDefault = true;
 				}
 				}
 			}
 			}
+			// If the selection didn't have the attribute, let's set it now according to the direction of the arrow
+			// key press. This also means we cannot let the Widget plugin listener move the selection.
+			else {
+				writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, isForward ? 'after' : 'before' );
+
+				shouldStopAndPreventDefault = true;
+			}
 		} );
 		} );
+
+		return shouldStopAndPreventDefault;
+	}
+
+	/**
+	 * Handles the keyboard navigation on "keydown" when **no** widget is selected but the selection is **directly** next
+	 * to one and upon the "fake caret" should become active for this widget upon arrow key press
+	 * (AKA entering/selecting the widget).
+	 *
+	 * **Note**: This code mirrors the implementation from the Widget plugin but also adds the selection attribute.
+	 * Unfortunately, there's no safe way to let the Widget plugin do the selection part first and then just set the
+	 * selection attribute here in the WidgetTypeAround plugin. This is why this code must duplicate some from the Widget plugin.
+	 *
+	 * @private
+	 * @param {Boolean} isForward `true` when the pressed arrow key was responsible for the forward model selection movement
+	 * as in {@link module:utils/keyboard~isForwardArrowKeyCode}.
+	 * @returns {Boolean} `true` when the key press was handled and no other keydown listener of the editor should
+	 * process the event any further. `false` otherwise.
+	 */
+	_handleArrowKeyPressWhenSelectionNextToAWidget( isForward ) {
+		const editor = this.editor;
+		const model = editor.model;
+		const schema = model.schema;
+		const widgetPlugin = editor.plugins.get( 'Widget' );
+
+		// This is the widget the selection is about to be set on.
+		const modelElementNextToSelection = widgetPlugin._getObjectElementNextToSelection( isForward );
+		const viewElementNextToSelection = editor.editing.mapper.toViewElement( modelElementNextToSelection );
+
+		if ( isTypeAroundWidget( viewElementNextToSelection, modelElementNextToSelection, schema ) ) {
+			model.change( writer => {
+				widgetPlugin._setSelectionOverElement( modelElementNextToSelection );
+				writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, isForward ? 'before' : 'after' );
+			} );
+
+			// The change() block above does the same job as the Widget plugin. The event can
+			// be safely canceled.
+			return true;
+		}
+
+		return false;
 	}
 	}
 
 
 	/**
 	/**
@@ -211,13 +454,180 @@ export default class WidgetTypeAround extends Plugin {
 
 
 			const buttonPosition = getTypeAroundButtonPosition( button );
 			const buttonPosition = getTypeAroundButtonPosition( button );
 			const widgetViewElement = getClosestWidgetViewElement( button, editingView.domConverter );
 			const widgetViewElement = getClosestWidgetViewElement( button, editingView.domConverter );
+			const widgetModelElement = editor.editing.mapper.toModelElement( widgetViewElement );
 
 
-			this._insertParagraph( widgetViewElement, buttonPosition );
+			this._insertParagraph( widgetModelElement, buttonPosition );
 
 
 			domEventData.preventDefault();
 			domEventData.preventDefault();
 			evt.stop();
 			evt.stop();
 		} );
 		} );
 	}
 	}
+
+	/**
+	 * Creates the "enter" key listener on the view document that allows the user to insert a paragraph
+	 * near the widget when either:
+	 *
+	 * * The "fake caret" was first activated using the arrow keys,
+	 * * The entire widget is selected in the model.
+	 *
+	 * In the first case, the new paragraph is inserted according to the "widget-type-around" selection
+	 * attribute (see {@link #_handleArrowKeyPress}).
+	 *
+	 * It the second case, the new paragraph is inserted based on whether a soft (Shift+Enter) keystroke
+	 * was pressed or not.
+	 *
+	 * @private
+	 */
+	_enableInsertingParagraphsOnEnterKeypress() {
+		const editor = this.editor;
+		const editingView = editor.editing.view;
+
+		this.listenTo( editingView.document, 'enter', ( evt, domEventData ) => {
+			const selectedViewElement = editingView.document.selection.getSelectedElement();
+			const selectedModelElement = editor.editing.mapper.toModelElement( selectedViewElement );
+			const schema = editor.model.schema;
+			let wasHandled;
+
+			// First check if the widget is selected and there's a type around selection attribute associated
+			// with the "fake caret" that would tell where to insert a new paragraph.
+			if ( this._insertParagraphAccordingToSelectionAttribute() ) {
+				wasHandled = true;
+			}
+			// Then, if there is no selection attribute associated with the "fake caret", check if the widget
+			// simply is selected and create a new paragraph according to the keystroke (Shift+)Enter.
+			else if ( isTypeAroundWidget( selectedViewElement, selectedModelElement, schema ) ) {
+				this._insertParagraph( selectedModelElement, domEventData.isSoft ? 'before' : 'after' );
+
+				wasHandled = true;
+			}
+
+			if ( wasHandled ) {
+				domEventData.preventDefault();
+				evt.stop();
+			}
+		} );
+	}
+
+	/**
+	 * Similar to the {@link #_enableInsertingParagraphsOnEnterKeypress}, it allows the user
+	 * to insert a paragraph next to a widget when the "fake caret" was activated using arrow
+	 * keys but it responds to "typing keystrokes" instead of "enter".
+	 *
+	 * "Typing keystrokes" are keystrokes that insert new content into the document
+	 * like, for instance, letters ("a") or numbers ("4"). The "keydown" listener enabled by this method
+	 * will insert a new paragraph according to the "widget-type-around" model selection attribute
+	 * as the user simply starts typing, which creates the impression that the "fake caret"
+	 * behaves like a "real one" rendered by the browser (AKA your text appears where the caret was).
+	 *
+	 * **Note**: ATM this listener creates 2 undo steps: one for the "insertParagraph" command
+	 * and the second for the actual typing. It's not a disaster but this may need to be fixed
+	 * sooner or later.
+	 *
+	 * Learn more in {@link module:typing/utils/injectunsafekeystrokeshandling}.
+	 *
+	 * @private
+	 */
+	_enableInsertingParagraphsOnTypingKeystroke() {
+		const editor = this.editor;
+		const editingView = editor.editing.view;
+		const keyCodesHandledSomewhereElse = [
+			keyCodes.enter,
+			keyCodes.delete,
+			keyCodes.backspace
+		];
+
+		// Note: The priority must precede the default Widget class keydown handler ("high") and the
+		// TableKeyboard keydown handler ("high + 1").
+		editingView.document.on( 'keydown', ( evt, domEventData ) => {
+			// Don't handle enter/backspace/delete here. They are handled in dedicated listeners.
+			if ( !keyCodesHandledSomewhereElse.includes( domEventData.keyCode ) && !isNonTypingKeystroke( domEventData ) ) {
+				this._insertParagraphAccordingToSelectionAttribute();
+			}
+		}, { priority: priorities.get( 'high' ) + 1 } );
+	}
+
+	/**
+	 * It creates a "delete" event listener on the view document to handle cases when delete/backspace
+	 * is pressed and the "fake caret" is currently active.
+	 *
+	 * The "fake caret" should create an illusion of a "real browser caret" so that when it appears
+	 * before/after a widget, pressing delete/backspace should remove a widget or delete a content
+	 * before/after a widget (depending on the content surrounding the widget).
+	 *
+	 * @private
+	 */
+	_enableDeleteIntegration() {
+		const editor = this.editor;
+		const editingView = editor.editing.view;
+		const model = editor.model;
+		const schema = model.schema;
+
+		// Note: The priority must precede the default Widget class delete handler.
+		this.listenTo( editingView.document, 'delete', ( evt, domEventData ) => {
+			const typeAroundSelectionAttributeValue = model.document.selection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE );
+
+			// This listener handles only these cases when the "fake caret" is active.
+			if ( !typeAroundSelectionAttributeValue ) {
+				return;
+			}
+
+			const direction = domEventData.direction;
+			const selectedModelWidget = model.document.selection.getSelectedElement();
+
+			const isFakeCaretBefore = typeAroundSelectionAttributeValue === 'before';
+			const isForwardDelete = direction == 'forward';
+			const shouldDeleteEntireWidget = isFakeCaretBefore === isForwardDelete;
+
+			if ( shouldDeleteEntireWidget ) {
+				editor.execute( 'delete', {
+					selection: model.createSelection( selectedModelWidget, 'on' )
+				} );
+			} else {
+				const range = schema.getNearestSelectionRange(
+					model.createPositionAt( selectedModelWidget, typeAroundSelectionAttributeValue ),
+					direction
+				);
+
+				// If there is somewhere to move selection to, then there will be something to delete.
+				if ( range ) {
+					// If the range is NOT collapsed, then we know that the range contains an object (see getNearestSelectionRange() docs).
+					if ( !range.isCollapsed ) {
+						model.change( writer => {
+							writer.setSelection( range );
+							editor.execute( isForwardDelete ? 'forwardDelete' : 'delete' );
+						} );
+					} else {
+						const probe = model.createSelection( range.start );
+						model.modifySelection( probe, { direction } );
+
+						// If the range is collapsed, let's see if a non-collapsed range exists that can could be deleted.
+						// If such range exists, use the editor command because it it safe for collaboration (it merges where it can).
+						if ( !probe.focus.isEqual( range.start ) ) {
+							model.change( writer => {
+								writer.setSelection( range );
+								editor.execute( isForwardDelete ? 'forwardDelete' : 'delete' );
+							} );
+						}
+						// If there is no non-collapsed range to be deleted then we are sure that there is an empty element
+						// next to a widget that should be removed. "delete" and "forwardDelete" commands cannot get rid of it
+						// so calling Model#deleteContent here manually.
+						else {
+							const deepestEmptyRangeAncestor = getDeepestEmptyElementAncestor( schema, range.start.parent );
+
+							model.deleteContent( model.createSelection( deepestEmptyRangeAncestor, 'on' ), {
+								doNotAutoparagraph: true
+							} );
+						}
+					}
+				}
+			}
+
+			// If some content was deleted, don't let the handler from the Widget plugin kick in.
+			// If nothing was deleted, then the default handler will have nothing to do anyway.
+			domEventData.preventDefault();
+			evt.stop();
+		}, { priority: priorities.get( 'high' ) + 1 } );
+	}
 }
 }
 
 
 // Injects the type around UI into a view widget instance.
 // Injects the type around UI into a view widget instance.
@@ -232,6 +642,7 @@ function injectUIIntoWidget( viewWriter, buttonTitles, widgetViewElement ) {
 		const wrapperDomElement = this.toDomElement( domDocument );
 		const wrapperDomElement = this.toDomElement( domDocument );
 
 
 		injectButtons( wrapperDomElement, buttonTitles );
 		injectButtons( wrapperDomElement, buttonTitles );
+		injectFakeCaret( wrapperDomElement );
 
 
 		return wrapperDomElement;
 		return wrapperDomElement;
 	} );
 	} );
@@ -266,3 +677,42 @@ function injectButtons( wrapperDomElement, buttonTitles ) {
 		wrapperDomElement.appendChild( buttonTemplate.render() );
 		wrapperDomElement.appendChild( buttonTemplate.render() );
 	}
 	}
 }
 }
+
+// @param {HTMLElement} wrapperDomElement
+function injectFakeCaret( wrapperDomElement ) {
+	const caretTemplate = new Template( {
+		tag: 'div',
+		attributes: {
+			class: [
+				'ck',
+				'ck-widget__type-around__fake-caret'
+			]
+		}
+	} );
+
+	wrapperDomElement.appendChild( caretTemplate.render() );
+}
+
+// Returns the ancestor of an element closest to the root which is empty. For instance,
+// for `<baz>`:
+//
+//		<foo>abc<bar><baz></baz></bar></foo>
+//
+// it returns `<bar>`.
+//
+// @param {module:engine/model/schema~Schema} schema
+// @param {module:engine/model/element~Element} element
+// @returns {module:engine/model/element~Element|null}
+function getDeepestEmptyElementAncestor( schema, element ) {
+	let deepestEmptyAncestor = element;
+
+	for ( const ancestor of element.getAncestors( { parentFirst: true } ) ) {
+		if ( ancestor.childCount > 1 || schema.isLimit( ancestor ) ) {
+			break;
+		}
+
+		deepestEmptyAncestor = ancestor;
+	}
+
+	return deepestEmptyAncestor;
+}

+ 2 - 2
packages/ckeditor5-widget/tests/manual/type-around.html

@@ -39,13 +39,13 @@
 					<td>&nbsp;</td>
 					<td>&nbsp;</td>
 					<td>
 					<td>
 						<figure class="media">
 						<figure class="media">
-							<div data-oembed-url="https://www.youtube.com/watch?v=ZVv7UMQPEWk"><div style="position: relative; padding-bottom: 100%; height: 0; padding-bottom: 56.2493%;"><iframe src="https://www.youtube.com/embed/ZVv7UMQPEWk" style="position: absolute; width: 100%; height: 100%; top: 0; left: 0;" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen=""></iframe></div></div>
+							<div data-oembed-url="https://www.youtube.com/watch?v=ZVv7UMQPEWk"></div>
 						</figure>
 						</figure>
 					</td>
 					</td>
 				</tr>
 				</tr>
 				<tr>
 				<tr>
 					<td>&nbsp;</td>
 					<td>&nbsp;</td>
-					<td>
+					<td style="background: hsl(0,100%,0%)">
 						<p>&nbsp;</p>
 						<p>&nbsp;</p>
 						<figure class="image"><img src="sample.jpg" alt="bar">
 						<figure class="image"><img src="sample.jpg" alt="bar">
 							<figcaption>Caption</figcaption>
 							<figcaption>Caption</figcaption>

+ 15 - 2
packages/ckeditor5-widget/tests/manual/type-around.js

@@ -9,6 +9,9 @@ import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'
 import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
 import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
 import HorizontalLine from '@ckeditor/ckeditor5-horizontal-line/src/horizontalline';
 import HorizontalLine from '@ckeditor/ckeditor5-horizontal-line/src/horizontalline';
 import MediaEmbed from '@ckeditor/ckeditor5-media-embed/src/mediaembed';
 import MediaEmbed from '@ckeditor/ckeditor5-media-embed/src/mediaembed';
+import TableProperties from '@ckeditor/ckeditor5-table/src/tableproperties';
+import TableCellProperties from '@ckeditor/ckeditor5-table/src/tablecellproperties';
+import ImageResize from '@ckeditor/ckeditor5-image/src/imageresize';
 
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
 import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
@@ -96,7 +99,15 @@ document.querySelector( '#toggleReadOnly' ).addEventListener( 'click', () => {
 
 
 ClassicEditor
 ClassicEditor
 	.create( document.querySelector( '#editor' ), {
 	.create( document.querySelector( '#editor' ), {
-		plugins: [ ArticlePluginSet, HorizontalLine, InlineWidget, MediaEmbed ],
+		plugins: [
+			ArticlePluginSet,
+			HorizontalLine,
+			InlineWidget,
+			MediaEmbed,
+			TableProperties,
+			TableCellProperties,
+			ImageResize
+		],
 		toolbar: [
 		toolbar: [
 			'heading',
 			'heading',
 			'|',
 			'|',
@@ -123,7 +134,9 @@ ClassicEditor
 			contentToolbar: [
 			contentToolbar: [
 				'tableColumn',
 				'tableColumn',
 				'tableRow',
 				'tableRow',
-				'mergeTableCells'
+				'mergeTableCells',
+				'tableProperties',
+				'tableCellProperties'
 			]
 			]
 		}
 		}
 	} )
 	} )

+ 7 - 6
packages/ckeditor5-widget/tests/widget-integration.js

@@ -6,6 +6,7 @@
 /* global document */
 /* global document */
 
 
 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 Typing from '@ckeditor/ckeditor5-typing/src/typing';
 import Typing from '@ckeditor/ckeditor5-typing/src/typing';
 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';
@@ -31,7 +32,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: [ Widget, Typing ] } )
+		return ClassicEditor.create( editorElement, { plugins: [ Paragraph, Widget, Typing ] } )
 			.then( newEditor => {
 			.then( newEditor => {
 				editor = newEditor;
 				editor = newEditor;
 				model = editor.model;
 				model = editor.model;
@@ -113,7 +114,7 @@ describe( 'Widget - integration', () => {
 
 
 		expect( getViewData( view ) ).to.equal(
 		expect( getViewData( view ) ).to.equal(
 			'<p>[]</p>' +
 			'<p>[]</p>' +
-			'<div class="ck-widget ck-widget_can-type-around_after" contenteditable="false">' +
+			'<div class="ck-widget" contenteditable="false">' +
 				'<figcaption contenteditable="true">foo bar</figcaption>' +
 				'<figcaption contenteditable="true">foo bar</figcaption>' +
 				'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
 				'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
 			'</div>'
 			'</div>'
@@ -139,7 +140,7 @@ describe( 'Widget - integration', () => {
 		sinon.assert.called( preventDefault );
 		sinon.assert.called( preventDefault );
 
 
 		expect( getViewData( view ) ).to.equal(
 		expect( getViewData( view ) ).to.equal(
-			'<div class="ck-widget ck-widget_can-type-around_after ck-widget_can-type-around_before" contenteditable="false">' +
+			'<div class="ck-widget" contenteditable="false">' +
 				'<figcaption contenteditable="true">{foo bar}</figcaption>' +
 				'<figcaption contenteditable="true">{foo bar}</figcaption>' +
 				'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
 				'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
 			'</div>'
 			'</div>'
@@ -164,7 +165,7 @@ describe( 'Widget - integration', () => {
 		sinon.assert.called( preventDefault );
 		sinon.assert.called( preventDefault );
 
 
 		expect( getViewData( view ) ).to.equal(
 		expect( getViewData( view ) ).to.equal(
-			'<div class="ck-widget ck-widget_can-type-around_after ck-widget_can-type-around_before" contenteditable="false">' +
+			'<div class="ck-widget" contenteditable="false">' +
 				'<figcaption contenteditable="true">foo</figcaption>' +
 				'<figcaption contenteditable="true">foo</figcaption>' +
 				'<figcaption contenteditable="true">{bar}</figcaption>' +
 				'<figcaption contenteditable="true">{bar}</figcaption>' +
 				'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
 				'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
@@ -191,7 +192,7 @@ describe( 'Widget - integration', () => {
 		sinon.assert.called( preventDefault );
 		sinon.assert.called( preventDefault );
 
 
 		expect( getViewData( view ) ).to.equal(
 		expect( getViewData( view ) ).to.equal(
-			'<div class="ck-widget ck-widget_can-type-around_after ck-widget_can-type-around_before" contenteditable="false">' +
+			'<div class="ck-widget" contenteditable="false">' +
 				'<figcaption contenteditable="true">{foo bar}</figcaption>' +
 				'<figcaption contenteditable="true">{foo bar}</figcaption>' +
 				'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
 				'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
 			'</div>'
 			'</div>'
@@ -243,7 +244,7 @@ describe( 'Widget - integration', () => {
 
 
 		expect( getViewData( view ) ).to.equal(
 		expect( getViewData( view ) ).to.equal(
 			'<p>[]</p>' +
 			'<p>[]</p>' +
-			'<div class="ck-widget ck-widget_can-type-around_after" contenteditable="false">' +
+			'<div class="ck-widget" contenteditable="false">' +
 				'<figcaption contenteditable="true">foo bar</figcaption>' +
 				'<figcaption contenteditable="true">foo bar</figcaption>' +
 				'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
 				'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
 			'</div>'
 			'</div>'

+ 151 - 171
packages/ckeditor5-widget/tests/widget.js

@@ -6,6 +6,8 @@
 /* global document */
 /* global document */
 
 
 import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
 import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
+import Enter from '@ckeditor/ckeditor5-enter/src/enter';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import Widget from '../src/widget';
 import Widget from '../src/widget';
 import WidgetTypeAround from '../src/widgettypearound/widgettypearound';
 import WidgetTypeAround from '../src/widgettypearound/widgettypearound';
 import Typing from '@ckeditor/ckeditor5-typing/src/typing';
 import Typing from '@ckeditor/ckeditor5-typing/src/typing';
@@ -28,7 +30,7 @@ describe( 'Widget', () => {
 
 
 		return ClassicTestEditor
 		return ClassicTestEditor
 			.create( element, {
 			.create( element, {
-				plugins: [ Widget, Typing ]
+				plugins: [ Paragraph, Widget, Typing, Enter ]
 			} )
 			} )
 			.then( newEditor => {
 			.then( newEditor => {
 				editor = newEditor;
 				editor = newEditor;
@@ -198,7 +200,7 @@ describe( 'Widget', () => {
 		setModelData( model, '[<widget>foo bar</widget>]' );
 		setModelData( model, '[<widget>foo bar</widget>]' );
 
 
 		expect( getViewData( view ) ).to.equal(
 		expect( getViewData( view ) ).to.equal(
-			'[<div class="ck-widget ck-widget_can-type-around_after ck-widget_can-type-around_before ck-widget_selected" ' +
+			'[<div class="ck-widget ck-widget_selected" ' +
 			'contenteditable="false">' +
 			'contenteditable="false">' +
 				'foo bar' +
 				'foo bar' +
 				'<b></b>' +
 				'<b></b>' +
@@ -221,11 +223,11 @@ describe( 'Widget', () => {
 		expect( getViewData( view ) ).to.equal(
 		expect( getViewData( view ) ).to.equal(
 
 
 			'<p>{foo</p>' +
 			'<p>{foo</p>' +
-			'<div class="ck-widget ck-widget_can-type-around_after ck-widget_selected" contenteditable="false">' +
+			'<div class="ck-widget ck-widget_selected" contenteditable="false">' +
 				'<b></b>' +
 				'<b></b>' +
 				'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
 				'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
 			'</div>' +
 			'</div>' +
-			'<div class="ck-widget ck-widget_can-type-around_after ck-widget_can-type-around_before ck-widget_selected" ' +
+			'<div class="ck-widget ck-widget_selected" ' +
 			'contenteditable="false">' +
 			'contenteditable="false">' +
 				'<b></b>' +
 				'<b></b>' +
 				'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
 				'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
@@ -244,7 +246,7 @@ describe( 'Widget', () => {
 
 
 		expect( getViewData( view ) ).to.equal(
 		expect( getViewData( view ) ).to.equal(
 			'<p>foo</p>' +
 			'<p>foo</p>' +
-			'[<div class="ck-widget ck-widget_can-type-around_after ck-widget_selected" contenteditable="false">' +
+			'[<div class="ck-widget ck-widget_selected" contenteditable="false">' +
 				'foo' +
 				'foo' +
 				'<b></b>' +
 				'<b></b>' +
 				'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
 				'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
@@ -257,7 +259,7 @@ describe( 'Widget', () => {
 
 
 		expect( getViewData( view ) ).to.equal(
 		expect( getViewData( view ) ).to.equal(
 			'<p>{}foo</p>' +
 			'<p>{}foo</p>' +
-			'<div class="ck-widget ck-widget_can-type-around_after" contenteditable="false">' +
+			'<div class="ck-widget" contenteditable="false">' +
 				'foo' +
 				'foo' +
 				'<b></b>' +
 				'<b></b>' +
 				'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
 				'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
@@ -269,7 +271,7 @@ describe( 'Widget', () => {
 		setModelData( model, '<widget><editable>foo bar</editable></widget><editable>[baz]</editable>' );
 		setModelData( model, '<widget><editable>foo bar</editable></widget><editable>[baz]</editable>' );
 
 
 		expect( getViewData( view ) ).to.equal(
 		expect( getViewData( view ) ).to.equal(
-			'<div class="ck-widget ck-widget_can-type-around_before" contenteditable="false">' +
+			'<div class="ck-widget" contenteditable="false">' +
 				'<figcaption contenteditable="true">foo bar</figcaption>' +
 				'<figcaption contenteditable="true">foo bar</figcaption>' +
 				'<b></b>' +
 				'<b></b>' +
 				'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
 				'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
@@ -283,112 +285,128 @@ describe( 'Widget', () => {
 			test(
 			test(
 				'should move selection forward from selected object - right arrow',
 				'should move selection forward from selected object - right arrow',
 				'[<widget></widget>]<paragraph>foo</paragraph>',
 				'[<widget></widget>]<paragraph>foo</paragraph>',
-				keyCodes.arrowright,
+				// Note: The first step is handled by the WidgetTypeAround plugin.
+				[ keyCodes.arrowright, keyCodes.arrowright ],
 				'<widget></widget><paragraph>[]foo</paragraph>'
 				'<widget></widget><paragraph>[]foo</paragraph>'
 			);
 			);
 
 
 			test(
 			test(
 				'should move selection forward from selected object - down arrow',
 				'should move selection forward from selected object - down arrow',
 				'[<widget></widget>]<paragraph>foo</paragraph>',
 				'[<widget></widget>]<paragraph>foo</paragraph>',
-				keyCodes.arrowdown,
+				// Note: The first step is handled by the WidgetTypeAround plugin.
+				[ keyCodes.arrowdown, keyCodes.arrowdown ],
 				'<widget></widget><paragraph>[]foo</paragraph>'
 				'<widget></widget><paragraph>[]foo</paragraph>'
 			);
 			);
 
 
 			test(
 			test(
 				'should move selection backward from selected object - left arrow',
 				'should move selection backward from selected object - left arrow',
 				'<paragraph>foo</paragraph>[<widget></widget>]',
 				'<paragraph>foo</paragraph>[<widget></widget>]',
-				keyCodes.arrowleft,
+				// Note: The first step is handled by the WidgetTypeAround plugin.
+				[ keyCodes.arrowleft, keyCodes.arrowleft ],
 				'<paragraph>foo[]</paragraph><widget></widget>'
 				'<paragraph>foo[]</paragraph><widget></widget>'
 			);
 			);
 
 
 			test(
 			test(
 				'should move selection backward from selected object - up arrow',
 				'should move selection backward from selected object - up arrow',
 				'<paragraph>foo</paragraph>[<widget></widget>]',
 				'<paragraph>foo</paragraph>[<widget></widget>]',
-				keyCodes.arrowup,
+				// Note: The first step is handled by the WidgetTypeAround plugin.
+				[ keyCodes.arrowup, keyCodes.arrowup ],
 				'<paragraph>foo[]</paragraph><widget></widget>'
 				'<paragraph>foo[]</paragraph><widget></widget>'
 			);
 			);
 
 
 			test(
 			test(
 				'should move selection to next widget - right arrow',
 				'should move selection to next widget - right arrow',
 				'[<widget></widget>]<widget></widget>',
 				'[<widget></widget>]<widget></widget>',
-				keyCodes.arrowright,
+				// Note: The first step is handled by the WidgetTypeAround plugin.
+				[ keyCodes.arrowright, keyCodes.arrowright ],
 				'<widget></widget>[<widget></widget>]'
 				'<widget></widget>[<widget></widget>]'
 			);
 			);
 
 
 			test(
 			test(
 				'should move selection to next widget - down arrow',
 				'should move selection to next widget - down arrow',
 				'[<widget></widget>]<widget></widget>',
 				'[<widget></widget>]<widget></widget>',
-				keyCodes.arrowdown,
+				// Note: The first step is handled by the WidgetTypeAround plugin.
+				[ keyCodes.arrowdown, keyCodes.arrowdown ],
 				'<widget></widget>[<widget></widget>]'
 				'<widget></widget>[<widget></widget>]'
 			);
 			);
 
 
 			test(
 			test(
 				'should move selection to previous widget - left arrow',
 				'should move selection to previous widget - left arrow',
 				'<widget></widget>[<widget></widget>]',
 				'<widget></widget>[<widget></widget>]',
-				keyCodes.arrowleft,
+				// Note: The first step is handled by the WidgetTypeAround plugin.
+				[ keyCodes.arrowleft, keyCodes.arrowleft ],
 				'[<widget></widget>]<widget></widget>'
 				'[<widget></widget>]<widget></widget>'
 			);
 			);
 
 
 			test(
 			test(
 				'should move selection to previous widget - up arrow',
 				'should move selection to previous widget - up arrow',
 				'<widget></widget>[<widget></widget>]',
 				'<widget></widget>[<widget></widget>]',
-				keyCodes.arrowup,
+				// Note: The first step is handled by the WidgetTypeAround plugin.
+				[ keyCodes.arrowup, keyCodes.arrowup ],
 				'[<widget></widget>]<widget></widget>'
 				'[<widget></widget>]<widget></widget>'
 			);
 			);
 
 
+			// Note: Testing an inline widget only because block widgets are handled and tested by the WidgetTypeAround plugin.
 			test(
 			test(
-				'should do nothing on non-collapsed selection next to object - right arrow',
-				'<paragraph>ba[r]</paragraph><widget></widget>',
+				'should do nothing on non-collapsed selection next to an inline widget - right arrow',
+				'<paragraph>ba[r]<inline-widget></inline-widget></paragraph>',
 				keyCodes.arrowright,
 				keyCodes.arrowright,
-				'<paragraph>ba[r]</paragraph><widget></widget>'
+				'<paragraph>ba[r]<inline-widget></inline-widget></paragraph>'
 			);
 			);
 
 
+			// Note: Testing an inline widget only because block widgets are handled and tested by the WidgetTypeAround plugin.
 			test(
 			test(
-				'should do nothing on non-collapsed selection next to object - down arrow',
-				'<paragraph>ba[r]</paragraph><widget></widget>',
+				'should do nothing on non-collapsed selection next to an inline widget - down arrow',
+				'<paragraph>ba[r]<inline-widget></inline-widget></paragraph>',
 				keyCodes.arrowdown,
 				keyCodes.arrowdown,
-				'<paragraph>ba[r]</paragraph><widget></widget>'
+				'<paragraph>ba[r]<inline-widget></inline-widget></paragraph>'
 			);
 			);
 
 
+			// Note: Testing an inline widget only because block widgets are handled and tested by the WidgetTypeAround plugin.
 			test(
 			test(
-				'should do nothing on non-collapsed selection next to object - left arrow',
-				'<widget></widget><paragraph>[b]ar</paragraph>',
+				'should do nothing on non-collapsed selection next to an inline widget - left arrow',
+				'<paragraph><inline-widget></inline-widget>[b]ar</paragraph>',
 				keyCodes.arrowleft,
 				keyCodes.arrowleft,
-				'<widget></widget><paragraph>[b]ar</paragraph>'
+				'<paragraph><inline-widget></inline-widget>[b]ar</paragraph>'
 			);
 			);
 
 
+			// Note: Testing an inline widget only because block widgets are handled and tested by the WidgetTypeAround plugin.
 			test(
 			test(
-				'should do nothing on non-collapsed selection next to object - up arrow',
-				'<widget></widget><paragraph>[b]ar</paragraph>',
+				'should do nothing on non-collapsed selection next to an inline widget - up arrow',
+				'<paragraph><inline-widget></inline-widget>[b]ar</paragraph>',
 				keyCodes.arrowup,
 				keyCodes.arrowup,
-				'<widget></widget><paragraph>[b]ar</paragraph>'
+				'<paragraph><inline-widget></inline-widget>[b]ar</paragraph>'
 			);
 			);
 
 
 			test(
 			test(
 				'should not move selection if there is no correct location - right arrow',
 				'should not move selection if there is no correct location - right arrow',
 				'<paragraph>foo</paragraph>[<widget></widget>]',
 				'<paragraph>foo</paragraph>[<widget></widget>]',
-				keyCodes.arrowright,
+				// Note: The first step is handled by the WidgetTypeAround plugin.
+				[ keyCodes.arrowright, keyCodes.arrowright ],
 				'<paragraph>foo</paragraph>[<widget></widget>]'
 				'<paragraph>foo</paragraph>[<widget></widget>]'
 			);
 			);
 
 
 			test(
 			test(
 				'should not move selection if there is no correct location - down arrow',
 				'should not move selection if there is no correct location - down arrow',
 				'<paragraph>foo</paragraph>[<widget></widget>]',
 				'<paragraph>foo</paragraph>[<widget></widget>]',
-				keyCodes.arrowdown,
+				// Note: The first step is handled by the WidgetTypeAround plugin.
+				[ keyCodes.arrowdown, keyCodes.arrowdown ],
 				'<paragraph>foo</paragraph>[<widget></widget>]'
 				'<paragraph>foo</paragraph>[<widget></widget>]'
 			);
 			);
 
 
 			test(
 			test(
 				'should not move selection if there is no correct location - left arrow',
 				'should not move selection if there is no correct location - left arrow',
 				'[<widget></widget>]<paragraph>foo</paragraph>',
 				'[<widget></widget>]<paragraph>foo</paragraph>',
-				keyCodes.arrowleft,
+				// Note: The first step is handled by the WidgetTypeAround plugin.
+				[ keyCodes.arrowleft, keyCodes.arrowleft ],
 				'[<widget></widget>]<paragraph>foo</paragraph>'
 				'[<widget></widget>]<paragraph>foo</paragraph>'
 			);
 			);
 
 
 			test(
 			test(
 				'should not move selection if there is no correct location - up arrow',
 				'should not move selection if there is no correct location - up arrow',
 				'[<widget></widget>]<paragraph>foo</paragraph>',
 				'[<widget></widget>]<paragraph>foo</paragraph>',
-				keyCodes.arrowup,
+				// Note: The first step is handled by the WidgetTypeAround plugin.
+				[ keyCodes.arrowup, keyCodes.arrowup ],
 				'[<widget></widget>]<paragraph>foo</paragraph>'
 				'[<widget></widget>]<paragraph>foo</paragraph>'
 			);
 			);
 
 
@@ -409,10 +427,12 @@ describe( 'Widget', () => {
 				setModelData( model, '<paragraph>foo</paragraph>[<widget></widget>]' );
 				setModelData( model, '<paragraph>foo</paragraph>[<widget></widget>]' );
 				viewDocument.on( 'keydown', keydownHandler );
 				viewDocument.on( 'keydown', keydownHandler );
 
 
+				// Note: The first step is handled by the WidgetTypeAround plugin.
+				viewDocument.fire( 'keydown', domEventDataMock );
 				viewDocument.fire( 'keydown', domEventDataMock );
 				viewDocument.fire( 'keydown', domEventDataMock );
 
 
 				expect( getModelData( model ) ).to.equal( '<paragraph>foo</paragraph>[<widget></widget>]' );
 				expect( getModelData( model ) ).to.equal( '<paragraph>foo</paragraph>[<widget></widget>]' );
-				sinon.assert.calledOnce( domEventDataMock.preventDefault );
+				sinon.assert.calledTwice( domEventDataMock.preventDefault );
 				sinon.assert.notCalled( keydownHandler );
 				sinon.assert.notCalled( keydownHandler );
 			} );
 			} );
 
 
@@ -425,10 +445,12 @@ describe( 'Widget', () => {
 				setModelData( model, '[<widget></widget>]<paragraph>foo</paragraph>' );
 				setModelData( model, '[<widget></widget>]<paragraph>foo</paragraph>' );
 				viewDocument.on( 'keydown', keydownHandler );
 				viewDocument.on( 'keydown', keydownHandler );
 
 
+				// Note: The first step is handled by the WidgetTypeAround plugin.
+				viewDocument.fire( 'keydown', domEventDataMock );
 				viewDocument.fire( 'keydown', domEventDataMock );
 				viewDocument.fire( 'keydown', domEventDataMock );
 
 
 				expect( getModelData( model ) ).to.equal( '[<widget></widget>]<paragraph>foo</paragraph>' );
 				expect( getModelData( model ) ).to.equal( '[<widget></widget>]<paragraph>foo</paragraph>' );
-				sinon.assert.calledOnce( domEventDataMock.preventDefault );
+				sinon.assert.calledTwice( domEventDataMock.preventDefault );
 				sinon.assert.notCalled( keydownHandler );
 				sinon.assert.notCalled( keydownHandler );
 			} );
 			} );
 
 
@@ -491,84 +513,132 @@ describe( 'Widget', () => {
 			test(
 			test(
 				'should work correctly with modifier key: right arrow + ctrl',
 				'should work correctly with modifier key: right arrow + ctrl',
 				'[<widget></widget>]<paragraph>foo</paragraph>',
 				'[<widget></widget>]<paragraph>foo</paragraph>',
-				{ keyCode: keyCodes.arrowright, ctrlKey: true },
+				// Note: The first step is handled by the WidgetTypeAround plugin.
+				[
+					{ keyCode: keyCodes.arrowright, ctrlKey: true },
+					{ keyCode: keyCodes.arrowright, ctrlKey: true }
+				],
 				'<widget></widget><paragraph>[]foo</paragraph>'
 				'<widget></widget><paragraph>[]foo</paragraph>'
 			);
 			);
 
 
 			test(
 			test(
 				'should work correctly with modifier key: right arrow + alt',
 				'should work correctly with modifier key: right arrow + alt',
 				'[<widget></widget>]<paragraph>foo</paragraph>',
 				'[<widget></widget>]<paragraph>foo</paragraph>',
-				{ keyCode: keyCodes.arrowright, altKey: true },
+				// Note: The first step is handled by the WidgetTypeAround plugin.
+				[
+					{ keyCode: keyCodes.arrowright, altKey: true },
+					{ keyCode: keyCodes.arrowright, altKey: true }
+				],
 				'<widget></widget><paragraph>[]foo</paragraph>'
 				'<widget></widget><paragraph>[]foo</paragraph>'
 			);
 			);
 
 
 			test(
 			test(
 				'should work correctly with modifier key: right arrow + shift',
 				'should work correctly with modifier key: right arrow + shift',
 				'[<widget></widget>]<paragraph>foo</paragraph>',
 				'[<widget></widget>]<paragraph>foo</paragraph>',
-				{ keyCode: keyCodes.arrowright, shiftKey: true },
+				// Note: The first step is handled by the WidgetTypeAround plugin.
+				[
+					{ keyCode: keyCodes.arrowright, shiftKey: true },
+					{ keyCode: keyCodes.arrowright, shiftKey: true }
+				],
 				'<widget></widget><paragraph>[]foo</paragraph>'
 				'<widget></widget><paragraph>[]foo</paragraph>'
 			);
 			);
 
 
 			test(
 			test(
 				'should work correctly with modifier key: down arrow + ctrl',
 				'should work correctly with modifier key: down arrow + ctrl',
 				'[<widget></widget>]<paragraph>foo</paragraph>',
 				'[<widget></widget>]<paragraph>foo</paragraph>',
-				{ keyCode: keyCodes.arrowdown, ctrlKey: true },
+				// Note: The first step is handled by the WidgetTypeAround plugin.
+				[
+					{ keyCode: keyCodes.arrowdown, ctrlKey: true },
+					{ keyCode: keyCodes.arrowdown, ctrlKey: true }
+				],
 				'<widget></widget><paragraph>[]foo</paragraph>'
 				'<widget></widget><paragraph>[]foo</paragraph>'
 			);
 			);
 
 
 			test(
 			test(
 				'should work correctly with modifier key: down arrow + alt',
 				'should work correctly with modifier key: down arrow + alt',
 				'[<widget></widget>]<paragraph>foo</paragraph>',
 				'[<widget></widget>]<paragraph>foo</paragraph>',
-				{ keyCode: keyCodes.arrowdown, altKey: true },
+				// Note: The first step is handled by the WidgetTypeAround plugin.
+				[
+					{ keyCode: keyCodes.arrowdown, altKey: true },
+					{ keyCode: keyCodes.arrowdown, altKey: true }
+				],
 				'<widget></widget><paragraph>[]foo</paragraph>'
 				'<widget></widget><paragraph>[]foo</paragraph>'
 			);
 			);
 
 
 			test(
 			test(
 				'should work correctly with modifier key: down arrow + shift',
 				'should work correctly with modifier key: down arrow + shift',
 				'[<widget></widget>]<paragraph>foo</paragraph>',
 				'[<widget></widget>]<paragraph>foo</paragraph>',
-				{ keyCode: keyCodes.arrowdown, shiftKey: true },
+				// Note: The first step is handled by the WidgetTypeAround plugin.
+				[
+					{ keyCode: keyCodes.arrowdown, shiftKey: true },
+					{ keyCode: keyCodes.arrowdown, shiftKey: true }
+				],
 				'<widget></widget><paragraph>[]foo</paragraph>'
 				'<widget></widget><paragraph>[]foo</paragraph>'
 			);
 			);
 
 
 			test(
 			test(
 				'should work correctly with modifier key: left arrow + ctrl',
 				'should work correctly with modifier key: left arrow + ctrl',
 				'<paragraph>foo</paragraph>[<widget></widget>]',
 				'<paragraph>foo</paragraph>[<widget></widget>]',
-				{ keyCode: keyCodes.arrowleft, ctrlKey: true },
+				// Note: The first step is handled by the WidgetTypeAround plugin.
+				[
+					{ keyCode: keyCodes.arrowleft, ctrlKey: true },
+					{ keyCode: keyCodes.arrowleft, ctrlKey: true }
+				],
 				'<paragraph>foo[]</paragraph><widget></widget>'
 				'<paragraph>foo[]</paragraph><widget></widget>'
 			);
 			);
 
 
 			test(
 			test(
 				'should work correctly with modifier key: left arrow + alt',
 				'should work correctly with modifier key: left arrow + alt',
 				'<paragraph>foo</paragraph>[<widget></widget>]',
 				'<paragraph>foo</paragraph>[<widget></widget>]',
-				{ keyCode: keyCodes.arrowleft, altKey: true },
+				// Note: The first step is handled by the WidgetTypeAround plugin.
+				[
+					{ keyCode: keyCodes.arrowleft, altKey: true },
+					{ keyCode: keyCodes.arrowleft, altKey: true }
+				],
 				'<paragraph>foo[]</paragraph><widget></widget>'
 				'<paragraph>foo[]</paragraph><widget></widget>'
 			);
 			);
 
 
 			test(
 			test(
 				'should work correctly with modifier key: left arrow + shift',
 				'should work correctly with modifier key: left arrow + shift',
 				'<paragraph>foo</paragraph>[<widget></widget>]',
 				'<paragraph>foo</paragraph>[<widget></widget>]',
-				{ keyCode: keyCodes.arrowleft, shiftKey: true },
+				// Note: The first step is handled by the WidgetTypeAround plugin.
+				[
+					{ keyCode: keyCodes.arrowleft, shiftKey: true },
+					{ keyCode: keyCodes.arrowleft, shiftKey: true }
+				],
 				'<paragraph>foo[]</paragraph><widget></widget>'
 				'<paragraph>foo[]</paragraph><widget></widget>'
 			);
 			);
 
 
 			test(
 			test(
 				'should work correctly with modifier key: up arrow + ctrl',
 				'should work correctly with modifier key: up arrow + ctrl',
 				'<paragraph>foo</paragraph>[<widget></widget>]',
 				'<paragraph>foo</paragraph>[<widget></widget>]',
-				{ keyCode: keyCodes.arrowup, ctrlKey: true },
+				// Note: The first step is handled by the WidgetTypeAround plugin.
+				[
+					{ keyCode: keyCodes.arrowup, ctrlKey: true },
+					{ keyCode: keyCodes.arrowup, ctrlKey: true }
+				],
 				'<paragraph>foo[]</paragraph><widget></widget>'
 				'<paragraph>foo[]</paragraph><widget></widget>'
 			);
 			);
 
 
 			test(
 			test(
 				'should work correctly with modifier key: up arrow + alt',
 				'should work correctly with modifier key: up arrow + alt',
 				'<paragraph>foo</paragraph>[<widget></widget>]',
 				'<paragraph>foo</paragraph>[<widget></widget>]',
-				{ keyCode: keyCodes.arrowup, altKey: true },
+				// Note: The first step is handled by the WidgetTypeAround plugin.
+				[
+					{ keyCode: keyCodes.arrowup, altKey: true },
+					{ keyCode: keyCodes.arrowup, altKey: true }
+				],
 				'<paragraph>foo[]</paragraph><widget></widget>'
 				'<paragraph>foo[]</paragraph><widget></widget>'
 			);
 			);
 
 
 			test(
 			test(
 				'should work correctly with modifier key: up arrow + shift',
 				'should work correctly with modifier key: up arrow + shift',
 				'<paragraph>foo</paragraph>[<widget></widget>]',
 				'<paragraph>foo</paragraph>[<widget></widget>]',
-				{ keyCode: keyCodes.arrowup, shiftKey: true },
+				// Note: The first step is handled by the WidgetTypeAround plugin.
+				[
+					{ keyCode: keyCodes.arrowup, shiftKey: true },
+					{ keyCode: keyCodes.arrowup, shiftKey: true }
+				],
 				'<paragraph>foo[]</paragraph><widget></widget>'
 				'<paragraph>foo[]</paragraph><widget></widget>'
 			);
 			);
 
 
@@ -695,7 +765,8 @@ describe( 'Widget', () => {
 				test(
 				test(
 					'should move selection forward from selected object - left arrow',
 					'should move selection forward from selected object - left arrow',
 					'[<widget></widget>]<paragraph>foo</paragraph>',
 					'[<widget></widget>]<paragraph>foo</paragraph>',
-					keyCodes.arrowleft,
+					// Note: The first step is handled by the WidgetTypeAround plugin.
+					[ keyCodes.arrowleft, keyCodes.arrowleft ],
 					'<widget></widget><paragraph>[]foo</paragraph>',
 					'<widget></widget><paragraph>[]foo</paragraph>',
 					null,
 					null,
 					'rtl'
 					'rtl'
@@ -704,7 +775,8 @@ describe( 'Widget', () => {
 				test(
 				test(
 					'should move selection backward from selected object - right arrow',
 					'should move selection backward from selected object - right arrow',
 					'<paragraph>foo</paragraph>[<widget></widget>]',
 					'<paragraph>foo</paragraph>[<widget></widget>]',
-					keyCodes.arrowright,
+					// Note: The first step is handled by the WidgetTypeAround plugin.
+					[ keyCodes.arrowright, keyCodes.arrowright ],
 					'<paragraph>foo[]</paragraph><widget></widget>',
 					'<paragraph>foo[]</paragraph><widget></widget>',
 					null,
 					null,
 					'rtl'
 					'rtl'
@@ -713,7 +785,8 @@ describe( 'Widget', () => {
 				test(
 				test(
 					'should move selection to next widget - left arrow',
 					'should move selection to next widget - left arrow',
 					'[<widget></widget>]<widget></widget>',
 					'[<widget></widget>]<widget></widget>',
-					keyCodes.arrowleft,
+					// Note: The first step is handled by the WidgetTypeAround plugin.
+					[ keyCodes.arrowleft, keyCodes.arrowleft ],
 					'<widget></widget>[<widget></widget>]',
 					'<widget></widget>[<widget></widget>]',
 					null,
 					null,
 					'rtl'
 					'rtl'
@@ -722,7 +795,8 @@ describe( 'Widget', () => {
 				test(
 				test(
 					'should move selection to previous widget - right arrow',
 					'should move selection to previous widget - right arrow',
 					'<widget></widget>[<widget></widget>]',
 					'<widget></widget>[<widget></widget>]',
-					keyCodes.arrowright,
+					// Note: The first step is handled by the WidgetTypeAround plugin.
+					[ keyCodes.arrowright, keyCodes.arrowright ],
 					'[<widget></widget>]<widget></widget>',
 					'[<widget></widget>]<widget></widget>',
 					null,
 					null,
 					'rtl'
 					'rtl'
@@ -730,118 +804,33 @@ describe( 'Widget', () => {
 			} );
 			} );
 		} );
 		} );
 
 
-		describe( 'enter', () => {
-			test(
-				'should insert a paragraph after the selected widget upon Enter',
-				'[<widget></widget>]',
-				keyCodes.enter,
-				'<widget></widget><paragraph>[]</paragraph>'
-			);
-
-			test(
-				'should insert a paragraph before the selected widget upon Shift+Enter',
-				'[<widget></widget>]',
-				{ keyCode: keyCodes.enter, shiftKey: true },
-				'<paragraph>[]</paragraph><widget></widget>'
-			);
-
-			test(
-				'should insert a paragraph when not a first-child of the root',
-				'[<widget></widget>]<paragraph>foo</paragraph>',
-				keyCodes.enter,
-				'<widget></widget><paragraph>[]</paragraph><paragraph>foo</paragraph>'
-			);
-
-			test(
-				'should insert a paragraph when not a last-child of the root',
-				'<paragraph>foo</paragraph>[<widget></widget>]',
-				{ keyCode: keyCodes.enter, shiftKey: true },
-				'<paragraph>foo</paragraph><paragraph>[]</paragraph><widget></widget>'
-			);
+		function test( name, data, actions, expected, expectedView, contentLanguageDirection = 'ltr' ) {
+			it( name, () => {
+				testUtils.sinon.stub( editor.locale, 'contentLanguageDirection' ).value( contentLanguageDirection );
 
 
-			test(
-				'should insert a paragraph only when an entire widget is selected (#1)',
-				'<widget><nested>[foo] bar</nested></widget>',
-				keyCodes.enter,
-				'<widget><nested>[] bar</nested></widget>'
-			);
+				if ( !Array.isArray( actions ) ) {
+					actions = [ actions ];
+				}
 
 
-			test(
-				'should insert a paragraph only when an entire widget is selected (#2)',
-				'<paragraph>f[oo</paragraph><widget></widget><paragraph>b]ar</paragraph>',
-				keyCodes.enter,
-				'<paragraph>f[]ar</paragraph>'
-			);
+				actions = actions.map( action => {
+					if ( typeof action === 'object' ) {
+						return action;
+					}
 
 
-			// https://github.com/ckeditor/ckeditor5/issues/1529
-			it( 'should split parent when widget is inside a block element', () => {
-				model.schema.register( 'allowP', {
-					inheritAllFrom: '$block'
-				} );
-				model.schema.register( 'disallowP', {
-					inheritAllFrom: '$block',
-					allowIn: [ 'allowP' ]
+					return {
+						keyCode: action
+					};
 				} );
 				} );
-				model.schema.extend( 'widget', {
-					allowIn: [ 'allowP', 'disallowP' ]
-				} );
-				model.schema.extend( 'paragraph', {
-					allowIn: [ 'allowP' ]
-				} );
-
-				editor.conversion.for( 'downcast' ).elementToElement( { model: 'parent', view: 'parent' } );
-				editor.conversion.for( 'downcast' ).elementToElement( { model: 'allowP', view: 'allowP' } );
-				editor.conversion.for( 'downcast' ).elementToElement( { model: 'disallowP', view: 'disallowP' } );
-
-				setModelData( model, '<allowP><disallowP>[<widget></widget>]</disallowP></allowP>' );
-
-				viewDocument.fire( 'keydown', new DomEventData(
-					viewDocument,
-					{ target: document.createElement( 'div' ), preventDefault() {} },
-					{ keyCode: keyCodes.enter }
-				) );
-
-				expect( getModelData( model ) ).to.equal(
-					'<allowP><disallowP><widget></widget></disallowP><paragraph>[]</paragraph><disallowP></disallowP></allowP>'
-				);
-			} );
-
-			test(
-				'should do nothing if selected is inline object',
-				'<paragraph>foo[<inline-widget></inline-widget>]bar</paragraph>',
-				keyCodes.enter,
-				'<paragraph>foo[]bar</paragraph>'
-			);
-
-			test(
-				'should insert a paragraph after the selected widget inside an element that is not a block upon Enter',
-				'<blockQuote>[<widget></widget>]</blockQuote>',
-				keyCodes.enter,
-				'<blockQuote><widget></widget><paragraph>[]</paragraph></blockQuote>'
-			);
-
-			test(
-				'should insert a paragraph before the selected widget inside an element that is not a block upon Shift+Enter',
-				'<blockQuote>[<widget></widget>]</blockQuote>',
-				{ keyCode: keyCodes.enter, shiftKey: true },
-				'<blockQuote><paragraph>[]</paragraph><widget></widget></blockQuote>'
-			);
-		} );
-
-		function test( name, data, keyCodeOrMock, expected, expectedView, contentLanguageDirection = 'ltr' ) {
-			it( name, () => {
-				testUtils.sinon.stub( editor.locale, 'contentLanguageDirection' ).value( contentLanguageDirection );
-
-				const domEventDataMock = ( typeof keyCodeOrMock == 'object' ) ? keyCodeOrMock : {
-					keyCode: keyCodeOrMock
-				};
 
 
 				setModelData( model, data );
 				setModelData( model, data );
-				viewDocument.fire( 'keydown', new DomEventData(
-					viewDocument,
-					{ target: document.createElement( 'div' ), preventDefault() {} },
-					domEventDataMock
-				) );
+
+				for ( const action of actions ) {
+					viewDocument.fire( 'keydown', new DomEventData(
+						viewDocument,
+						{ target: document.createElement( 'div' ), preventDefault() {} },
+						action
+					) );
+				}
 
 
 				expect( getModelData( model ) ).to.equal( expected );
 				expect( getModelData( model ) ).to.equal( expected );
 
 
@@ -1274,7 +1263,7 @@ describe( 'Widget', () => {
 
 
 			return ClassicTestEditor
 			return ClassicTestEditor
 				.create( element, {
 				.create( element, {
-					plugins: [ Widget, Typing ]
+					plugins: [ Paragraph, Widget, Typing ]
 				} )
 				} )
 				.then( newEditor => {
 				.then( newEditor => {
 					editor = newEditor;
 					editor = newEditor;
@@ -1349,18 +1338,16 @@ describe( 'Widget', () => {
 			expect( getViewData( view ) ).to.equal(
 			expect( getViewData( view ) ).to.equal(
 				'[<div class="' +
 				'[<div class="' +
 					'ck-widget ' +
 					'ck-widget ' +
-					'ck-widget_can-type-around_after ck-widget_can-type-around_before ' +
 					'ck-widget_selected ck-widget_with-selection-handle" contenteditable="false"' +
 					'ck-widget_selected ck-widget_with-selection-handle" contenteditable="false"' +
 				'>' +
 				'>' +
 					'<div class="' +
 					'<div class="' +
 						'ck-widget ' +
 						'ck-widget ' +
-						'ck-widget_can-type-around_after ck-widget_can-type-around_before ' +
 						'ck-widget_with-selection-handle" contenteditable="false"' +
 						'ck-widget_with-selection-handle" contenteditable="false"' +
 					'>' +
 					'>' +
 						'<div class="ck ck-widget__selection-handle"></div>' +
 						'<div class="ck ck-widget__selection-handle"></div>' +
 						'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
 						'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
 					'</div>' +
 					'</div>' +
-					'<div class="ck-widget ck-widget_can-type-around_before ck-widget_with-selection-handle" contenteditable="false">' +
+					'<div class="ck-widget ck-widget_with-selection-handle" contenteditable="false">' +
 						'<div class="ck ck-widget__selection-handle"></div>' +
 						'<div class="ck ck-widget__selection-handle"></div>' +
 						'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
 						'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
 					'</div>' +
 					'</div>' +
@@ -1390,23 +1377,22 @@ describe( 'Widget', () => {
 			viewDocument.fire( 'mousedown', domEventDataMock );
 			viewDocument.fire( 'mousedown', domEventDataMock );
 
 
 			expect( getViewData( view ) ).to.equal(
 			expect( getViewData( view ) ).to.equal(
-				'<div class="ck-widget ck-widget_can-type-around_after ck-widget_can-type-around_before ck-widget_with-selection-handle" ' +
+				'<div class="ck-widget ck-widget_with-selection-handle" ' +
 				'contenteditable="false">' +
 				'contenteditable="false">' +
 					'<div class="ck ck-widget__selection-handle"></div>' +
 					'<div class="ck ck-widget__selection-handle"></div>' +
 					'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
 					'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
 				'</div>' +
 				'</div>' +
 				'[<div class="' +
 				'[<div class="' +
 					'ck-widget ' +
 					'ck-widget ' +
-					'ck-widget_can-type-around_after ck-widget_can-type-around_before ' +
 					'ck-widget_selected ck-widget_with-selection-handle" contenteditable="false"' +
 					'ck-widget_selected ck-widget_with-selection-handle" contenteditable="false"' +
 				'>' +
 				'>' +
 					'<div ' +
 					'<div ' +
-					'class="ck-widget ck-widget_can-type-around_after ck-widget_can-type-around_before ck-widget_with-selection-handle" ' +
+					'class="ck-widget ck-widget_with-selection-handle" ' +
 					'contenteditable="false">' +
 					'contenteditable="false">' +
 					'<div class="ck ck-widget__selection-handle"></div>' +
 					'<div class="ck ck-widget__selection-handle"></div>' +
 					'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
 					'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
 				'</div>' +
 				'</div>' +
-					'<div class="ck-widget ck-widget_can-type-around_before ck-widget_with-selection-handle" ' +
+					'<div class="ck-widget ck-widget_with-selection-handle" ' +
 					'contenteditable="false">' +
 					'contenteditable="false">' +
 					'<div class="ck ck-widget__selection-handle"></div>' +
 					'<div class="ck ck-widget__selection-handle"></div>' +
 					'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
 					'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
@@ -1415,7 +1401,7 @@ describe( 'Widget', () => {
 					'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
 					'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
 				'</div>]' +
 				'</div>]' +
 					'<div ' +
 					'<div ' +
-					'class="ck-widget ck-widget_can-type-around_after ck-widget_can-type-around_before ck-widget_with-selection-handle" ' +
+					'class="ck-widget ck-widget_with-selection-handle" ' +
 					'contenteditable="false">' +
 					'contenteditable="false">' +
 					'<div class="ck ck-widget__selection-handle"></div>' +
 					'<div class="ck ck-widget__selection-handle"></div>' +
 					'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
 					'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
@@ -1443,7 +1429,6 @@ describe( 'Widget', () => {
 			expect( getViewData( view ) ).to.equal(
 			expect( getViewData( view ) ).to.equal(
 				'[<div class="' +
 				'[<div class="' +
 					'ck-widget ' +
 					'ck-widget ' +
-					'ck-widget_can-type-around_after ck-widget_can-type-around_before ' +
 					'ck-widget_selected ck-widget_with-selection-handle" contenteditable="false"' +
 					'ck-widget_selected ck-widget_with-selection-handle" contenteditable="false"' +
 				'>' +
 				'>' +
 					'<figcaption contenteditable="true">foo bar</figcaption>' +
 					'<figcaption contenteditable="true">foo bar</figcaption>' +
@@ -1479,12 +1464,10 @@ describe( 'Widget', () => {
 			expect( getViewData( view ) ).to.equal(
 			expect( getViewData( view ) ).to.equal(
 				'<div class="' +
 				'<div class="' +
 					'ck-widget ' +
 					'ck-widget ' +
-					'ck-widget_can-type-around_after ck-widget_can-type-around_before ' +
 					'ck-widget_with-selection-handle" contenteditable="false"' +
 					'ck-widget_with-selection-handle" contenteditable="false"' +
 				'>' +
 				'>' +
 					'<div class="' +
 					'<div class="' +
 						'ck-widget ' +
 						'ck-widget ' +
-						'ck-widget_can-type-around_after ck-widget_can-type-around_before ' +
 						'ck-widget_with-selection-handle" contenteditable="false"' +
 						'ck-widget_with-selection-handle" contenteditable="false"' +
 					'>' +
 					'>' +
 						'<div class="ck ck-widget__selection-handle"></div>' +
 						'<div class="ck ck-widget__selection-handle"></div>' +
@@ -1492,10 +1475,9 @@ describe( 'Widget', () => {
 					'</div>' +
 					'</div>' +
 					'[<div class="' +
 					'[<div class="' +
 						'ck-widget ' +
 						'ck-widget ' +
-						'ck-widget_can-type-around_before ' +
 						'ck-widget_selected ck-widget_with-selection-handle" contenteditable="false"' +
 						'ck-widget_selected ck-widget_with-selection-handle" contenteditable="false"' +
 					'>' +
 					'>' +
-						'<div class="ck-widget ck-widget_can-type-around_before ck-widget_with-selection-handle" contenteditable="false">' +
+						'<div class="ck-widget ck-widget_with-selection-handle" contenteditable="false">' +
 							'<div class="ck ck-widget__selection-handle"></div>' +
 							'<div class="ck ck-widget__selection-handle"></div>' +
 							'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
 							'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
 						'</div>' +
 						'</div>' +
@@ -1525,13 +1507,11 @@ describe( 'Widget', () => {
 			expect( getViewData( view ) ).to.equal(
 			expect( getViewData( view ) ).to.equal(
 				'<div class="' +
 				'<div class="' +
 					'ck-widget ' +
 					'ck-widget ' +
-					'ck-widget_can-type-around_after ck-widget_can-type-around_before ' +
 					'ck-widget_with-selection-handle" contenteditable="false"' +
 					'ck-widget_with-selection-handle" contenteditable="false"' +
 				'>' +
 				'>' +
 					'<figcaption contenteditable="true">[' +
 					'<figcaption contenteditable="true">[' +
 						'<div class="' +
 						'<div class="' +
 							'ck-widget ' +
 							'ck-widget ' +
-							'ck-widget_can-type-around_after ck-widget_can-type-around_before ' +
 							'ck-widget_selected ck-widget_with-selection-handle" contenteditable="false"' +
 							'ck-widget_selected ck-widget_with-selection-handle" contenteditable="false"' +
 						'>' +
 						'>' +
 							'<div class="ck ck-widget__selection-handle"></div>' +
 							'<div class="ck ck-widget__selection-handle"></div>' +

+ 4 - 1
packages/ckeditor5-widget/tests/widgetresize.js

@@ -561,7 +561,10 @@ describe( 'WidgetResize', () => {
 			.create( element, {
 			.create( element, {
 				plugins: [
 				plugins: [
 					ArticlePluginSet, WidgetResize, simpleWidgetPlugin
 					ArticlePluginSet, WidgetResize, simpleWidgetPlugin
-				]
+				],
+				image: {
+					toolbar: [ 'imageStyle:full', 'imageStyle:side' ]
+				}
 			} );
 			} );
 	}
 	}
 
 

+ 4 - 1
packages/ckeditor5-widget/tests/widgetresize/resizer.js

@@ -24,7 +24,10 @@ describe( 'Resizer', () => {
 			.create( editorElement, {
 			.create( editorElement, {
 				plugins: [
 				plugins: [
 					ArticlePluginSet
 					ArticlePluginSet
-				]
+				],
+				image: {
+					toolbar: [ 'imageStyle:full', 'imageStyle:side' ]
+				}
 			} )
 			} )
 			.then( newEditor => {
 			.then( newEditor => {
 				editor = newEditor;
 				editor = newEditor;

+ 15 - 1
packages/ckeditor5-widget/tests/widgettoolbarrepository.js

@@ -3,7 +3,7 @@
  * 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
  */
  */
 
 
-/* global document */
+/* global document, console */
 
 
 import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
 import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
 import BalloonEditor from '@ckeditor/ckeditor5-editor-balloon/src/ballooneditor';
 import BalloonEditor from '@ckeditor/ckeditor5-editor-balloon/src/ballooneditor';
@@ -126,6 +126,20 @@ describe( 'WidgetToolbarRepository', () => {
 
 
 			toolbarView.destroy();
 			toolbarView.destroy();
 		} );
 		} );
+
+		it( 'should not register a toolbar when passed an empty collection of the items', () => {
+			const consoleWarnStub = sinon.stub( console, 'warn' );
+
+			widgetToolbarRepository.register( 'fake', {
+				items: [],
+				getRelatedElement: () => null
+			} );
+
+			expect( widgetToolbarRepository._toolbarDefinitions.get( 'fake' ) ).to.be.undefined;
+
+			expect( consoleWarnStub.calledOnce ).to.equal( true );
+			expect( consoleWarnStub.firstCall.args[ 0 ] ).to.match( /^widget-toolbar-no-items:/ );
+		} );
 	} );
 	} );
 
 
 	describe( 'integration tests', () => {
 	describe( 'integration tests', () => {

File diff suppressed because it is too large
+ 1054 - 57
packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js


+ 36 - 11
packages/ckeditor5-widget/theme/widgettypearound.css

@@ -37,17 +37,6 @@
 		}
 		}
 	}
 	}
 
 
-	/*
-	 * Hide the type around buttons depending on which directions the widget supports.
-	 */
-	&:not(.ck-widget_can-type-around_before) > .ck-widget__type-around > .ck-widget__type-around__button_before {
-		display: none;
-	}
-
-	&:not(.ck-widget_can-type-around_after) > .ck-widget__type-around > .ck-widget__type-around__button_after {
-		display: none;
-	}
-
 	/*
 	/*
 	 * Styles for the buttons when:
 	 * Styles for the buttons when:
 	 * - the widget is selected,
 	 * - the widget is selected,
@@ -64,6 +53,42 @@
 			z-index: calc(var(--ck-z-default) + 1);
 			z-index: calc(var(--ck-z-default) + 1);
 		}
 		}
 	}
 	}
+
+	/*
+	 * Styles for the horizontal "fake caret" which is displayed when the user navigates using the keyboard.
+	 */
+	& > .ck-widget__type-around > .ck-widget__type-around__fake-caret {
+		display: none;
+		position: absolute;
+		left: 0;
+		right: 0;
+	}
+
+	/*
+	 * When the widget is hovered the "fake caret" would normally be narrower than the
+	 * extra outline displayed around the widget. Let's extend the "fake caret" to match
+	 * the full width of the widget.
+	 */
+	&:hover > .ck-widget__type-around > .ck-widget__type-around__fake-caret {
+		left: calc( -1 * var(--ck-widget-outline-thickness) );
+		right: calc( -1 * var(--ck-widget-outline-thickness) );
+	}
+
+	/*
+	 * Styles for the horizontal "fake caret" when it should be displayed before the widget (backward keyboard navigation).
+	 */
+	&.ck-widget_type-around_show-fake-caret_before > .ck-widget__type-around > .ck-widget__type-around__fake-caret {
+		top: calc( -1 * var(--ck-widget-outline-thickness) - 1px );
+		display: block;
+	}
+
+	/*
+	 * Styles for the horizontal "fake caret" when it should be displayed after the widget (forward keyboard navigation).
+	 */
+	&.ck-widget_type-around_show-fake-caret_after > .ck-widget__type-around > .ck-widget__type-around__fake-caret {
+		bottom: calc( -1 * var(--ck-widget-outline-thickness) - 1px );
+		display: block;
+	}
 }
 }
 
 
 /*
 /*

Some files were not shown because too many files changed in this diff