瀏覽代碼

Merge branch 'master' into i/714-revert

# Conflicts:
#	packages/ckeditor5-widget/tests/widgetresize/_utils/utils.js
Maciej Gołaszewski 5 年之前
父節點
當前提交
ac06823256
共有 100 個文件被更改,包括 2824 次插入324 次删除
  1. 1 1
      CHANGELOG.md
  2. 1 0
      package.json
  3. 1 1
      packages/ckeditor5-basic-styles/src/code/codeediting.js
  4. 1 1
      packages/ckeditor5-basic-styles/tests/code/codeediting.js
  5. 0 0
      packages/ckeditor5-engine/docs/_snippets/framework/build-element-reconversion-source.html
  6. 22 0
      packages/ckeditor5-engine/docs/_snippets/framework/build-element-reconversion-source.js
  7. 111 0
      packages/ckeditor5-engine/docs/_snippets/framework/element-reconversion-demo.html
  8. 355 0
      packages/ckeditor5-engine/docs/_snippets/framework/element-reconversion-demo.js
  9. 9 9
      packages/ckeditor5-engine/docs/_snippets/framework/extending-content-custom-element-converter.html
  10. 626 0
      packages/ckeditor5-engine/docs/framework/guides/deep-dive/element-reconversion.md
  11. 2 0
      packages/ckeditor5-image/docs/_snippets/features/build-image-source.js
  12. 1 1
      packages/ckeditor5-image/docs/_snippets/features/image-caption.js
  13. 3 0
      packages/ckeditor5-image/docs/_snippets/features/image-insert-via-pasting-url-into-editor.html
  14. 20 0
      packages/ckeditor5-image/docs/_snippets/features/image-insert-via-pasting-url-into-editor.js
  15. 1 1
      packages/ckeditor5-image/docs/_snippets/features/image-insert-via-url.js
  16. 1 1
      packages/ckeditor5-image/docs/_snippets/features/image-link.js
  17. 1 1
      packages/ckeditor5-image/docs/_snippets/features/image-resize-buttons-dropdown.js
  18. 1 1
      packages/ckeditor5-image/docs/_snippets/features/image-resize-buttons.js
  19. 1 1
      packages/ckeditor5-image/docs/_snippets/features/image-resize-px.js
  20. 1 1
      packages/ckeditor5-image/docs/_snippets/features/image-resize.js
  21. 1 1
      packages/ckeditor5-image/docs/_snippets/features/image-style-presentational.js
  22. 1 1
      packages/ckeditor5-image/docs/_snippets/features/image-style.js
  23. 1 1
      packages/ckeditor5-image/docs/_snippets/features/image-toolbar.js
  24. 1 1
      packages/ckeditor5-image/docs/_snippets/features/image.js
  25. 1 0
      packages/ckeditor5-image/docs/api/image.md
  26. 17 0
      packages/ckeditor5-image/docs/features/image.md
  27. 174 0
      packages/ckeditor5-image/src/autoimage.js
  28. 4 6
      packages/ckeditor5-image/src/image/imageinsertcommand.js
  29. 13 12
      packages/ckeditor5-image/src/image/utils.js
  30. 1 2
      packages/ckeditor5-image/src/imageinsert/utils.js
  31. 38 12
      packages/ckeditor5-image/src/imageresize/imageresizehandles.js
  32. 4 0
      packages/ckeditor5-image/src/imagetextalternative/imagetextalternativeui.js
  33. 3 1
      packages/ckeditor5-image/src/imagetextalternative/ui/textalternativeformview.js
  34. 6 9
      packages/ckeditor5-image/src/imageupload/imageuploadcommand.js
  35. 555 0
      packages/ckeditor5-image/tests/autoimage.js
  36. 3 9
      packages/ckeditor5-image/tests/image/utils.js
  37. 5 4
      packages/ckeditor5-image/tests/imageinsert/ui/imageinsertpanelview.js
  38. 1 6
      packages/ckeditor5-image/tests/imageinsert/utils.js
  39. 37 0
      packages/ckeditor5-image/tests/imageresize/_utils/utils.js
  40. 1 4
      packages/ckeditor5-image/tests/imageresize/imageresizeediting.js
  41. 78 17
      packages/ckeditor5-image/tests/imageresize/imageresizehandles.js
  42. 13 0
      packages/ckeditor5-image/tests/imagetextalternative/imagetextalternativeui.js
  43. 5 1
      packages/ckeditor5-image/tests/imagetextalternative/ui/textalternativeformview.js
  44. 3 2
      packages/ckeditor5-image/tests/manual/imageinsertviaurl.js
  45. 5 0
      packages/ckeditor5-image/tests/manual/imageinsertviaurl.md
  46. 1 1
      packages/ckeditor5-image/theme/imageinsert.css
  47. 5 1
      packages/ckeditor5-link/src/linkui.js
  48. 6 5
      packages/ckeditor5-link/src/ui/linkformview.js
  49. 12 0
      packages/ckeditor5-link/tests/linkui.js
  50. 7 8
      packages/ckeditor5-link/tests/ui/linkformview.js
  51. 11 0
      packages/ckeditor5-link/theme/linkform.css
  52. 35 1
      packages/ckeditor5-markdown-gfm/docs/_snippets/features/markdown.html
  53. 20 2
      packages/ckeditor5-markdown-gfm/docs/_snippets/features/markdown.js
  54. 19 9
      packages/ckeditor5-markdown-gfm/docs/features/markdown.md
  55. 15 0
      packages/ckeditor5-media-embed/src/mediaembedui.js
  56. 3 1
      packages/ckeditor5-media-embed/src/ui/mediaformview.js
  57. 9 11
      packages/ckeditor5-media-embed/tests/automediaembed.js
  58. 10 0
      packages/ckeditor5-media-embed/tests/mediaembedui.js
  59. 7 6
      packages/ckeditor5-media-embed/tests/ui/mediaformview.js
  60. 96 7
      packages/ckeditor5-paste-from-office/src/filters/list.js
  61. 2 2
      packages/ckeditor5-paste-from-office/tests/_data/list/heading1/model.word2016.html
  62. 2 2
      packages/ckeditor5-paste-from-office/tests/_data/list/heading3-styled/model.word2016.html
  63. 1 1
      packages/ckeditor5-paste-from-office/tests/_data/list/heading3-styled/normalized.safari.word2016.html
  64. 1 1
      packages/ckeditor5-paste-from-office/tests/_data/list/heading3-styled/normalized.word2016.html
  65. 1 1
      packages/ckeditor5-paste-from-office/tests/_data/list/heading7/model.word2016.html
  66. 12 12
      packages/ckeditor5-paste-from-office/tests/_data/list/many-one-item/model.word2016.html
  67. 8 8
      packages/ckeditor5-paste-from-office/tests/_data/list/many-one-item/normalized.safari.word2016.html
  68. 8 8
      packages/ckeditor5-paste-from-office/tests/_data/list/many-one-item/normalized.word2016.html
  69. 4 4
      packages/ckeditor5-paste-from-office/tests/_data/list/multiple-combined/model.word2016.html
  70. 3 3
      packages/ckeditor5-paste-from-office/tests/_data/list/multiple/model.word2016.html
  71. 1 1
      packages/ckeditor5-paste-from-office/tests/_data/list/multiple/normalized.safari.word2016.html
  72. 1 1
      packages/ckeditor5-paste-from-office/tests/_data/list/multiple/normalized.word2016.html
  73. 6 6
      packages/ckeditor5-paste-from-office/tests/_data/list/nested-mixed/model.word2016.html
  74. 2 2
      packages/ckeditor5-paste-from-office/tests/_data/list/nested-mixed/normalized.word2016.html
  75. 9 9
      packages/ckeditor5-paste-from-office/tests/_data/list/nested-multiple/model.word2016.html
  76. 6 6
      packages/ckeditor5-paste-from-office/tests/_data/list/nested-multiple/normalized.safari.word2016.html
  77. 5 5
      packages/ckeditor5-paste-from-office/tests/_data/list/nested-multiple/normalized.word2016.html
  78. 7 7
      packages/ckeditor5-paste-from-office/tests/_data/list/nested/model.word2016.html
  79. 2 2
      packages/ckeditor5-paste-from-office/tests/_data/list/nested/normalized.word2016.html
  80. 12 12
      packages/ckeditor5-paste-from-office/tests/_data/list/resume-template/model.safari.word2016.html
  81. 12 12
      packages/ckeditor5-paste-from-office/tests/_data/list/resume-template/model.word2016.html
  82. 8 8
      packages/ckeditor5-paste-from-office/tests/_data/list/resume-template/normalized.safari.word2016.html
  83. 8 8
      packages/ckeditor5-paste-from-office/tests/_data/list/resume-template/normalized.word2016.html
  84. 2 2
      packages/ckeditor5-paste-from-office/tests/_data/list/simple/model.word2016.html
  85. 3 3
      packages/ckeditor5-paste-from-office/tests/_data/list/styled/model.safari.word2016.html
  86. 3 3
      packages/ckeditor5-paste-from-office/tests/_data/list/styled/model.word2016.html
  87. 1 1
      packages/ckeditor5-paste-from-office/tests/_data/list/styled/normalized.safari.word2016.html
  88. 1 1
      packages/ckeditor5-paste-from-office/tests/_data/list/styled/normalized.word2016.html
  89. 2 1
      packages/ckeditor5-paste-from-office/tests/data/integration.js
  90. 165 0
      packages/ckeditor5-paste-from-office/tests/filters/list.js
  91. 3 3
      packages/ckeditor5-table/src/tablecellproperties/tablecellpropertiesui.js
  92. 24 7
      packages/ckeditor5-table/src/tablecellproperties/ui/tablecellpropertiesview.js
  93. 3 3
      packages/ckeditor5-table/src/tableproperties/tablepropertiesui.js
  94. 26 10
      packages/ckeditor5-table/src/tableproperties/ui/tablepropertiesview.js
  95. 25 4
      packages/ckeditor5-table/src/ui/colorinputview.js
  96. 3 1
      packages/ckeditor5-table/src/utils/ui/table-properties.js
  97. 14 0
      packages/ckeditor5-table/tests/tablecellproperties/tablecellpropertiesui.js
  98. 14 2
      packages/ckeditor5-table/tests/tablecellproperties/ui/tablecellpropertiesview.js
  99. 14 0
      packages/ckeditor5-table/tests/tableproperties/tablepropertiesui.js
  100. 14 2
      packages/ckeditor5-table/tests/tableproperties/ui/tablepropertiesview.js

+ 1 - 1
CHANGELOG.md

@@ -7,7 +7,7 @@ Changelog
 
 We are happy to announce the release of CKEditor 5 v23.1.0.
 
-This release introduces a new HTML embed feature and adds the list style feature to the document editor build.
+This release introduces a new [HTML embed feature](https://ckeditor.com/docs/ckeditor5/latest/features/html-embed.html) and adds the list style feature to the document editor build.
 
 It also comes with new API features:
 

+ 1 - 0
package.json

@@ -117,6 +117,7 @@
     "svgo": "^1.3.2",
     "terser-webpack-plugin": "^3.0.2",
     "umberto": "^1.5.4",
+    "upath": "^2.0.0",
     "webpack": "^4.43.0"
   },
   "engines": {

+ 1 - 1
packages/ckeditor5-basic-styles/src/code/codeediting.js

@@ -48,7 +48,7 @@ export default class CodeEditing extends Plugin {
 		editor.model.schema.extend( '$text', { allowAttributes: CODE } );
 		editor.model.schema.setAttributeProperties( CODE, {
 			isFormatting: true,
-			copyOnEnter: true
+			copyOnEnter: false
 		} );
 
 		editor.conversion.attributeToElement( {

+ 1 - 1
packages/ckeditor5-basic-styles/tests/code/codeediting.js

@@ -54,7 +54,7 @@ describe( 'CodeEditing', () => {
 
 	it( 'its attribute is marked with a copOnEnter property', () => {
 		expect( model.schema.getAttributeProperties( 'code' ) ).to.include( {
-			copyOnEnter: true
+			copyOnEnter: false
 		} );
 	} );
 

+ 0 - 0
packages/ckeditor5-engine/docs/_snippets/framework/build-element-reconversion-source.html


+ 22 - 0
packages/ckeditor5-engine/docs/_snippets/framework/build-element-reconversion-source.js

@@ -0,0 +1,22 @@
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals window */
+
+import ClassicEditor from '@ckeditor/ckeditor5-build-classic/src/ckeditor';
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
+import Command from '@ckeditor/ckeditor5-core/src/command';
+import { toWidget, toWidgetEditable, findOptimalInsertionPosition } from '@ckeditor/ckeditor5-widget/src/utils';
+import createElement from '@ckeditor/ckeditor5-utils/src/dom/createelement';
+
+window.ClassicEditor = ClassicEditor;
+window.Plugin = Plugin;
+window.ButtonView = ButtonView;
+window.Command = Command;
+window.toWidget = toWidget;
+window.toWidgetEditable = toWidgetEditable;
+window.createElement = createElement;
+window.findOptimalInsertionPosition = findOptimalInsertionPosition;

+ 111 - 0
packages/ckeditor5-engine/docs/_snippets/framework/element-reconversion-demo.html

@@ -0,0 +1,111 @@
+<style>
+	.ck-content .side-card {
+		padding: 0.5em;
+		border-radius: 8px;
+		width: 300px;
+		margin: 1em auto 1em 1.5em;
+		font-size: 0.9em;
+		float: right;
+	}
+
+	.ck-content .side-card.ck-widget.ck-widget_with-selection-handle .ck-widget__selection-handle {
+		transform: translateY(calc(-100% - 7px));
+		left: calc(0px - var(--ck-widget-outline-thickness) - 1px)
+	}
+
+	.ck-content .side-card-title {
+		margin-bottom: 0.5em;
+		font-size: 1.2em;
+		color: inherit;
+		border-bottom-color: inherit;
+		border-bottom-style: solid;
+		border-bottom-width: 1px;
+	}
+
+	.ck-content .side-card-alternate {
+		background: hsl(48, 100%, 93%);
+		border: 1px solid hsl(47, 100%, 75%);
+	}
+
+	.ck-content .side-card-alternate .side-card-title {
+		color: hsl(47, 100%, 32%)
+	}
+
+	.ck-content .side-card-default {
+		background: hsl(205, 100%, 95%);
+		border: 1px solid hsl(205, 91%, 82%);
+	}
+
+	.ck-content .side-card-default .side-card-title {
+		color: hsl(204, 79%, 41%)
+	}
+
+	.ck-content .side-card-section {
+		padding: 4px;
+		background: hsl(0, 0%, 100%);
+		border-radius: 6px;
+		margin-top: 0.5em;
+	}
+
+	.ck-content .side-card-section p:only-child,
+	.ck-content .side-card-section p:last-child {
+		margin-bottom: 0;
+	}
+
+	.ck-content .side-card-url {
+		margin: 0.5em 0;
+		font-style: italic;
+		font-size: 0.8em;
+		color: hsl(0, 0%, 20%);
+	}
+
+	.ck-content .side-card-actions {
+		margin: 0.5em 0;
+	}
+
+	.ck-content .side-card-actions button {
+		font-size: 0.8em;
+		border: 1px solid hsl(0, 0%, 80%);
+		padding: 2px 4px;
+		margin-right: 0.5em;
+	}
+</style>
+
+<div id="editor-element-reconversion">
+	<p>With the break of dawn came the stranger. A gunslinger, little doubt did he leave about it, sporting a rather big iron on his hip. He came from the southside and rode the street slowly, speaking none. The people of Agua Caliente had little liking for such visitors but there was no-one to ask the rider what his business was, as he progressed, eyeing the buildings and those few townfolks he could spot.</p>
+	<p>- Aye, let me tell ya, that&rsquo;s some right hangdog - commented the undertaker.</p>
+	<p>- Sure as my name he&rsquo;s on the run - whispered Laughing Jessie, the saloon owner. - And I am damned if he ain&rsquo;t got some business here!</p>
+
+	<aside class="side-card side-card-default">
+		<div class="side-card-title">Gunslinger</div>
+		<div class="side-card-section">
+			<p>Used to denote someone who is quick on the draw with a pistol.</p>
+		</div>
+		<div class="side-card-section">
+			<p>A word dated to 1920s.</p>
+		</div>
+		<div class="side-card-url">https://www.dictionary.com/browse/gunslinger</div>
+	</aside>
+
+	<p>The rider strode closer to the talking duo. He tipped his hat, but he didn&rsquo;t dismount.</p>
+	<p>- Looking for Kansas Ted - he spoke slowly, in a firm, sound voice. - Any chance you know where he&rsquo;s at?</p>
+	<p>- And who&rsquo;s that asking? - Jessie wanted to know. - Kansas Ted is no fun character to meet.</p>
+	<p>- This I know - spoke the stranger. - I&rsquo;m a mounted trooper from New Mexico and I am after him for killing twenty people. And I will take him back with me, be him alive or dead, that makes me no difference whatsoever.</p>
+	<p>And the handsome New Mexico trooper rested his hand firmly on the big iron on his hip, leaving no place for doubt about what his business with Ted might be.</p>
+	<p>A word of mouth rounded the town of Agua Caliente and soon Kansas himself knew there was a shooting planned for the day. He was neither worried nor afraid as he was used to people trying to gun him down. He was only 24, but there were already twenty notches on his pistol as all those men that tried to take him have failed. He went on with his usual life all morning, and it was twenty past eleven when he finally walked out to the street. The handsome trooper was there. And the townfolks were there, looking from the windows.</p>
+
+	<aside class="side-card side-card-alternate">
+		<div class="side-card-title">Gunman</div>
+		<div class="side-card-section">
+			<p>Was a hired gun who made a living with his weapons in the Old West.</p>
+		</div>
+		<div class="side-card-section">
+			<p>Used mostly pre-20th century.</p>
+		</div>
+		<div class="side-card-url">https://www.dictionary.com/browse/gunman</div>
+	</aside>
+
+	<p>Kansas Ted stood there smiling, relaxed. A vicious man convinced that the stranger from New Mexico was facing defeat. So thought the people of Agua Caliente, watching this show with ghoulish delight. And then a bullet ripped and there was a look of surprise on the face of the young killer as he didn&rsquo;t even clear the leather. He met his death from the hands of the New Mexico trooper, who put the big iron back to his hip.</p>
+	<p>Folks gathered around the body, as it was lying lifelessly in the dust. Kansas Ted made his one final mistake when he tried to match a superior gunslinger. Nobody spoke as the trooper rode out of town, taking the fallen gunman with him.</p>
+
+</div>

+ 355 - 0
packages/ckeditor5-engine/docs/_snippets/framework/element-reconversion-demo.js

@@ -0,0 +1,355 @@
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals
+	ClassicEditor, Plugin, ButtonView, Command, toWidget, toWidgetEditable, createElement, findOptimalInsertionPosition,
+	console, window, prompt, document
+*/
+
+import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config';
+
+/**
+ * Helper for extracting the side card type from a view element based on its CSS class.
+ */
+const getTypeFromViewElement = viewElement => {
+	for ( const type of [ 'default', 'alternate' ] ) {
+		if ( viewElement.hasClass( `side-card-${ type }` ) ) {
+			return type;
+		}
+	}
+
+	return 'default';
+};
+
+/**
+ * Single upcast converter to the <sideCard/> element with all its attributes.
+ */
+const upcastCard = ( viewElement, { writer } ) => {
+	const sideCard = writer.createElement( 'sideCard' );
+
+	const type = getTypeFromViewElement( viewElement );
+	writer.setAttribute( 'cardType', type, sideCard );
+
+	const urlWrapper = [ ...viewElement.getChildren() ].find( child => {
+		return child.is( 'element', 'div' ) && child.hasClass( 'side-card-url' );
+	} );
+
+	if ( urlWrapper ) {
+		writer.setAttribute( 'cardURL', urlWrapper.getChild( 0 ).data, sideCard );
+	}
+
+	return sideCard;
+};
+
+/**
+ * Helper for creating a DOM button with an editor callback.
+ */
+const addActionButton = ( text, callback, domElement, editor ) => {
+	const domDocument = domElement.ownerDocument;
+
+	const button = createElement( domDocument, 'button', {}, [ text ] );
+
+	button.addEventListener( 'click', () => {
+		editor.model.change( callback );
+	} );
+
+	domElement.appendChild( button );
+
+	return button;
+};
+
+/**
+ * Helper function that creates the card editing UI inside the card.
+ */
+const createActionsView = ( editor, modelElement ) => function( domElement ) {
+	//
+	// Set the URL action button.
+	//
+	addActionButton( 'Set URL', writer => {
+		// eslint-disable-next-line no-alert
+		const newURL = prompt( 'Set URL', modelElement.getAttribute( 'cardURL' ) || '' );
+
+		writer.setAttribute( 'cardURL', newURL, modelElement );
+	}, domElement, editor );
+
+	const currentType = modelElement.getAttribute( 'cardType' );
+	const newType = currentType === 'default' ? 'alternate' : 'default';
+
+	//
+	// Change the card action button.
+	//
+	addActionButton( 'Change type', writer => {
+		writer.setAttribute( 'cardType', newType, modelElement );
+	}, domElement, editor );
+
+	const childCount = modelElement.childCount;
+
+	//
+	// Add the content section to the card action button.
+	//
+	const addButton = addActionButton( 'Add section', writer => {
+		writer.insertElement( 'sideCardSection', modelElement, 'end' );
+	}, domElement, editor );
+
+	// Disable the button so only 1-3 content boxes are in the card (there will always be a title).
+	if ( childCount > 4 ) {
+		addButton.setAttribute( 'disabled', 'disabled' );
+	}
+
+	//
+	// Remove the content section from the card action button.
+	//
+	const removeButton = addActionButton( 'Remove section', writer => {
+		writer.remove( modelElement.getChild( childCount - 1 ) );
+	}, domElement, editor );
+
+	// Disable the button so only 1-3 content boxes are in the card (there will always be a title).
+	if ( childCount < 3 ) {
+		removeButton.setAttribute( 'disabled', 'disabled' );
+	}
+};
+
+/**
+ * The downcast converter for the <sideCard/> element.
+ *
+ * It returns the full view structure based on the current state of the model element.
+ */
+const downcastSideCard = ( editor, { asWidget } ) => {
+	return ( modelElement, { writer, consumable, mapper } ) => {
+		const type = modelElement.getAttribute( 'cardType' ) || 'default';
+
+		// The main view element for the side card.
+		const sideCardView = writer.createContainerElement( 'aside', {
+			class: `side-card side-card-${ type }`
+		} );
+
+		// Create inner views from the side card children.
+		for ( const child of modelElement.getChildren() ) {
+			const childView = writer.createEditableElement( 'div' );
+
+			// Child is either a "title" or "section".
+			if ( child.is( 'element', 'sideCardTitle' ) ) {
+				writer.addClass( 'side-card-title', childView );
+			} else {
+				writer.addClass( 'side-card-section', childView );
+			}
+
+			// It is important to consume and bind converted elements.
+			consumable.consume( child, 'insert' );
+			mapper.bindElements( child, childView );
+
+			// Make it an editable part of the widget.
+			if ( asWidget ) {
+				toWidgetEditable( childView, writer );
+			}
+
+			writer.insert( writer.createPositionAt( sideCardView, 'end' ), childView );
+		}
+
+		const urlAttribute = modelElement.getAttribute( 'cardURL' );
+
+		// Do not render an empty URL field.
+		if ( urlAttribute ) {
+			const urlBox = writer.createRawElement( 'div', {
+				class: 'side-card-url'
+			}, function( domElement ) {
+				domElement.innerText = `URL: "${ urlAttribute }"`;
+			} );
+
+			writer.insert( writer.createPositionAt( sideCardView, 'end' ), urlBox );
+		}
+
+		// Inner element used to render a simple UI that allows to change the side card's attributes.
+		// It will only be needed in the editing view inside the widgetized element.
+		// The data output should not contain this section.
+		if ( asWidget ) {
+			const actionsView = writer.createRawElement( 'div', {
+				class: 'side-card-actions',
+				contenteditable: 'false', 			// Prevents editing of the element.
+				'data-cke-ignore-events': 'true'	// Allows using custom UI elements inside the editing view.
+			}, createActionsView( editor, modelElement ) ); // See the full code for details.
+
+			writer.insert( writer.createPositionAt( sideCardView, 'end' ), actionsView );
+
+			toWidget( sideCardView, writer, { widgetLabel: 'Side card', hasSelectionHandle: true } );
+		}
+
+		return sideCardView;
+	};
+};
+
+class InsertCardCommand extends Command {
+	/**
+	 * Refresh used schema definition to check if a side card can be inserted in the current selection.
+	 */
+	refresh() {
+		const model = this.editor.model;
+		const validParent = findOptimalInsertionPosition( model.document.selection, model );
+
+		this.isEnabled = model.schema.checkChild( validParent, 'sideCard' );
+	}
+
+	/**
+	 * Creates a full side card element with all required children and attributes.
+	 */
+	execute() {
+		const model = this.editor.model;
+		const selection = model.document.selection;
+
+		const insertPosition = findOptimalInsertionPosition( selection, model );
+
+		model.change( writer => {
+			const sideCard = writer.createElement( 'sideCard', { cardType: 'default' } );
+			const title = writer.createElement( 'sideCardTitle' );
+			const section = writer.createElement( 'sideCardSection' );
+			const paragraph = writer.createElement( 'paragraph' );
+
+			writer.insert( title, sideCard, 0 );
+			writer.insert( section, sideCard, 1 );
+			writer.insert( paragraph, section, 0 );
+
+			model.insertContent( sideCard, insertPosition );
+
+			writer.setSelection( writer.createPositionAt( title, 0 ) );
+		} );
+	}
+}
+
+class ComplexBox extends Plugin {
+	constructor( editor ) {
+		super( editor );
+
+		this._defineSchema();
+		this._defineConversion();
+
+		editor.commands.add( 'insertCard', new InsertCardCommand( editor ) );
+
+		this._defineUI();
+	}
+
+	_defineConversion() {
+		const editor = this.editor;
+		const conversion = editor.conversion;
+
+		conversion.for( 'upcast' )
+			.elementToElement( {
+				view: { name: 'aside', classes: [ 'side-card' ] },
+				model: upcastCard
+			} )
+			.elementToElement( {
+				view: { name: 'div', classes: [ 'side-card-title' ] },
+				model: 'sideCardTitle'
+			} )
+			.elementToElement( {
+				view: { name: 'div', classes: [ 'side-card-section' ] },
+				model: 'sideCardSection'
+			} );
+
+		// The downcast conversion must be split as you need a widget in the editing pipeline.
+		conversion.for( 'editingDowncast' ).elementToElement( {
+			model: 'sideCard',
+			view: downcastSideCard( editor, { asWidget: true } ),
+			triggerBy: {
+				attributes: [ 'cardType', 'cardURL' ],
+				children: [ 'sideCardSection' ]
+			}
+		} );
+		// The data downcast is always executed from the current model stat, so `triggerBy` will take no effect.
+		conversion.for( 'dataDowncast' ).elementToElement( {
+			model: 'sideCard',
+			view: downcastSideCard( editor, { asWidget: false } )
+		} );
+	}
+
+	_defineSchema() {
+		const schema = this.editor.model.schema;
+
+		// The main element with attributes for type and URL:
+		schema.register( 'sideCard', {
+			allowWhere: '$block',
+			isObject: true,
+			allowAttributes: [ 'cardType', 'cardURL' ]
+		} );
+		// Disallow side card nesting.
+		schema.addChildCheck( ( context, childDefinition ) => {
+			if ( [ ...context.getNames() ].includes( 'sideCard' ) && childDefinition.name === 'sideCard' ) {
+				return false;
+			}
+		} );
+
+		// A text-only title.
+		schema.register( 'sideCardTitle', {
+			isLimit: true,
+			allowIn: 'sideCard'
+		} );
+		// Allow text in title...
+		schema.extend( '$text', { allowIn: 'sideCardTitle' } );
+		// ...but disallow any text attribute inside.
+		schema.addAttributeCheck( context => {
+			if ( context.endsWith( 'sideCardTitle $text' ) ) {
+				return false;
+			}
+		} );
+
+		// A content block which can have any content allowed in $root.
+		schema.register( 'sideCardSection', {
+			isLimit: true,
+			allowIn: 'sideCard',
+			allowContentOf: '$root'
+		} );
+	}
+
+	_defineUI() {
+		const editor = this.editor;
+
+		// Defines a simple text button.
+		editor.ui.componentFactory.add( 'insertCard', locale => {
+			const button = new ButtonView( locale );
+
+			const command = editor.commands.get( 'insertCard' );
+
+			button.set( {
+				withText: true,
+				icon: false,
+				label: 'Insert card'
+			} );
+
+			button.bind( 'isEnabled' ).to( command );
+
+			button.on( 'execute', () => {
+				editor.execute( 'insertCard' );
+				editor.editing.view.focus();
+			} );
+
+			return button;
+		} );
+	}
+}
+
+ClassicEditor
+	.create( document.querySelector( '#editor-element-reconversion' ), {
+		cloudServices: CS_CONFIG,
+		extraPlugins: [ ComplexBox ],
+		image: {
+			toolbar: [ 'imageStyle:full', 'imageStyle:side', '|', 'imageTextAlternative' ]
+		},
+		table: {
+			contentToolbar: [
+				'tableColumn',
+				'tableRow',
+				'mergeTableCells'
+			]
+		},
+		toolbar: {
+			items: [ 'heading', '|', 'bold', 'italic', '|', 'insertCard' ],
+			viewportTopOffset: window.getViewportTopOffsetConfig()
+		}
+	} )
+	.then( editor => {
+		window.editor = editor;
+	} )
+	.catch( err => {
+		console.error( err );
+	} );

+ 9 - 9
packages/ckeditor5-engine/docs/_snippets/framework/extending-content-custom-element-converter.html

@@ -1,44 +1,44 @@
 <style>
-	.info-box {
+	.ck-content .info-box {
 		background: hsl(0, 0%, 45%);
 		border: 1px solid hsl(0, 0%, 80%);
 		padding: 1em;
 		border-radius: 8px;
 	}
 
-	.info-box-title {
+	.ck-content .info-box-title {
 		margin-bottom: 0.5em;
 		font-size: 1.4em;
 		font-weight: bold;
 		color: inherit;
 	}
 
-	.info-box-warning {
+	.ck-content .info-box-warning {
 		background: hsl(48, 100%, 87%);
 		border: 1px solid hsl(47, 100%, 62%);
 	}
 
-	.info-box-warning .info-box-title  {
+	.ck-content .info-box-warning .info-box-title  {
 		color: hsl(47, 100%, 32%)
 	}
 
-	.info-box-info {
+	.ck-content .info-box-info {
 		background: hsl(205, 100%, 95%);
 		border: 1px solid hsl(205, 91%, 82%);
 	}
 
-	.info-box-info .info-box-title  {
+	.ck-content .info-box-info .info-box-title  {
 		color: hsl(204, 79%, 41%)
 	}
 
-	.info-box-content {
+	.ck-content .info-box-content {
 		padding: 8px 10px;
 		background: hsl(0, 0%, 100%);
 		border-radius: 6px;
 	}
 
-	.info-box-content p:only-child,
-	.info-box-content p:last-child {
+	.ck-content .info-box-content p:only-child,
+	.ck-content .info-box-content p:last-child {
 		margin-bottom: 0;
 	}
 </style>

+ 626 - 0
packages/ckeditor5-engine/docs/framework/guides/deep-dive/element-reconversion.md

@@ -0,0 +1,626 @@
+---
+category: framework-deep-dive-conversion
+order: 50
+since: 24.0.0
+---
+
+# Element reconversion
+
+{@snippet framework/build-element-reconversion-source}
+
+<info-box warning>
+	Element reconversion is currently in beta version. The API will be extended to support more cases and will be changing with time.
+</info-box>
+
+This guide introduces the concept of the _reconversion of model elements_ during the downcast (model-to-view) {@link framework/guides/architecture/editing-engine#conversion conversion}.
+
+Reconversion allows simplifying downcast converters for model elements by merging multiple separate converters into a single converter that reacts to more types of model changes.
+
+## Prerequisites
+
+To better understand the concepts used in this guide, it is recommended to familiarize yourself with other conversion guides, too:
+
+* {@link framework/guides/tutorials/implementing-a-block-widget Implementing a block widget}
+* {@link framework/guides/deep-dive/custom-element-conversion Custom element conversion}
+
+## Atomic converters vs element reconversion
+
+In order to convert a model element to its view representation, you often write the following converters:
+
+* An `elementToElement()` converter. This converter reacts to the insertion of a model element specified in the `model` field.
+* If the model element has attributes and these attributes may change with time, you need to add the `attributeToAttribute()` converters for each attribute. These converters react to changes in the model element attributes and update the view accordingly.
+
+This granular approach to conversion is used by many editor features as it ensures extensibility of the base features and provides a separation of concerns. For example, the {@link features/image#base-image-support base image feature} provides conversion for a simple `<image src="...">` model element, while the {@link features/image#resizing-images image resize feature} adds support for the `width` and `height` attributes, the {@link features/image#image-captions image caption feature} for the `<figcaption>` HTML element, and so on.
+
+Apart from the extensibility aspect, the above approach ensures that a change of a model attribute or structure requires minimal changes in the view.
+
+However, in some cases where granularity is not necessary this approach may be an overkill. Consider a case in which you need to create a multi-layer view structure for one model element, or a case in which the view structure depends on a value of a model attribute. In such cases, writing a separate converter for a model element and separate converters for each attribute becomes cumbersome.
+
+Thankfully, element reconversion allows merging these converters into a single converter that reacts to multiple types of model changes (element insertion, its attribute changes and changes in its direct children). This approach can be considered more "functional" as the `view` callback executed on any of these changes should produce the entire view structure (down to a certain level) without taking into account what state changes have just happened.
+
+An additional perk of using element reconversion is that the parts of the model tree that have not been changed, like paragraphs and text inside your feature element, will not be reconverted. In other words, their view elements are kept in memory and re-used inside the changed parent.
+
+To sum up, element reconversion comes in handy for cases where you need to convert a relatively simple model to a complex view structure. And also, writing a single functional converter is easier to grasp in your project.
+
+## Enabling element reconversion
+
+Element reconversion is enabled by setting the reconversion trigger configuration (`triggerBy`) for the {@link module:engine/conversion/downcasthelpers~DowncastHelpers#elementToElement `elementToElement()`} downcast helper.
+
+The model element can be reconverted when:
+
+* one or many attributes change (using `triggerBy.attributes`) or
+* a child is inserted or removed (using `triggerBy.children`)
+
+<info-box>
+	Note that when using the `children` configuration option, the current implementation assumes that the downcast converter will either:
+	* handle an element and its children conversion at once,
+	* have a "flat" structure.
+</info-box>
+
+A simple example of an element reconversion configuration is demonstrated below:
+
+```js
+editor.conversion.for( 'downcast' ).elementToElement( {
+	model: 'myElement',
+	view: ( modelElement, { writer } ) => {
+		return writer.createContainerElement( 'div', {
+			'data-owner-id': modelElement.getAttribute( 'ownerId' ),
+			class: `my-element my-element-${ modelElement.getAttribute( 'type' ) }`
+		} );
+	},
+	triggerBy: {
+		attributes: [ 'ownerId', 'type' ]
+	}
+} )
+```
+
+In this example:
+
+* The downcast converter for `myElement` creates a `<div>` with a `data-owner-id` attribute and a set of CSS classes.
+* The value of `data-owner-id` is set from the `ownerId` model element's attribute.
+* The second CSS class is constructed off the `type` model element's attribute.
+* The `triggerBy.attributes` configuration defines that the element will be converted upon changes of the `onwerId` or `type` attributes.
+
+Before CKEditor version `23.1.0` you would have to define a set of atomic converters for the element and for each attribute:
+
+```js
+editor.conversion.for( 'downcast' )
+		.elementToElement( {
+			model: 'myElement',
+			view: 'div'
+		} )
+		.attributeToAttribute( {
+			model: 'owner-id',
+			view: 'data-owner-id'
+		} )
+		.attributeToAttribute( {
+			model: 'type',
+			view: modelAttributeValue => ( {
+				key: 'class',
+				value: `my-element my-element-${ modelAttributeValue }`
+			} )
+		} );
+```
+
+## Example implementation
+
+In this example implementation you will implement a "card" box which is displayed beside the main article content. The card will contain a text-only title, one to four content sections and an optional URL. Additionally, the user can choose the type of the card.
+
+### Demo
+
+{@snippet framework/element-reconversion-demo}
+
+### Model and view structure
+
+A simplified model markup for the side card looks as follows:
+
+```html
+<sideCardSection cardType="info" cardURL="https://ckeditor.com/">
+	<sideCardTitle>The title</sideCardTitle>
+	<sideCardSection>
+		<paragraph>The content</paragraph>
+	</sideCardSection>
+</sideCard>
+```
+
+This will be converted to the below view structure:
+
+```html
+<aside class="side-card side-card-default">
+	<div class="side-card-title">Hey! Did you know?</div>
+	<div class="side-card-section">
+		<p>Editable content of the <strong>side card</strong>.</p>
+	</div>
+	<div class="side-card-section">
+		<p>Another content box.</p>
+	</div>
+	<div class="side-card-actions">
+		<!-- Simple form elements for the editing view. -->
+	</div>
+</aside>
+```
+
+In the above example you can observe that the `'cardURL'` model attribute is converted as a view element inside the main view container while the type attribute is translated to a CSS class. Additionally, the UI controls are injected to the view after all other child views of the main container. Describing it using atomic converters would introduce a convoluted complexity.
+
+### Schema
+
+The side card model structure is represented in the editor's {@link framework/guides/deep-dive/schema schema} as follows:
+
+```js
+// The main element with attributes for type and URL:
+schema.register( 'sideCard', {
+	allowWhere: '$block',
+	isObject: true,
+	allowAttributes: [ 'cardType', 'cardURL' ]
+} );
+// Disallow side card nesting.
+schema.addChildCheck( ( context, childDefinition ) => {
+	if ( [ ...context.getNames() ].includes( 'sideCard' ) && childDefinition.name === 'sideCard' ) {
+		return false;
+	}
+} );
+
+// A text-only title.
+schema.register( 'sideCardTitle', {
+	isLimit: true,
+	allowIn: 'sideCard'
+} );
+// Allow text in title...
+schema.extend( '$text', { allowIn: 'sideCardTitle' } );
+// ...but disallow any text attribute inside.
+schema.addAttributeCheck( context => {
+	if ( context.endsWith( 'sideCardTitle $text' ) ) {
+		return false;
+	}
+} );
+
+// A content block which can have any content allowed in $root.
+schema.register( 'sideCardSection', {
+	isLimit: true,
+	allowIn: 'sideCard',
+	allowContentOf: '$root'
+} );
+```
+
+### Reconversion definition
+
+To enable element reconversion, define for which attribute and children modifications the main element will be converted:
+
+```js
+conversion.for( 'editingDowncast' ).elementToElement( {
+	model: 'sideCard',
+	view: downcastSideCard( editor, { asWidget: true } ),
+	triggerBy: {
+		attributes: [ 'cardType', 'cardURL' ],
+		children: [ 'sideCardSection' ]
+	}
+} );
+```
+
+The above definition will use the `downcastSideCard()` function to re-create the view when:
+
+* The `sideCard` element is inserted into the model.
+* One of `cardType` or `cardURL` has changed.
+* A child `sideCardSection` is added or removed from the parent `sideCard`.
+
+### Downcast converter details
+
+The function that creates a complete view for the model element:
+
+```js
+const downcastSideCard = ( editor, { asWidget } ) => {
+	return ( modelElement, { writer, consumable, mapper } ) => {
+		const type = modelElement.getAttribute( 'cardType' ) || 'default';
+
+		// The main view element for the side card.
+		const sideCardView = writer.createContainerElement( 'aside', {
+			class: `side-card side-card-${ type }`
+		} );
+
+		// Create inner views from the side card children.
+		for ( const child of modelElement.getChildren() ) {
+			const childView = writer.createEditableElement( 'div' );
+
+			// Child is either a "title" or "section".
+			if ( child.is( 'element', 'sideCardTitle' ) ) {
+				writer.addClass( 'side-card-title', childView );
+			} else {
+				writer.addClass( 'side-card-section', childView );
+			}
+
+			// It is important to consume and bind converted elements.
+			consumable.consume( child, 'insert' );
+			mapper.bindElements( child, childView );
+
+			// Make it an editable part of the widget.
+			if ( asWidget ) {
+				toWidgetEditable( childView, writer );
+			}
+
+			writer.insert( writer.createPositionAt( sideCardView, 'end' ), childView );
+		}
+
+		const urlAttribute = modelElement.getAttribute( 'cardURL' );
+
+		// Do not render an empty URL field
+		if ( urlAttribute ) {
+			const urlBox = writer.createRawElement( 'div', {
+				class: 'side-card-url'
+			}, function( domElement ) {
+				domElement.innerText = `URL: "${ urlAttribute }"`;
+			} );
+
+			writer.insert( writer.createPositionAt( sideCardView, 'end' ), urlBox );
+		}
+
+		// Inner element used to render a simple UI that allows to change the side card's attributes.
+		// It will only be needed in the editing view inside the widgetized element.
+		// The data output should not contain this section.
+		if ( asWidget ) {
+			const actionsView = writer.createRawElement( 'div', {
+				class: 'side-card-actions',
+				contenteditable: 'false', 			// Prevents editing of the element.
+				'data-cke-ignore-events': 'true'	// Allows using custom UI elements inside the editing view.
+			}, createActionsView( editor, modelElement ) ); // See the full code for details.
+
+			writer.insert( writer.createPositionAt( sideCardView, 'end' ), actionsView );
+
+			toWidget( sideCardView, writer, { widgetLabel: 'Side card', hasSelectionHandle: true  } );
+		}
+
+		return sideCardView;
+	};
+};
+```
+
+By using `mapper.bindElements( child, childView )` for `<sideCardTitle>` and `<sideCardSection>` you define which view elements correspond to which model elements. This allows the editor's conversion to re-use the existing view elements for the title and section children, so they will not be re-converted without a need.
+
+### Upcast conversion
+
+The upcast conversion uses standard element-to-element converters for the box and title, and a custom converter for the side card to extract metadata from the data.
+
+```js
+editor.conversion.for( 'upcast' )
+	.elementToElement( {
+		view: { name: 'aside', classes: [ 'side-card' ] },
+		model: upcastCard // Details in the full source code.
+	} )
+	.elementToElement( {
+		view: { name: 'div', classes: [ 'side-card-title' ] },
+		model: 'sideCardTitle'
+	} )
+	.elementToElement( {
+		view: { name: 'div', classes: [ 'side-card-section' ] },
+		model: 'sideCardSection'
+	} );
+```
+
+You can see the details of the upcast converter function (`upcastCard()`) in the full source code at the end of this guide.
+
+### Full source code
+
+```js
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
+import Command from '@ckeditor/ckeditor5-core/src/command';
+import { toWidget, toWidgetEditable, findOptimalInsertionPosition } from '@ckeditor/ckeditor5-widget/src/utils';
+import createElement from '@ckeditor/ckeditor5-utils/src/dom/createelement';
+
+/**
+ * Helper for extracting the side card type from a view element based on its CSS class.
+ */
+const getTypeFromViewElement = viewElement => {
+	for ( const type of [ 'default', 'alternate' ] ) {
+		if ( viewElement.hasClass( `side-card-${ type }` ) ) {
+			return type;
+		}
+	}
+
+	return 'default';
+};
+
+/**
+ * Single upcast converter to the <sideCard/> element with all its attributes.
+ */
+const upcastCard = ( viewElement, { writer } ) => {
+	const sideCard = writer.createElement( 'sideCard' );
+
+	const type = getTypeFromViewElement( viewElement );
+	writer.setAttribute( 'cardType', type, sideCard );
+
+	const urlWrapper = [ ...viewElement.getChildren() ].find( child => {
+		return child.is( 'element', 'div' ) && child.hasClass( 'side-card-url' );
+	} );
+
+	if ( urlWrapper ) {
+		writer.setAttribute( 'cardURL', urlWrapper.getChild( 0 ).data, sideCard );
+	}
+
+	return sideCard;
+};
+
+/**
+ * Helper for creating a DOM button with an editor callback.
+ */
+const addActionButton = ( text, callback, domElement, editor ) => {
+	const domDocument = domElement.ownerDocument;
+
+	const button = createElement( domDocument, 'button', {}, [ text ] );
+
+	button.addEventListener( 'click', () => {
+		editor.model.change( callback );
+	} );
+
+	domElement.appendChild( button );
+
+	return button;
+};
+
+/**
+ * Helper function that creates the card editing UI inside the card.
+ */
+const createActionsView = ( editor, modelElement ) => function( domElement ) {
+	//
+	// Set the URL action button.
+	//
+	addActionButton( 'Set URL', writer => {
+		// eslint-disable-next-line no-alert
+		const newURL = prompt( 'Set URL', modelElement.getAttribute( 'cardURL' ) || '' );
+
+		writer.setAttribute( 'cardURL', newURL, modelElement );
+	}, domElement, editor );
+
+	const currentType = modelElement.getAttribute( 'cardType' );
+	const newType = currentType === 'default' ? 'alternate' : 'default';
+
+	//
+	// Change the card action button.
+	//
+	addActionButton( 'Change type', writer => {
+		writer.setAttribute( 'cardType', newType, modelElement );
+	}, domElement, editor );
+
+	const childCount = modelElement.childCount;
+
+	//
+	// Add the content section to the card action button.
+	//
+	const addButton = addActionButton( 'Add section', writer => {
+		writer.insertElement( 'sideCardSection', modelElement, 'end' );
+	}, domElement, editor );
+
+	// Disable the button so only 1-3 content boxes are in the card (there will always be a title).
+	if ( childCount > 4 ) {
+		addButton.setAttribute( 'disabled', 'disabled' );
+	}
+
+	//
+	// Remove the content section from the card action button.
+	//
+	const removeButton = addActionButton( 'Remove section', writer => {
+		writer.remove( modelElement.getChild( childCount - 1 ) );
+	}, domElement, editor );
+
+	// Disable the button so only 1-3 content boxes are in the card (there will always be a title).
+	if ( childCount < 3 ) {
+		removeButton.setAttribute( 'disabled', 'disabled' );
+	}
+};
+
+/**
+ * The downcast converter for the <sideCard/> element.
+ *
+ * It returns the full view structure based on the current state of the model element.
+ */
+const downcastSideCard = ( editor, { asWidget } ) => {
+	return ( modelElement, { writer, consumable, mapper } ) => {
+		const type = modelElement.getAttribute( 'cardType' ) || 'default';
+
+		// The main view element for the side card.
+		const sideCardView = writer.createContainerElement( 'aside', {
+			class: `side-card side-card-${ type }`
+		} );
+
+		// Create inner views from the side card children.
+		for ( const child of modelElement.getChildren() ) {
+			const childView = writer.createEditableElement( 'div' );
+
+			// Child is either a "title" or "section".
+			if ( child.is( 'element', 'sideCardTitle' ) ) {
+				writer.addClass( 'side-card-title', childView );
+			} else {
+				writer.addClass( 'side-card-section', childView );
+			}
+
+			// It is important to consume and bind converted elements.
+			consumable.consume( child, 'insert' );
+			mapper.bindElements( child, childView );
+
+			// Make it an editable part of the widget.
+			if ( asWidget ) {
+				toWidgetEditable( childView, writer );
+			}
+
+			writer.insert( writer.createPositionAt( sideCardView, 'end' ), childView );
+		}
+
+		const urlAttribute = modelElement.getAttribute( 'cardURL' );
+
+		// Do not render an empty URL field.
+		if ( urlAttribute ) {
+			const urlBox = writer.createRawElement( 'div', {
+				class: 'side-card-url'
+			}, function( domElement ) {
+				domElement.innerText = `URL: "${ urlAttribute }"`;
+			} );
+
+			writer.insert( writer.createPositionAt( sideCardView, 'end' ), urlBox );
+		}
+
+		// Inner element used to render a simple UI that allows to change the side card's attributes.
+		// It will only be needed in the editing view inside the widgetized element.
+		// The data output should not contain this section.
+		if ( asWidget ) {
+			const actionsView = writer.createRawElement( 'div', {
+				class: 'side-card-actions',
+				contenteditable: 'false', 			// Prevents editing of the element.
+				'data-cke-ignore-events': 'true'	// Allows using custom UI elements inside the editing view.
+			}, createActionsView( editor, modelElement ) ); // See the full code for details.
+
+			writer.insert( writer.createPositionAt( sideCardView, 'end' ), actionsView );
+
+			toWidget( sideCardView, writer, { widgetLabel: 'Side card' } );
+		}
+
+		return sideCardView;
+	};
+};
+
+class InsertCardCommand extends Command {
+	/**
+	 * Refresh used schema definition to check if a side card can be inserted in the current selection.
+	 */
+	refresh() {
+		const model = this.editor.model;
+		const validParent = findOptimalInsertionPosition( model.document.selection, model );
+
+		this.isEnabled = model.schema.checkChild( validParent, 'sideCard' );
+	}
+
+	/**
+	 * Creates a full side card element with all required children and attributes.
+	 */
+	execute() {
+		const model = this.editor.model;
+		const selection = model.document.selection;
+
+		const insertPosition = findOptimalInsertionPosition( selection, model );
+
+		model.change( writer => {
+			const sideCard = writer.createElement( 'sideCard', { cardType: 'default' } );
+			const title = writer.createElement( 'sideCardTitle' );
+			const section = writer.createElement( 'sideCardSection' );
+			const paragraph = writer.createElement( 'paragraph' );
+
+			writer.insert( title, sideCard, 0 );
+			writer.insert( section, sideCard, 1 );
+			writer.insert( paragraph, section, 0 );
+
+			model.insertContent( sideCard, insertPosition );
+
+			writer.setSelection( writer.createPositionAt( title, 0 ) );
+		} );
+	}
+}
+
+class ComplexBox extends Plugin {
+	constructor( editor ) {
+		super( editor );
+
+		this._defineSchema();
+		this._defineConversion();
+
+		editor.commands.add( 'insertCard', new InsertCardCommand( editor ) );
+
+		this._defineUI();
+	}
+
+	_defineConversion() {
+		const editor = this.editor;
+		const conversion = editor.conversion;
+
+		conversion.for( 'upcast' )
+			.elementToElement( {
+				view: { name: 'aside', classes: [ 'side-card' ] },
+				model: upcastCard
+			} )
+			.elementToElement( {
+				view: { name: 'div', classes: [ 'side-card-title' ] },
+				model: 'sideCardTitle'
+			} )
+			.elementToElement( {
+				view: { name: 'div', classes: [ 'side-card-section' ] },
+				model: 'sideCardSection'
+			} );
+
+		// The downcast conversion must be split as you need a widget in the editing pipeline.
+		conversion.for( 'editingDowncast' ).elementToElement( {
+			model: 'sideCard',
+			view: downcastSideCard( editor, { asWidget: true } ),
+			triggerBy: {
+				attributes: [ 'cardType', 'cardURL' ],
+				children: [ 'sideCardSection' ]
+			}
+		} );
+		// The data downcast is always executed from the current model stat, so `triggerBy` will take no effect.
+		conversion.for( 'dataDowncast' ).elementToElement( {
+			model: 'sideCard',
+			view: downcastSideCard( editor, { asWidget: false } )
+		} );
+	}
+
+	_defineSchema() {
+		const schema = this.editor.model.schema;
+
+		// The main element with attributes for type and URL:
+		schema.register( 'sideCard', {
+			allowWhere: '$block',
+			isObject: true,
+			allowAttributes: [ 'cardType', 'cardURL' ]
+		} );
+		// Disallow side card nesting.
+		schema.addChildCheck( ( context, childDefinition ) => {
+			if ( [ ...context.getNames() ].includes( 'sideCard' ) && childDefinition.name === 'sideCard' ) {
+				return false;
+			}
+		} );
+
+		// A text-only title.
+		schema.register( 'sideCardTitle', {
+			isLimit: true,
+			allowIn: 'sideCard'
+		} );
+		// Allow text in title...
+		schema.extend( '$text', { allowIn: 'sideCardTitle' } );
+		// ...but disallow any text attribute inside.
+		schema.addAttributeCheck( context => {
+			if ( context.endsWith( 'sideCardTitle $text' ) ) {
+				return false;
+			}
+		} );
+
+		// A content block which can have any content allowed in $root.
+		schema.register( 'sideCardSection', {
+			isLimit: true,
+			allowIn: 'sideCard',
+			allowContentOf: '$root'
+		} );
+	}
+
+	_defineUI() {
+		const editor = this.editor;
+
+		// Defines a simple text button.
+		editor.ui.componentFactory.add( 'insertCard', locale => {
+			const button = new ButtonView( locale );
+
+			const command = editor.commands.get( 'insertCard' );
+
+			button.set( {
+				withText: true,
+				icon: false,
+				label: 'Insert card'
+			} );
+
+			button.bind( 'isEnabled' ).to( command );
+
+			button.on( 'execute', () => {
+				editor.execute( 'insertCard' );
+				editor.editing.view.focus();
+			} );
+
+			return button;
+		} );
+	}
+}
+```

+ 2 - 0
packages/ckeditor5-image/docs/_snippets/features/build-image-source.js

@@ -9,10 +9,12 @@ import ClassicEditor from '@ckeditor/ckeditor5-build-classic/src/ckeditor';
 
 import ImageResize from '@ckeditor/ckeditor5-image/src/imageresize';
 import ImageInsert from '@ckeditor/ckeditor5-image/src/imageinsert';
+import AutoImage from '@ckeditor/ckeditor5-image/src/autoimage';
 import LinkImage from '@ckeditor/ckeditor5-link/src/linkimage';
 
 ClassicEditor.builtinPlugins.push( ImageResize );
 ClassicEditor.builtinPlugins.push( ImageInsert );
 ClassicEditor.builtinPlugins.push( LinkImage );
+ClassicEditor.builtinPlugins.push( AutoImage );
 
 window.ClassicEditor = ClassicEditor;

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

@@ -9,7 +9,7 @@ import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud
 
 ClassicEditor
 	.create( document.querySelector( '#snippet-image-caption' ), {
-		removePlugins: [ 'ImageStyle', 'ImageResize', 'LinkImage' ],
+		removePlugins: [ 'ImageStyle', 'ImageResize', 'LinkImage', 'AutoImage' ],
 		image: {
 			toolbar: [ 'imageTextAlternative' ]
 		},

+ 3 - 0
packages/ckeditor5-image/docs/_snippets/features/image-insert-via-pasting-url-into-editor.html

@@ -0,0 +1,3 @@
+<div id="snippet-image-insert-via-pasting-url-into-editor">
+	<p>Insert an URL of an image: </p>
+</div>

+ 20 - 0
packages/ckeditor5-image/docs/_snippets/features/image-insert-via-pasting-url-into-editor.js

@@ -0,0 +1,20 @@
+/**
+ * @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 ClassicEditor, console, window, document */
+
+import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config.js';
+
+ClassicEditor
+	.create( document.querySelector( '#snippet-image-insert-via-pasting-url-into-editor' ), {
+		removePlugins: [ 'ImageToolbar', 'ImageCaption', 'ImageStyle', 'ImageResize', 'LinkImage' ],
+		cloudServices: CS_CONFIG
+	} )
+	.then( editor => {
+		window.editorInsertImageViaPastingUrlIntoEditor = editor;
+	} )
+	.catch( err => {
+		console.error( err );
+	} );

+ 1 - 1
packages/ckeditor5-image/docs/_snippets/features/image-insert-via-url.js

@@ -13,7 +13,7 @@ toolbarItems.splice( toolbarItems.indexOf( 'imageUpload' ), 1, 'imageInsert' );
 
 ClassicEditor
 	.create( document.querySelector( '#snippet-image-insert-via-url' ), {
-		removePlugins: [ 'ImageToolbar', 'ImageCaption', 'ImageStyle', 'ImageResize', 'LinkImage' ],
+		removePlugins: [ 'ImageToolbar', 'ImageCaption', 'ImageStyle', 'ImageResize', 'LinkImage', 'AutoImage' ],
 		toolbar: {
 			items: toolbarItems,
 			viewportTopOffset: window.getViewportTopOffsetConfig()

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

@@ -9,7 +9,7 @@ import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud
 
 ClassicEditor
 	.create( document.querySelector( '#snippet-image-link' ), {
-		removePlugins: [ 'ImageResize' ],
+		removePlugins: [ 'ImageResize', 'AutoImage' ],
 		toolbar: {
 			viewportTopOffset: window.getViewportTopOffsetConfig()
 		},

+ 1 - 1
packages/ckeditor5-image/docs/_snippets/features/image-resize-buttons-dropdown.js

@@ -9,7 +9,7 @@ import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud
 
 ClassicEditor
 	.create( document.querySelector( '#snippet-image-resize-buttons-dropdown' ), {
-		removePlugins: [ 'LinkImage' ],
+		removePlugins: [ 'LinkImage', 'AutoImage' ],
 		toolbar: {
 			viewportTopOffset: window.getViewportTopOffsetConfig()
 		},

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

@@ -9,7 +9,7 @@ import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud
 
 ClassicEditor
 	.create( document.querySelector( '#snippet-image-resize-buttons' ), {
-		removePlugins: [ 'LinkImage' ],
+		removePlugins: [ 'LinkImage', 'AutoImage' ],
 		toolbar: {
 			viewportTopOffset: window.getViewportTopOffsetConfig()
 		},

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

@@ -9,7 +9,7 @@ import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud
 
 ClassicEditor
 	.create( document.querySelector( '#snippet-image-resize-px' ), {
-		removePlugins: [ 'LinkImage' ],
+		removePlugins: [ 'LinkImage', 'AutoImage' ],
 		image: {
 			resizeUnit: 'px',
 			resizeOptions: [

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

@@ -9,7 +9,7 @@ import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud
 
 ClassicEditor
 	.create( document.querySelector( '#snippet-image-resize' ), {
-		removePlugins: [ 'LinkImage' ],
+		removePlugins: [ 'LinkImage', 'AutoImage' ],
 		toolbar: {
 			viewportTopOffset: window.getViewportTopOffsetConfig()
 		},

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

@@ -9,7 +9,7 @@ import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud
 
 ClassicEditor
 	.create( document.querySelector( '#snippet-image-style-custom' ), {
-		removePlugins: [ 'LinkImage' ],
+		removePlugins: [ 'LinkImage', 'AutoImage' ],
 		image: {
 			styles: [
 				'alignLeft',

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

@@ -9,7 +9,7 @@ import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud
 
 ClassicEditor
 	.create( document.querySelector( '#snippet-image-style' ), {
-		removePlugins: [ 'ImageResize', 'LinkImage' ],
+		removePlugins: [ 'ImageResize', 'LinkImage', 'AutoImage' ],
 		toolbar: {
 			viewportTopOffset: window.getViewportTopOffsetConfig()
 		},

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

@@ -9,7 +9,7 @@ import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud
 
 ClassicEditor
 	.create( document.querySelector( '#snippet-image-toolbar' ), {
-		removePlugins: [ 'ImageCaption', 'ImageStyle', 'ImageResize', 'LinkImage' ],
+		removePlugins: [ 'ImageCaption', 'ImageStyle', 'ImageResize', 'LinkImage', 'AutoImage' ],
 		image: {
 			toolbar: [ 'imageTextAlternative' ]
 		},

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

@@ -9,7 +9,7 @@ import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud
 
 ClassicEditor
 	.create( document.querySelector( '#snippet-image' ), {
-		removePlugins: [ 'ImageToolbar', 'ImageCaption', 'ImageStyle', 'ImageResize', 'LinkImage' ],
+		removePlugins: [ 'ImageToolbar', 'ImageCaption', 'ImageStyle', 'ImageResize', 'LinkImage', 'AutoImage' ],
 		toolbar: {
 			viewportTopOffset: window.getViewportTopOffsetConfig()
 		},

+ 1 - 0
packages/ckeditor5-image/docs/api/image.md

@@ -22,6 +22,7 @@ See the {@link features/image Image feature} guide and the documentation of the
 * {@link module:image/imagetextalternative~ImageTextAlternative}
 * {@link module:image/imagetoolbar~ImageToolbar}
 * {@link module:image/imageupload~ImageUpload}
+* {@link module:image/autoimage~AutoImage}
 
 ## Installation
 

+ 17 - 0
packages/ckeditor5-image/docs/features/image.md

@@ -14,6 +14,7 @@ The [`@ckeditor/ckeditor5-image`](https://www.npmjs.com/package/@ckeditor/ckedit
 * {@link module:image/imagetextalternative~ImageTextAlternative} adds support for adding text alternative.
 * {@link module:image/imageupload~ImageUpload} adds support for {@link features/image-upload uploading dropped or pasted images}.
 * {@link module:image/imageinsert~ImageInsert} adds support for [inserting images via URL](#inserting-images-via-source-url) and other custom integrations.
+* {@link module:image/autoimage~AutoImage} adds support for [inserting images via pasting URL into editor](#inserting-images-via-pasting-url-into-editor).
 * {@link module:image/imageresize~ImageResize} adds support for [resizing images](#resizing-images).
 * {@link module:link/linkimage~LinkImage} adds support for [linking images](#linking-images).
 
@@ -105,6 +106,22 @@ This will add a new **Insert image** dropdown in the toolbar. To open the panel
 
 {@snippet features/image-insert-via-url}
 
+## Inserting images via pasting URL into editor
+
+The {@link module:image/autoimage~AutoImage} plugin recognizes image links in the pasted content and embeds them shortly after they are injected into the document to speed up the editing. Accepted image extensions: jpg, jpeg, png, gif, ico.
+
+<info-box>
+	The image URL must be the only content pasted to be properly embedded. Multiple links (`"http://image.url http://another.image.url"`) as well as bigger chunks of content (`"This link http://image.url will not be auto–embedded when pasted."`) are ignored.
+</info-box>
+
+If the automatic embedding was unexpected, for instance when the link was meant to remain in the content as text, simply undo the action (by clicking the "Undo" button in the toolbar or using the <kbd>Ctrl/⌘</kbd>+<kbd>Z</kbd> keystrokes).
+
+You can paste the image URL directly into the editor content, and it will be automatically embedded.
+
+<input class="example-input" type="text" value="https://ckeditor.com/docs/ckeditor5/latest/assets/img/malta.jpg">
+
+{@snippet features/image-insert-via-pasting-url-into-editor}
+
 ## Responsive images
 
 Support for responsive images in CKEditor 5 is brought by the {@link features/easy-image Easy Image} feature without any additional configuration. Refer to the {@link features/easy-image#responsive-images Easy Image integration} guide to learn how to use the feature in your project.

+ 174 - 0
packages/ckeditor5-image/src/autoimage.js

@@ -0,0 +1,174 @@
+/**
+ * @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 image/autoimage
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard';
+import LiveRange from '@ckeditor/ckeditor5-engine/src/model/liverange';
+import LivePosition from '@ckeditor/ckeditor5-engine/src/model/liveposition';
+import Undo from '@ckeditor/ckeditor5-undo/src/undo';
+import global from '@ckeditor/ckeditor5-utils/src/dom/global';
+import { insertImage } from './image/utils';
+
+// Implements the pattern: http(s)://(www.)example.com/path/to/resource.ext?query=params&maybe=too.
+const IMAGE_URL_REGEXP = new RegExp( String( /^(http(s)?:\/\/)?[\w-]+(\.[\w-]+)+[\w._~:/?#[\]@!$&'()*+,;=%-]+/.source +
+	/\.(jpg|jpeg|png|gif|ico|JPG|JPEG|PNG|GIF|ICO)\??[\w._~:/#[\]@!$&'()*+,;=%-]*$/.source ) );
+
+/**
+ * The auto-image plugin. It recognizes image links in the pasted content and embeds
+ * them shortly after they are injected into the document.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class AutoImage extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get requires() {
+		return [ Clipboard, Undo ];
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static get pluginName() {
+		return 'AutoImage';
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	constructor( editor ) {
+		super( editor );
+
+		/**
+		 * The paste–to–embed `setTimeout` ID. Stored as a property to allow
+		 * cleaning of the timeout.
+		 *
+		 * @private
+		 * @member {Number} #_timeoutId
+		 */
+		this._timeoutId = null;
+
+		/**
+		 * The position where the `<image>` element will be inserted after the timeout,
+		 * determined each time the new content is pasted into the document.
+		 *
+		 * @private
+		 * @member {module:engine/model/liveposition~LivePosition} #_positionToInsert
+		 */
+		this._positionToInsert = null;
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		const editor = this.editor;
+		const modelDocument = editor.model.document;
+
+		// We need to listen on `Clipboard#inputTransformation` because we need to save positions of selection.
+		// After pasting, the content between those positions will be checked for a URL that could be transformed
+		// into image.
+		this.listenTo( editor.plugins.get( Clipboard ), 'inputTransformation', () => {
+			const firstRange = modelDocument.selection.getFirstRange();
+
+			const leftLivePosition = LivePosition.fromPosition( firstRange.start );
+			leftLivePosition.stickiness = 'toPrevious';
+
+			const rightLivePosition = LivePosition.fromPosition( firstRange.end );
+			rightLivePosition.stickiness = 'toNext';
+
+			modelDocument.once( 'change:data', () => {
+				this._embedImageBetweenPositions( leftLivePosition, rightLivePosition );
+
+				leftLivePosition.detach();
+				rightLivePosition.detach();
+			}, { priority: 'high' } );
+		} );
+
+		editor.commands.get( 'undo' ).on( 'execute', () => {
+			if ( this._timeoutId ) {
+				global.window.clearTimeout( this._timeoutId );
+				this._positionToInsert.detach();
+
+				this._timeoutId = null;
+				this._positionToInsert = null;
+			}
+		}, { priority: 'high' } );
+	}
+
+	/**
+	 * Analyzes the part of the document between provided positions in search for an URL representing an image.
+	 * When the URL is found, it is automatically converted into an image.
+	 *
+	 * @protected
+	 * @param {module:engine/model/liveposition~LivePosition} leftPosition Left position of the selection.
+	 * @param {module:engine/model/liveposition~LivePosition} rightPosition Right position of the selection.
+	 */
+	_embedImageBetweenPositions( leftPosition, rightPosition ) {
+		const editor = this.editor;
+		// TODO: Use marker instead of LiveRange & LivePositions.
+		const urlRange = new LiveRange( leftPosition, rightPosition );
+		const walker = urlRange.getWalker( { ignoreElementEnd: true } );
+
+		let src = '';
+
+		for ( const node of walker ) {
+			if ( node.item.is( '$textProxy' ) ) {
+				src += node.item.data;
+			}
+		}
+
+		src = src.trim();
+
+		// If the URL does not match to image URL regexp, let's skip that.
+		if ( !src.match( IMAGE_URL_REGEXP ) ) {
+			urlRange.detach();
+
+			return;
+		}
+
+		// Position won't be available in the `setTimeout` function so let's clone it.
+		this._positionToInsert = LivePosition.fromPosition( leftPosition );
+
+		// This action mustn't be executed if undo was called between pasting and auto-embedding.
+		this._timeoutId = global.window.setTimeout( () => {
+			// Don't do anything if image element cannot be inserted at the current position.
+			// See https://github.com/ckeditor/ckeditor5/issues/2763.
+			// Condition must be checked after timeout - pasting may take place on an element, replacing it. The final position matters.
+			const imageCommand = editor.commands.get( 'imageInsert' );
+
+			if ( !imageCommand.isEnabled ) {
+				urlRange.detach();
+
+				return;
+			}
+
+			editor.model.change( writer => {
+				this._timeoutId = null;
+
+				writer.remove( urlRange );
+				urlRange.detach();
+
+				let insertionPosition;
+
+				// Check if position where the element should be inserted is still valid.
+				// Otherwise leave it as undefined to use the logic of insertImage().
+				if ( this._positionToInsert.root.rootName !== '$graveyard' ) {
+					insertionPosition = this._positionToInsert.toPosition();
+				}
+
+				insertImage( editor.model, { src }, insertionPosition );
+
+				this._positionToInsert.detach();
+				this._positionToInsert = null;
+			} );
+		}, 100 );
+	}
+}

+ 4 - 6
packages/ckeditor5-image/src/image/imageinsertcommand.js

@@ -50,12 +50,10 @@ export default class ImageInsertCommand extends Command {
 	execute( options ) {
 		const model = this.editor.model;
 
-		model.change( writer => {
-			const sources = Array.isArray( options.source ) ? options.source : [ options.source ];
+		const sources = Array.isArray( options.source ) ? options.source : [ options.source ];
 
-			for ( const src of sources ) {
-				insertImage( writer, model, { src } );
-			}
-		} );
+		for ( const src of sources ) {
+			insertImage( model, { src } );
+		}
 	}
 }

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

@@ -71,25 +71,26 @@ export function isImage( modelElement ) {
 /**
  * Handles inserting single file. This method unifies image insertion using {@link module:widget/utils~findOptimalInsertionPosition} method.
  *
- *		model.change( writer => {
- *			insertImage( writer, model, { src: 'path/to/image.jpg' } );
- *		} );
+ *		insertImage( model, { src: 'path/to/image.jpg' } );
  *
- * @param {module:engine/model/writer~Writer} writer
  * @param {module:engine/model/model~Model} model
  * @param {Object} [attributes={}] Attributes of inserted image
+ * @param {module:engine/model/position~Position} [insertPosition] Position to insert the image. If not specified,
+ * the {@link module:widget/utils~findOptimalInsertionPosition} logic will be applied.
  */
-export function insertImage( writer, model, attributes = {} ) {
-	const imageElement = writer.createElement( 'image', attributes );
+export function insertImage( model, attributes = {}, insertPosition = null ) {
+	model.change( writer => {
+		const imageElement = writer.createElement( 'image', attributes );
 
-	const insertAtSelection = findOptimalInsertionPosition( model.document.selection, model );
+		const insertAtSelection = insertPosition || findOptimalInsertionPosition( model.document.selection, model );
 
-	model.insertContent( imageElement, insertAtSelection );
+		model.insertContent( imageElement, insertAtSelection );
 
-	// Inserting an image might've failed due to schema regulations.
-	if ( imageElement.parent ) {
-		writer.setSelection( imageElement, 'on' );
-	}
+		// Inserting an image might've failed due to schema regulations.
+		if ( imageElement.parent ) {
+			writer.setSelection( imageElement, 'on' );
+		}
+	} );
 }
 
 /**

+ 1 - 2
packages/ckeditor5-image/src/imageinsert/utils.js

@@ -70,8 +70,7 @@ export function createLabeledInputView( locale ) {
 	labeledInputView.set( {
 		label: t( 'Insert image via URL' )
 	} );
-	labeledInputView.fieldView.placeholder = 'https://example.com/src/image.png';
-	labeledInputView.infoText = t( 'Paste the image source URL.' );
+	labeledInputView.fieldView.placeholder = 'https://example.com/image.png';
 
 	return labeledInputView;
 }

+ 38 - 12
packages/ckeditor5-image/src/imageresize/imageresizehandles.js

@@ -9,6 +9,7 @@
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import WidgetResize from '@ckeditor/ckeditor5-widget/src/widgetresize';
+import ImageLoadObserver from '../image/imageloadobserver';
 
 /**
  * The image resize by handles feature.
@@ -37,21 +38,46 @@ export default class ImageResizeHandles extends Plugin {
 	 * @inheritDoc
 	 */
 	init() {
+		const command = this.editor.commands.get( 'imageResize' );
+		this.bind( 'isEnabled' ).to( command );
+
+		this._setupResizerCreator();
+	}
+
+	/**
+	 * Attaches the listeners responsible for creating a resizer for each image.
+	 *
+	 * @private
+	 */
+	_setupResizerCreator() {
 		const editor = this.editor;
-		const command = editor.commands.get( 'imageResize' );
+		const editingView = editor.editing.view;
 
-		this.bind( 'isEnabled' ).to( command );
+		editingView.addObserver( ImageLoadObserver );
+
+		this.listenTo( editingView.document, 'imageLoaded', ( evt, domEvent ) => {
+			const imageView = editor.editing.view.domConverter.domToView( domEvent.target );
+			const widgetView = imageView.findAncestor( 'figure' );
+			let resizer = this.editor.plugins.get( WidgetResize ).getResizerByViewElement( widgetView );
+
+			if ( resizer ) {
+				// There are rare cases when image will be triggered multiple times for the same widget, e.g. when
+				// image's src was changed after upload (https://github.com/ckeditor/ckeditor5/pull/8108#issuecomment-708302992).
+				resizer.redraw();
+
+				return;
+			}
 
-		editor.editing.downcastDispatcher.on( 'insert:image', ( evt, data, conversionApi ) => {
-			const widget = conversionApi.mapper.toViewElement( data.item );
+			const mapper = editor.editing.mapper;
+			const imageModel = mapper.toModelElement( widgetView );
 
-			const resizer = editor.plugins
+			resizer = editor.plugins
 				.get( WidgetResize )
 				.attachTo( {
 					unit: editor.config.get( 'image.resizeUnit' ),
 
-					modelElement: data.item,
-					viewElement: widget,
+					modelElement: imageModel,
+					viewElement: widgetView,
 					editor,
 
 					getHandleHost( domWidgetElement ) {
@@ -62,7 +88,7 @@ export default class ImageResizeHandles extends Plugin {
 					},
 					// TODO consider other positions.
 					isCentered() {
-						const imageStyle = data.item.getAttribute( 'imageStyle' );
+						const imageStyle = imageModel.getAttribute( 'imageStyle' );
 
 						return !imageStyle || imageStyle == 'full' || imageStyle == 'alignCenter';
 					},
@@ -73,14 +99,14 @@ export default class ImageResizeHandles extends Plugin {
 				} );
 
 			resizer.on( 'updateSize', () => {
-				if ( !widget.hasClass( 'image_resized' ) ) {
-					editor.editing.view.change( writer => {
-						writer.addClass( 'image_resized', widget );
+				if ( !widgetView.hasClass( 'image_resized' ) ) {
+					editingView.change( writer => {
+						writer.addClass( 'image_resized', widgetView );
 					} );
 				}
 			} );
 
 			resizer.bind( 'isEnabled' ).to( this );
-		}, { priority: 'low' } );
+		} );
 	}
 }

+ 4 - 0
packages/ckeditor5-image/src/imagetextalternative/imagetextalternativeui.js

@@ -165,6 +165,8 @@ export default class ImageTextAlternativeUI extends Plugin {
 		const command = editor.commands.get( 'imageTextAlternative' );
 		const labeledInput = this._form.labeledInput;
 
+		this._form.disableCssTransitions();
+
 		if ( !this._isInBalloon ) {
 			this._balloon.add( {
 				view: this._form,
@@ -180,6 +182,8 @@ export default class ImageTextAlternativeUI extends Plugin {
 		labeledInput.fieldView.value = labeledInput.fieldView.element.value = command.value || '';
 
 		this._form.labeledInput.fieldView.select();
+
+		this._form.enableCssTransitions();
 	}
 
 	/**

+ 3 - 1
packages/ckeditor5-image/src/imagetextalternative/ui/textalternativeformview.js

@@ -14,6 +14,7 @@ import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
 
 import LabeledFieldView from '@ckeditor/ckeditor5-ui/src/labeledfield/labeledfieldview';
 import { createLabeledInputText } from '@ckeditor/ckeditor5-ui/src/labeledfield/utils';
+import injectCssTransitionDisabler from '@ckeditor/ckeditor5-ui/src/bindings/injectcsstransitiondisabler';
 
 import submitHandler from '@ckeditor/ckeditor5-ui/src/bindings/submithandler';
 import KeystrokeHandler from '@ckeditor/ckeditor5-utils/src/keystrokehandler';
@@ -126,6 +127,8 @@ export default class TextAlternativeFormView extends View {
 				this.cancelButtonView
 			]
 		} );
+
+		injectCssTransitionDisabler( this );
 	}
 
 	/**
@@ -191,7 +194,6 @@ export default class TextAlternativeFormView extends View {
 		const labeledInput = new LabeledFieldView( this.locale, createLabeledInputText );
 
 		labeledInput.label = t( 'Text alternative' );
-		labeledInput.fieldView.placeholder = t( 'Text alternative' );
 
 		return labeledInput;
 	}

+ 6 - 9
packages/ckeditor5-image/src/imageupload/imageuploadcommand.js

@@ -63,22 +63,19 @@ export default class ImageUploadCommand extends Command {
 
 		const fileRepository = editor.plugins.get( FileRepository );
 
-		model.change( writer => {
-			const filesToUpload = Array.isArray( options.file ) ? options.file : [ options.file ];
+		const filesToUpload = Array.isArray( options.file ) ? options.file : [ options.file ];
 
-			for ( const file of filesToUpload ) {
-				uploadImage( writer, model, fileRepository, file );
-			}
-		} );
+		for ( const file of filesToUpload ) {
+			uploadImage( model, fileRepository, file );
+		}
 	}
 }
 
 // Handles uploading single file.
 //
-// @param {module:engine/model/writer~writer} writer
 // @param {module:engine/model/model~Model} model
 // @param {File} file
-function uploadImage( writer, model, fileRepository, file ) {
+function uploadImage( model, fileRepository, file ) {
 	const loader = fileRepository.createLoader( file );
 
 	// Do not throw when upload adapter is not set. FileRepository will log an error anyway.
@@ -86,5 +83,5 @@ function uploadImage( writer, model, fileRepository, file ) {
 		return;
 	}
 
-	insertImage( writer, model, { uploadId: loader.id } );
+	insertImage( model, { uploadId: loader.id } );
 }

+ 555 - 0
packages/ckeditor5-image/tests/autoimage.js

@@ -0,0 +1,555 @@
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/* global setTimeout */
+
+import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard';
+import Link from '@ckeditor/ckeditor5-link/src/link';
+import Table from '@ckeditor/ckeditor5-table/src/table';
+import Typing from '@ckeditor/ckeditor5-typing/src/typing';
+import Undo from '@ckeditor/ckeditor5-undo/src/undo';
+import global from '@ckeditor/ckeditor5-utils/src/dom/global';
+import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+
+import Image from '../src/image';
+import ImageCaption from '../src/imagecaption';
+import AutoImage from '../src/autoimage';
+
+describe( 'AutoImage - integration', () => {
+	let editorElement, editor;
+
+	beforeEach( () => {
+		editorElement = global.document.createElement( 'div' );
+		global.document.body.appendChild( editorElement );
+
+		return ClassicTestEditor
+			.create( editorElement, {
+				plugins: [ Typing, Paragraph, Link, Image, ImageCaption, AutoImage ]
+			} )
+			.then( newEditor => {
+				editor = newEditor;
+			} );
+	} );
+
+	afterEach( () => {
+		editorElement.remove();
+
+		return editor.destroy();
+	} );
+
+	it( 'should load Clipboard plugin', () => {
+		expect( editor.plugins.get( Clipboard ) ).to.instanceOf( Clipboard );
+	} );
+
+	it( 'should load Undo plugin', () => {
+		expect( editor.plugins.get( Undo ) ).to.instanceOf( Undo );
+	} );
+
+	it( 'has proper name', () => {
+		expect( AutoImage.pluginName ).to.equal( 'AutoImage' );
+	} );
+
+	describe( 'use fake timers', () => {
+		let clock;
+
+		beforeEach( () => {
+			clock = sinon.useFakeTimers();
+		} );
+
+		afterEach( () => {
+			clock.restore();
+		} );
+
+		it( 'replaces pasted text with image element after 100ms', () => {
+			setData( editor.model, '<paragraph>[]</paragraph>' );
+			pasteHtml( editor, 'http://example.com/image.png' );
+
+			expect( getData( editor.model ) ).to.equal(
+				'<paragraph>http://example.com/image.png[]</paragraph>'
+			);
+
+			clock.tick( 100 );
+
+			expect( getData( editor.model ) ).to.equal(
+				'[<image src="http://example.com/image.png"><caption></caption></image>]'
+			);
+		} );
+
+		it( 'can undo auto-embeding', () => {
+			setData( editor.model, '<paragraph>[]</paragraph>' );
+			pasteHtml( editor, 'http://example.com/image.png' );
+
+			expect( getData( editor.model ) ).to.equal(
+				'<paragraph>http://example.com/image.png[]</paragraph>'
+			);
+
+			clock.tick( 100 );
+
+			editor.commands.execute( 'undo' );
+
+			expect( getData( editor.model ) ).to.equal(
+				'<paragraph>http://example.com/image.png[]</paragraph>'
+			);
+		} );
+
+		describe( 'supported URL', () => {
+			const supportedURLs = [
+				'example.com/image.png',
+				'www.example.com/image.png',
+				'https://www.example.com/image.png',
+				'https://example.com/image.png',
+				'http://www.example.com/image.png',
+				'http://example.com/image.png',
+				'http://example.com/image.jpg',
+				'http://example.com/image.jpeg',
+				'http://example.com/image.gif',
+				'http://example.com/image.ico',
+				'http://example.com/image.JPG',
+				'http://example.com%20Fimage.png',
+				'http://example.com/image.png?foo=bar',
+				'http://example.com/image.png#foo'
+			];
+
+			for ( const supportedURL of supportedURLs ) {
+				it( `should detect "${ supportedURL }" as a valid URL`, () => {
+					setData( editor.model, '<paragraph>[]</paragraph>' );
+					pasteHtml( editor, supportedURL );
+
+					clock.tick( 100 );
+
+					expect( getData( editor.model ) ).to.equal(
+						`[<image src="${ supportedURL }"><caption></caption></image>]`
+					);
+				} );
+			}
+		} );
+
+		describe( 'unsupported URL', () => {
+			const unsupportedOrInvalid = [
+				'http://www.example.com',
+				'https://example.com',
+				'http://www.example.com/image.svg',
+				'http://www.example.com/image.webp',
+				'http://www.example.com/image.mp3',
+				'http://www.example.com/image.exe',
+				'http://www.example.com/image.txt',
+				'http://example.com/image.png http://example.com/image.png',
+				'Foo bar http://example.com/image.png bar foo.',
+				'https://example.com/im age.png'
+			];
+
+			for ( const unsupportedURL of unsupportedOrInvalid ) {
+				it( `should not detect "${ unsupportedURL }" as a valid URL`, () => {
+					setData( editor.model, '<paragraph>[]</paragraph>' );
+					pasteHtml( editor, unsupportedURL );
+
+					clock.tick( 100 );
+
+					expect( getData( editor.model ) ).to.equal(
+						`<paragraph>${ unsupportedURL }[]</paragraph>`
+					);
+				} );
+			}
+		} );
+
+		it( 'works for URL that was pasted as a link', () => {
+			setData( editor.model, '<paragraph>[]</paragraph>' );
+			pasteHtml( editor, '<a href="http://example.com/image.png">' +
+				'http://example.com/image.png</a>' );
+
+			clock.tick( 100 );
+
+			expect( getData( editor.model ) ).to.equal(
+				'[<image src="http://example.com/image.png"><caption></caption></image>]'
+			);
+		} );
+
+		it( 'works for URL that contains some inline styles', () => {
+			setData( editor.model, '<paragraph>[]</paragraph>' );
+			pasteHtml( editor, '<b>http://example.com/image.png</b>' );
+
+			clock.tick( 100 );
+
+			expect( getData( editor.model ) ).to.equal(
+				'[<image src="http://example.com/image.png"><caption></caption></image>]'
+			);
+		} );
+
+		it( 'works for not collapsed selection inside single element', () => {
+			setData( editor.model, '<paragraph>[Foo]</paragraph>' );
+			pasteHtml( editor, 'http://example.com/image.png' );
+
+			clock.tick( 100 );
+
+			expect( getData( editor.model ) ).to.equal(
+				'[<image src="http://example.com/image.png"><caption></caption></image>]'
+			);
+		} );
+
+		it( 'works for not collapsed selection over a few elements', () => {
+			setData( editor.model, '<paragraph>Fo[o</paragraph><paragraph>Ba]r</paragraph>' );
+			pasteHtml( editor, 'http://example.com/image.png' );
+
+			clock.tick( 100 );
+
+			expect( getData( editor.model ) ).to.equal(
+				'<paragraph>Fo</paragraph>' +
+				'[<image src="http://example.com/image.png"><caption></caption></image>]' +
+				'<paragraph>r</paragraph>'
+			);
+		} );
+
+		it( 'inserts image in-place (collapsed selection)', () => {
+			setData( editor.model, '<paragraph>Foo []Bar</paragraph>' );
+			pasteHtml( editor, 'http://example.com/image.png' );
+
+			clock.tick( 100 );
+
+			expect( getData( editor.model ) ).to.equal(
+				'<paragraph>Foo </paragraph>' +
+				'[<image src="http://example.com/image.png"><caption></caption></image>]' +
+				'<paragraph>Bar</paragraph>'
+			);
+		} );
+
+		it( 'inserts image in-place (non-collapsed selection)', () => {
+			setData( editor.model, '<paragraph>Foo [Bar] Baz</paragraph>' );
+			pasteHtml( editor, 'http://example.com/image.png' );
+
+			clock.tick( 100 );
+
+			expect( getData( editor.model ) ).to.equal(
+				'<paragraph>Foo </paragraph>' +
+				'[<image src="http://example.com/image.png"><caption></caption></image>]' +
+				'<paragraph> Baz</paragraph>'
+			);
+		} );
+
+		it( 'does nothing if pasted more than single node', () => {
+			setData( editor.model, '<paragraph>[]</paragraph>' );
+			pasteHtml( editor,
+				'http://example.com/image.png ' +
+				'<a href="http://example.com/image.png">' +
+				'http://example.com/image.png</a>'
+			);
+
+			clock.tick( 100 );
+
+			expect( getData( editor.model, { withoutSelection: true } ) ).to.equal(
+				'<paragraph>http://example.com/image.png ' +
+				'<$text linkHref="http://example.com/image.png">' +
+				'http://example.com/image.png</$text>' +
+				'</paragraph>'
+			);
+		} );
+
+		it( 'does nothing if pasted a paragraph with the url', () => {
+			setData( editor.model, '<paragraph>[]</paragraph>' );
+			pasteHtml( editor, '<p>http://example.com/image.png</p>' );
+
+			clock.tick( 100 );
+
+			expect( getData( editor.model ) ).to.equal(
+				'<paragraph>http://example.com/image.png[]</paragraph>'
+			);
+		} );
+
+		it( 'does nothing if pasted a block of content that looks like a URL', () => {
+			setData( editor.model, '<paragraph>[]</paragraph>' );
+			pasteHtml( editor, '<p>https://</p><p>example.com/image</p><p>.png</p>' );
+
+			clock.tick( 100 );
+
+			expect( getData( editor.model ) ).to.equal(
+				'<paragraph>https://</paragraph><paragraph>example.com/image</paragraph><paragraph>.png[]</paragraph>'
+			);
+		} );
+
+		// #47
+		it( 'does not transform a valid URL into a image if the element cannot be placed in the current position', () => {
+			setData( editor.model, '<image src="/assets/sample.png"><caption>Foo.[]</caption></image>' );
+			pasteHtml( editor, 'http://example.com/image.png' );
+
+			clock.tick( 100 );
+
+			expect( getData( editor.model ) ).to.equal(
+				'<image src="/assets/sample.png"><caption>' +
+				'Foo.http://example.com/image.png[]' +
+				'</caption></image>'
+			);
+		} );
+
+		it( 'replaces a URL in image if pasted a link when other image element was selected', () => {
+			setData(
+				editor.model,
+				'[<image src="http://example.com/image.png"><caption></caption></image>]'
+			);
+
+			pasteHtml( editor, 'http://example.com/image2.png' );
+
+			clock.tick( 100 );
+
+			expect( getData( editor.model ) ).to.equal(
+				'[<image src="http://example.com/image2.png"><caption></caption></image>]'
+			);
+		} );
+
+		it( 'inserts a new image element if pasted a link when other image element was selected in correct place', () => {
+			setData(
+				editor.model,
+				'<paragraph>Foo. <$text linkHref="https://cksource.com">Bar</$text></paragraph>' +
+				'[<image src="http://example.com/image.png"><caption></caption></image>]' +
+				'<paragraph><$text>Bar</$text>.</paragraph>'
+			);
+
+			pasteHtml( editor, 'http://example.com/image2.png' );
+
+			clock.tick( 100 );
+
+			expect( getData( editor.model ) ).to.equal(
+				'<paragraph>Foo. <$text linkHref="https://cksource.com">Bar</$text></paragraph>' +
+				'[<image src="http://example.com/image2.png"><caption></caption></image>]' +
+				'<paragraph>Bar.</paragraph>'
+			);
+		} );
+	} );
+
+	describe( 'use real timers', () => {
+		const characters = Array( 10 ).fill( 1 ).map( ( x, i ) => String.fromCharCode( 65 + i ) );
+
+		it( 'undo breaks the auto-image feature (undo was done before auto-image)', done => {
+			setData( editor.model, '<paragraph>[]</paragraph>' );
+			pasteHtml( editor, 'http://example.com/image.png' );
+
+			expect( getData( editor.model ) ).to.equal(
+				'<paragraph>http://example.com/image.png[]</paragraph>'
+			);
+
+			setTimeout( () => {
+				editor.commands.execute( 'undo' );
+
+				expect( getData( editor.model ) ).to.equal(
+					'<paragraph>[]</paragraph>'
+				);
+
+				done();
+			} );
+		} );
+
+		// Checking whether paste+typing calls the auto-image handler once.
+		it( 'pasting handler should be executed once', done => {
+			const AutoImagePlugin = editor.plugins.get( AutoImage );
+			const autoImageHandler = AutoImagePlugin._embedImageBetweenPositions;
+			let counter = 0;
+
+			AutoImagePlugin._embedImageBetweenPositions = function( ...args ) {
+				counter += 1;
+
+				return autoImageHandler.apply( this, args );
+			};
+
+			setData( editor.model, '<paragraph>[]</paragraph>' );
+			pasteHtml( editor, 'http://example.com/image.png' );
+			simulateTyping( 'Foo. Bar.' );
+
+			setTimeout( () => {
+				AutoImagePlugin._embedImageBetweenPositions = autoImageHandler;
+
+				expect( counter ).to.equal( 1 );
+
+				done();
+			}, 100 );
+		} );
+
+		it( 'typing before pasted link during collaboration should not blow up', done => {
+			setData( editor.model, '<paragraph>[]</paragraph>' );
+
+			pasteHtml( editor, 'http://example.com/image.png' );
+
+			for ( let i = 0; i < 10; ++i ) {
+				const rootEl = editor.model.document.getRoot();
+
+				setTimeout( () => {
+					editor.model.enqueueChange( 'transparent', writer => {
+						writer.insertText( characters[ i ], writer.createPositionFromPath( rootEl, [ 0, i ] ) );
+					} );
+				}, i * 5 );
+			}
+
+			setTimeout( () => {
+				expect( getData( editor.model ) ).to.equal(
+					'[<image src="http://example.com/image.png"><caption></caption></image>]' +
+					'<paragraph>ABCDEFGHIJ</paragraph>'
+				);
+
+				done();
+			}, 100 );
+		} );
+
+		it( 'typing after pasted link during collaboration should not blow up', done => {
+			setData( editor.model, '<paragraph>[]</paragraph>' );
+
+			pasteHtml( editor, 'http://example.com/image.png' );
+
+			for ( let i = 0; i < 10; ++i ) {
+				setTimeout( () => {
+					editor.model.enqueueChange( 'transparent', writer => {
+						writer.insertText( characters[ i ], editor.model.document.selection.getFirstPosition() );
+					} );
+				}, i * 5 );
+			}
+
+			setTimeout( () => {
+				expect( getData( editor.model ) ).to.equal(
+					'[<image src="http://example.com/image.png"><caption></caption></image>]' +
+					'<paragraph>ABCDEFGHIJ</paragraph>'
+				);
+
+				done();
+			}, 100 );
+		} );
+
+		it( 'should insert the image element even if parent element where the URL was pasted has been deleted', done => {
+			setData( editor.model, '<paragraph>Foo.</paragraph><paragraph>Bar.[]</paragraph>' );
+
+			pasteHtml( editor, 'http://example.com/image.png' );
+
+			editor.model.enqueueChange( 'transparent', writer => {
+				writer.remove( writer.createRangeOn( editor.model.document.getRoot().getChild( 1 ) ) );
+			} );
+
+			setTimeout( () => {
+				expect( getData( editor.model ) ).to.equal(
+					'<paragraph>Foo.</paragraph>' +
+					'[<image src="http://example.com/image.png"><caption></caption></image>]'
+				);
+
+				done();
+			}, 100 );
+		} );
+
+		it( 'should insert the image element even if new element appeared above the pasted URL', done => {
+			setData( editor.model, '<paragraph>Foo.</paragraph><paragraph>Bar.[]</paragraph>' );
+
+			pasteHtml( editor, 'http://example.com/image.png' );
+
+			editor.model.enqueueChange( 'transparent', writer => {
+				const paragraph = writer.createElement( 'paragraph' );
+				writer.insert( paragraph, writer.createPositionAfter( editor.model.document.getRoot().getChild( 0 ) ) );
+				writer.setSelection( paragraph, 'in' );
+			} );
+
+			for ( let i = 0; i < 10; ++i ) {
+				setTimeout( () => {
+					editor.model.enqueueChange( 'transparent', writer => {
+						writer.insertText( characters[ i ], editor.model.document.selection.getFirstPosition() );
+					} );
+				}, i * 5 );
+			}
+
+			setTimeout( () => {
+				expect( getData( editor.model ) ).to.equal(
+					'<paragraph>Foo.</paragraph>' +
+					'<paragraph>ABCDEFGHIJ</paragraph>' +
+					'<paragraph>Bar.</paragraph>' +
+					'[<image src="http://example.com/image.png"><caption></caption></image>]'
+				);
+
+				done();
+			}, 100 );
+		} );
+
+		it( 'should insert the image element even if new element appeared below the pasted URL', done => {
+			setData( editor.model, '<paragraph>Foo.</paragraph><paragraph>Bar.[]</paragraph>' );
+
+			pasteHtml( editor, 'http://example.com/image.png' );
+
+			editor.model.enqueueChange( 'transparent', writer => {
+				const paragraph = writer.createElement( 'paragraph' );
+				writer.insert( paragraph, writer.createPositionAfter( editor.model.document.getRoot().getChild( 1 ) ) );
+				writer.setSelection( paragraph, 'in' );
+			} );
+
+			for ( let i = 0; i < 10; ++i ) {
+				setTimeout( () => {
+					editor.model.enqueueChange( 'transparent', writer => {
+						writer.insertText( characters[ i ], editor.model.document.selection.getFirstPosition() );
+					} );
+				}, i * 5 );
+			}
+
+			setTimeout( () => {
+				expect( getData( editor.model ) ).to.equal(
+					'<paragraph>Foo.</paragraph>' +
+					'<paragraph>Bar.</paragraph>' +
+					'[<image src="http://example.com/image.png"><caption></caption></image>]' +
+					'<paragraph>ABCDEFGHIJ</paragraph>'
+				);
+
+				done();
+			}, 100 );
+		} );
+	} );
+
+	it( 'should detach LiveRange', async () => {
+		const editor = await ClassicTestEditor.create( editorElement, {
+			plugins: [ Typing, Paragraph, Link, Image, ImageCaption, Table, AutoImage ]
+		} );
+
+		setData(
+			editor.model,
+			'<table>' +
+				'<tableRow>' +
+					'[<tableCell><paragraph>foo</paragraph></tableCell>]' +
+					'[<tableCell><paragraph>bar</paragraph></tableCell>]' +
+				'</tableRow>' +
+			'</table>'
+		);
+
+		pasteHtml( editor, '<table><tr><td>one</td><td>two</td></tr></table>' );
+
+		expect( getData( editor.model, { withoutSelection: true } ) ).to.equal(
+			'<table>' +
+				'<tableRow>' +
+					'<tableCell><paragraph>one</paragraph></tableCell>' +
+					'<tableCell><paragraph>two</paragraph></tableCell>' +
+				'</tableRow>' +
+			'</table>'
+		);
+
+		expect( () => {
+			editor.setData( '' );
+		} ).not.to.throw();
+
+		await editor.destroy();
+	} );
+
+	function simulateTyping( text ) {
+		// While typing, every character is an atomic change.
+		text.split( '' ).forEach( character => {
+			editor.execute( 'input', {
+				text: character
+			} );
+		} );
+	}
+
+	function pasteHtml( editor, html ) {
+		editor.editing.view.document.fire( 'paste', {
+			dataTransfer: createDataTransfer( { 'text/html': html } ),
+			stopPropagation() {},
+			preventDefault() {}
+		} );
+	}
+
+	function createDataTransfer( data ) {
+		return {
+			getData( type ) {
+				return data[ type ];
+			}
+		};
+	}
+} );

+ 3 - 9
packages/ckeditor5-image/tests/image/utils.js

@@ -240,9 +240,7 @@ describe( 'image widget utils', () => {
 		it( 'should insert image at selection position as other widgets', () => {
 			setModelData( model, '<paragraph>f[o]o</paragraph>' );
 
-			model.change( writer => {
-				insertImage( writer, model );
-			} );
+			insertImage( model );
 
 			expect( getModelData( model ) ).to.equal( '[<image></image>]<paragraph>foo</paragraph>' );
 		} );
@@ -250,9 +248,7 @@ describe( 'image widget utils', () => {
 		it( 'should insert image with given attributes', () => {
 			setModelData( model, '<paragraph>f[o]o</paragraph>' );
 
-			model.change( writer => {
-				insertImage( writer, model, { src: 'bar' } );
-			} );
+			insertImage( model, { src: 'bar' } );
 
 			expect( getModelData( model ) ).to.equal( '[<image src="bar"></image>]<paragraph>foo</paragraph>' );
 		} );
@@ -268,9 +264,7 @@ describe( 'image widget utils', () => {
 
 			setModelData( model, '<other>[]</other>' );
 
-			model.change( writer => {
-				insertImage( writer, model );
-			} );
+			insertImage( model );
 
 			expect( getModelData( model ) ).to.equal( '<other>[]</other>' );
 		} );

+ 5 - 4
packages/ckeditor5-image/tests/imageinsert/ui/imageinsertpanelview.js

@@ -161,9 +161,9 @@ describe( 'ImageUploadPanelView', () => {
 		} );
 
 		it( 'should register child views\' #element in #focusTracker with no integrations', () => {
-			const spy = testUtils.sinon.spy( FocusTracker.prototype, 'add' );
-
 			view = new ImageUploadPanelView( { t: () => {} } );
+
+			const spy = testUtils.sinon.spy( view.focusTracker, 'add' );
 			view.render();
 
 			sinon.assert.calledWithExactly( spy.getCall( 0 ), view.insertButtonView.element );
@@ -171,11 +171,12 @@ describe( 'ImageUploadPanelView', () => {
 		} );
 
 		it( 'should register child views\' #element in #focusTracker with "insertImageViaUrl" integration', () => {
-			const spy = testUtils.sinon.spy( FocusTracker.prototype, 'add' );
-
 			view = new ImageUploadPanelView( { t: () => {} }, {
 				'insertImageViaUrl': createLabeledInputView( { t: val => val } )
 			} );
+
+			const spy = testUtils.sinon.spy( view.focusTracker, 'add' );
+
 			view.render();
 
 			sinon.assert.calledWithExactly( spy.getCall( 0 ), view.getIntegration( 'insertImageViaUrl' ).element );

+ 1 - 6
packages/ckeditor5-image/tests/imageinsert/utils.js

@@ -130,12 +130,7 @@ describe( 'Upload utils', () => {
 		describe( 'image URL input view', () => {
 			it( 'should have placeholder', () => {
 				const view = createLabeledInputView( { t: val => val } );
-				expect( view.fieldView.placeholder ).to.equal( 'https://example.com/src/image.png' );
-			} );
-
-			it( 'should have info text', () => {
-				const view = createLabeledInputView( { t: val => val } );
-				expect( view.infoText ).to.match( /^Paste the image source URL/ );
+				expect( view.fieldView.placeholder ).to.equal( 'https://example.com/image.png' );
 			} );
 		} );
 	} );

+ 37 - 0
packages/ckeditor5-image/tests/imageresize/_utils/utils.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
+ */
+
+import ImageLoadObserver from '../../../src/image/imageloadobserver';
+
+// A 100x50 black png image
+export const IMAGE_SRC_FIXTURE = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAAAyCAQAAAAAPLY1AAAAQklEQVR42u3PQREAAAgDoK1/' +
+		'aM3g14MGNJMXKiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiJysRFNMgH0RpujAAAAAElFTkSuQmCC';
+
+export async function waitForAllImagesLoaded( editor ) {
+	// Returns a promise that waits for all the images in editor to be loaded.
+	// This is needed because resizers are created only after images are loaded.
+	const root = editor.model.document.getRoot();
+	const editingView = editor.editing.view;
+	const images = new Set();
+
+	for ( const curModel of root.getChildren() ) {
+		if ( curModel.is( 'element', 'image' ) ) {
+			const imageView = editor.editing.mapper.toViewElement( curModel );
+			images.add( editingView.domConverter.viewToDom( imageView ).querySelector( 'img' ) );
+		}
+	}
+
+	editingView.addObserver( ImageLoadObserver );
+
+	return new Promise( resolve => {
+		editingView.document.on( 'imageLoaded', ( evt, domEvent ) => {
+			images.delete( domEvent.target );
+
+			if ( images.size === 0 ) {
+				resolve();
+			}
+		} );
+	} );
+}

+ 1 - 4
packages/ckeditor5-image/tests/imageresize/imageresizeediting.js

@@ -18,12 +18,9 @@ import ImageStyle from '../../src/imagestyle';
 import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
 import { focusEditor } from '@ckeditor/ckeditor5-widget/tests/widgetresize/_utils/utils';
+import { IMAGE_SRC_FIXTURE } from './_utils/utils';
 
 describe( 'ImageResizeEditing', () => {
-	// 100x50 black png image
-	const IMAGE_SRC_FIXTURE = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAAAyCAQAAAAAPLY1AAAAQklEQVR42u3PQREAAAgDoK1/' +
-		'aM3g14MGNJMXKiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiJysRFNMgH0RpujAAAAAElFTkSuQmCC';
-
 	let editor, editorElement;
 
 	beforeEach( () => {

+ 78 - 17
packages/ckeditor5-image/tests/imageresize/imageresizehandles.js

@@ -30,12 +30,9 @@ import {
 } from '@ckeditor/ckeditor5-widget/tests/widgetresize/_utils/utils';
 
 import WidgetResize from '@ckeditor/ckeditor5-widget/src/widgetresize';
+import { IMAGE_SRC_FIXTURE, waitForAllImagesLoaded } from './_utils/utils';
 
 describe( 'ImageResizeHandles', () => {
-	// 100x50 black png image
-	const IMAGE_SRC_FIXTURE = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAAAyCAQAAAAAPLY1AAAAQklEQVR42u3PQREAAAgDoK1/' +
-		'aM3g14MGNJMXKiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiJysRFNMgH0RpujAAAAAElFTkSuQmCC';
-
 	let widget, editor, view, viewDocument, editorElement;
 
 	beforeEach( () => {
@@ -62,7 +59,7 @@ describe( 'ImageResizeHandles', () => {
 
 		const attachToSpy = sinon.spy( localEditor.plugins.get( WidgetResize ), 'attachTo' );
 
-		setData( localEditor.model, `[<image imageStyle="side" src="${ IMAGE_SRC_FIXTURE }"></image>]` );
+		await setModelAndWaitForImages( localEditor, `[<image imageStyle="side" src="${ IMAGE_SRC_FIXTURE }"></image>]` );
 
 		expect( attachToSpy.args[ 0 ][ 0 ] ).to.have.a.property( 'unit', '%' );
 
@@ -79,7 +76,7 @@ describe( 'ImageResizeHandles', () => {
 		it( 'uses the command on commit', async () => {
 			const spy = sinon.spy( editor.commands.get( 'imageResize' ), 'execute' );
 
-			setData( editor.model, `<paragraph>foo</paragraph>[<image src="${ IMAGE_SRC_FIXTURE }"></image>]` );
+			await setModelAndWaitForImages( editor, `<paragraph>foo</paragraph>[<image src="${ IMAGE_SRC_FIXTURE }"></image>]` );
 			widget = viewDocument.getRoot().getChild( 1 );
 			const domParts = getWidgetDomParts( editor, widget, 'bottom-left' );
 
@@ -92,7 +89,7 @@ describe( 'ImageResizeHandles', () => {
 		} );
 
 		it( 'disables the resizer if the command is disabled', async () => {
-			setData( editor.model, `<paragraph>foo</paragraph>[<image src="${ IMAGE_SRC_FIXTURE }"></image>]` );
+			await setModelAndWaitForImages( editor, `<paragraph>foo</paragraph>[<image src="${ IMAGE_SRC_FIXTURE }"></image>]` );
 
 			const resizer = getSelectedImageResizer( editor );
 
@@ -124,6 +121,8 @@ describe( 'ImageResizeHandles', () => {
 				editor.model.insertContent( writer.createElement( 'image', { src: IMAGE_SRC_FIXTURE } ) );
 			} );
 
+			await waitForAllImagesLoaded( editor );
+
 			const resizer = getSelectedImageResizer( editor );
 			const resizerWrapper = editor.ui.getEditableElement().querySelector( '.ck-widget__resizer' );
 
@@ -136,7 +135,8 @@ describe( 'ImageResizeHandles', () => {
 		beforeEach( async () => {
 			editor = await createEditor();
 
-			setData( editor.model, `<paragraph>foo</paragraph>[<image imageStyle="side" src="${ IMAGE_SRC_FIXTURE }"></image>]` );
+			await setModelAndWaitForImages( editor,
+				`<paragraph>foo</paragraph>[<image imageStyle="side" src="${ IMAGE_SRC_FIXTURE }"></image>]` );
 
 			widget = viewDocument.getRoot().getChild( 1 );
 		} );
@@ -178,7 +178,7 @@ describe( 'ImageResizeHandles', () => {
 		beforeEach( async () => {
 			editor = await createEditor();
 
-			setData( editor.model, `<paragraph>foo</paragraph>[<image src="${ IMAGE_SRC_FIXTURE }"></image>]` );
+			await setModelAndWaitForImages( editor, `<paragraph>foo</paragraph>[<image src="${ IMAGE_SRC_FIXTURE }"></image>]` );
 
 			widget = viewDocument.getRoot().getChild( 1 );
 		} );
@@ -200,9 +200,9 @@ describe( 'ImageResizeHandles', () => {
 
 			// Toggle _visibleResizer to force synchronous redraw. Otherwise you'd need to wait ~200ms for
 			// throttled redraw to take place, making tests slower.
-			const visibleResizer = plugin._visibleResizer;
-			plugin._visibleResizer = null;
-			plugin._visibleResizer = visibleResizer;
+			for ( const [ , resizer ] of plugin._resizers.entries() ) {
+				resizer.redraw();
+			}
 
 			const resizerWrapper = document.querySelector( '.ck-widget__resizer' );
 			const shadowBoundingRect = resizerWrapper.getBoundingClientRect();
@@ -210,13 +210,37 @@ describe( 'ImageResizeHandles', () => {
 			expect( shadowBoundingRect.width ).to.equal( 100 );
 			expect( shadowBoundingRect.height ).to.equal( 50 );
 		} );
+
+		it( 'doesn\'t show resizers when undoing to multiple images', async () => {
+			// Based on https://github.com/ckeditor/ckeditor5/pull/8108#issuecomment-695949745.
+			await setModelAndWaitForImages( editor,
+				`[<image src="${ IMAGE_SRC_FIXTURE }"></image><image src="${ IMAGE_SRC_FIXTURE }"></image>]` );
+
+			const paragraph = editor.model.change( writer => {
+				return writer.createElement( 'paragraph' );
+			} );
+			editor.model.insertContent( paragraph );
+
+			// Undo to go back to two, selected images.
+			editor.commands.get( 'undo' ).execute();
+
+			for ( let i = 0; i < 2; i++ ) {
+				widget = viewDocument.getRoot().getChild( i );
+				const domImage = getWidgetDomParts( editor, widget, 'bottom-right' ).widget.querySelector( 'img' );
+				viewDocument.fire( 'imageLoaded', { target: domImage } );
+
+				const domResizeWrapper = getWidgetDomParts( editor, widget, 'bottom-left' ).resizeWrapper;
+
+				expect( domResizeWrapper.getBoundingClientRect().height ).to.equal( 0 );
+			}
+		} );
 	} );
 
 	describe( 'table integration', () => {
 		it( 'works when resizing in a table', async () => {
 			editor = await createEditor();
 
-			setData( editor.model,
+			await setModelAndWaitForImages( editor,
 				'<table>' +
 					`<tableRow><tableCell>[<image src="${ IMAGE_SRC_FIXTURE }"></image>]</tableCell></tableRow>` +
 				'</table>'
@@ -238,6 +262,36 @@ describe( 'ImageResizeHandles', () => {
 		} );
 	} );
 
+	it( 'doesn\'t create multiple resizers for a single image widget', async () => {
+		// https://github.com/ckeditor/ckeditor5/pull/8108#issuecomment-708302992
+		editor = await createEditor();
+		await setModelAndWaitForImages( editor, `[<image src="${ IMAGE_SRC_FIXTURE }"></image>]` );
+		widget = viewDocument.getRoot().getChild( 0 );
+
+		const domParts = getWidgetDomParts( editor, widget, 'bottom-right' );
+		const alternativeImageFixture =
+			'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=';
+
+		// Change the image so that load event triggers for the same img element again.
+		domParts.widget.querySelector( 'img' ).src = alternativeImageFixture;
+		await waitForAllImagesLoaded( editor );
+
+		expect( domParts.widget.querySelectorAll( '.ck-widget__resizer' ).length ).to.equal( 1 );
+	} );
+
+	it( 'only creates a resizer after the image is loaded', async () => {
+		// https://github.com/ckeditor/ckeditor5/issues/8088
+		editor = await createEditor();
+		setData( editor.model, `[<image src="${ IMAGE_SRC_FIXTURE }"></image>]` );
+		widget = viewDocument.getRoot().getChild( 0 );
+		const domParts = getWidgetDomParts( editor, widget, 'bottom-right' );
+
+		expect( domParts.widget.querySelectorAll( '.ck-widget__resizer' ).length ).to.equal( 0 );
+
+		await waitForAllImagesLoaded( editor );
+		expect( domParts.widget.querySelectorAll( '.ck-widget__resizer' ).length ).to.equal( 1 );
+	} );
+
 	describe( 'srcset integration', () => {
 		// The image is 96x96 pixels.
 		const imageBaseUrl = '/assets/sample.png';
@@ -272,11 +326,13 @@ describe( 'ImageResizeHandles', () => {
 				</figure>`
 			);
 
+			await waitForAllImagesLoaded( editor );
+
 			widget = viewDocument.getRoot().getChild( 0 );
 			model = editor.model.document.getRoot().getChild( 0 );
 		} );
 
-		it( 'works with images containing srcset', () => {
+		it( 'works with images containing srcset', async () => {
 			const domParts = getWidgetDomParts( editor, widget, 'bottom-right' );
 			const initialPosition = getHandleCenterPoint( domParts.widget, 'bottom-right' );
 			const finalPointerPosition = initialPosition.clone().moveBy( -20, -20 );
@@ -289,7 +345,7 @@ describe( 'ImageResizeHandles', () => {
 			expect( model.getAttribute( 'width' ) ).to.equal( '76px' );
 		} );
 
-		it( 'retains width after removing srcset', () => {
+		it( 'retains width after removing srcset', async () => {
 			const domParts = getWidgetDomParts( editor, widget, 'bottom-right' );
 			const initialPosition = getHandleCenterPoint( domParts.widget, 'bottom-right' );
 			const finalPointerPosition = initialPosition.clone().moveBy( -16, -16 );
@@ -339,7 +395,7 @@ describe( 'ImageResizeHandles', () => {
 				}
 			} );
 
-			setData( editor.model, `<paragraph>foo</paragraph>[<image src="${ IMAGE_SRC_FIXTURE }"></image>]` );
+			await setModelAndWaitForImages( editor, `<paragraph>foo</paragraph>[<image src="${ IMAGE_SRC_FIXTURE }"></image>]` );
 
 			widget = viewDocument.getRoot().getChild( 1 );
 
@@ -395,7 +451,7 @@ describe( 'ImageResizeHandles', () => {
 	}
 
 	function getSelectedImageResizer( editor ) {
-		return editor.plugins.get( 'WidgetResize' )._getResizerByViewElement(
+		return editor.plugins.get( 'WidgetResize' ).getResizerByViewElement(
 			editor.editing.view.document.selection.getSelectedElement()
 		);
 	}
@@ -415,4 +471,9 @@ describe( 'ImageResizeHandles', () => {
 			return newEditor;
 		} );
 	}
+
+	async function setModelAndWaitForImages( editor, data ) {
+		setData( editor.model, data );
+		return waitForAllImagesLoaded( editor );
+	}
 } );

+ 13 - 0
packages/ckeditor5-image/tests/imagetextalternative/imagetextalternativeui.js

@@ -96,6 +96,19 @@ describe( 'ImageTextAlternativeUI', () => {
 			sinon.assert.calledOnce( spy );
 			expect( form.labeledInput.fieldView.value ).equals( '' );
 		} );
+
+		it( 'should disable CSS transitions before showing the form to avoid unnecessary animations (and then enable them again)', () => {
+			const addSpy = sinon.spy( balloon, 'add' );
+			const disableCssTransitionsSpy = sinon.spy( form, 'disableCssTransitions' );
+			const enableCssTransitionsSpy = sinon.spy( form, 'enableCssTransitions' );
+			const selectSpy = sinon.spy( form.labeledInput.fieldView, 'select' );
+
+			setData( model, '[<image src="" alt="foo bar"></image>]' );
+
+			button.fire( 'execute' );
+
+			sinon.assert.callOrder( disableCssTransitionsSpy, addSpy, selectSpy, enableCssTransitionsSpy );
+		} );
 	} );
 
 	describe( 'balloon panel form', () => {

+ 5 - 1
packages/ckeditor5-image/tests/imagetextalternative/ui/textalternativeformview.js

@@ -67,6 +67,10 @@ describe( 'TextAlternativeFormView', () => {
 
 			sinon.assert.calledOnce( spy );
 		} );
+
+		it( 'should implement the CSS transition disabling feature', () => {
+			expect( view.disableCssTransitions ).to.be.a( 'function' );
+		} );
 	} );
 
 	describe( 'render()', () => {
@@ -90,7 +94,7 @@ describe( 'TextAlternativeFormView', () => {
 			} );
 
 			it( 'should register child views\' #element in #focusTracker', () => {
-				const spy = testUtils.sinon.spy( FocusTracker.prototype, 'add' );
+				const spy = testUtils.sinon.spy( view.focusTracker, 'add' );
 
 				view.render();
 

+ 3 - 2
packages/ckeditor5-image/tests/manual/imageinsertviaurl.js

@@ -7,12 +7,13 @@
 
 import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
 import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
-import ImageInsert from '../../src/imageinsert';
 import CKFinder from '@ckeditor/ckeditor5-ckfinder/src/ckfinder';
+import ImageInsert from '../../src/imageinsert';
+import AutoImage from '../../src/autoimage';
 
 ClassicEditor
 	.create( document.querySelector( '#editor' ), {
-		plugins: [ ArticlePluginSet, ImageInsert, CKFinder ],
+		plugins: [ ArticlePluginSet, ImageInsert, AutoImage, CKFinder ],
 		toolbar: [
 			'heading',
 			'|',

+ 5 - 0
packages/ckeditor5-image/tests/manual/imageinsertviaurl.md

@@ -16,3 +16,8 @@
 1. In the **Editor 2** click on the arrow button in `imageInsert` plugin to reveal the image insert panel.
 1. Click on the **CKFinder** button.
 1. Choose image and confirm.
+
+## Image insert via pasting URL into editor
+
+1. Copy image URL to clipboard (eg: `https://ckeditor.com/docs/ckeditor5/latest/assets/img/malta.jpg`).
+1. Paste the URL to the editor.

+ 1 - 1
packages/ckeditor5-image/theme/imageinsert.css

@@ -4,7 +4,7 @@
  */
 
 .ck.ck-image-insert__panel {
-	padding: var(--ck-spacing-standard);
+	padding: var(--ck-spacing-large);
 }
 
 .ck.ck-image-insert__ck-finder-button {

+ 5 - 1
packages/ckeditor5-link/src/linkui.js

@@ -163,7 +163,7 @@ export default class LinkUI extends Plugin {
 		const linkCommand = editor.commands.get( 'link' );
 		const defaultProtocol = editor.config.get( 'link.defaultProtocol' );
 
-		const formView = new LinkFormView( editor.locale, linkCommand, defaultProtocol );
+		const formView = new LinkFormView( editor.locale, linkCommand );
 
 		formView.urlInputView.fieldView.bind( 'value' ).to( linkCommand, 'value' );
 
@@ -314,6 +314,8 @@ export default class LinkUI extends Plugin {
 		const editor = this.editor;
 		const linkCommand = editor.commands.get( 'link' );
 
+		this.formView.disableCssTransitions();
+
 		this._balloon.add( {
 			view: this.formView,
 			position: this._getBalloonPositionData()
@@ -324,6 +326,8 @@ export default class LinkUI extends Plugin {
 			this.formView.urlInputView.fieldView.select();
 		}
 
+		this.formView.enableCssTransitions();
+
 		// Make sure that each time the panel shows up, the URL field remains in sync with the value of
 		// the command. If the user typed in the input, then canceled the balloon (`urlInputView.fieldView#value` stays
 		// unaltered) and re-opened it without changing the value of the link command (e.g. because they

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

@@ -15,6 +15,7 @@ import SwitchButtonView from '@ckeditor/ckeditor5-ui/src/button/switchbuttonview
 
 import LabeledFieldView from '@ckeditor/ckeditor5-ui/src/labeledfield/labeledfieldview';
 import { createLabeledInputText } from '@ckeditor/ckeditor5-ui/src/labeledfield/utils';
+import injectCssTransitionDisabler from '@ckeditor/ckeditor5-ui/src/bindings/injectcsstransitiondisabler';
 
 import submitHandler from '@ckeditor/ckeditor5-ui/src/bindings/submithandler';
 import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker';
@@ -43,7 +44,7 @@ export default class LinkFormView extends View {
 	 * @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, protocol ) {
+	constructor( locale, linkCommand ) {
 		super( locale );
 
 		const t = locale.t;
@@ -69,7 +70,7 @@ export default class LinkFormView extends View {
 		 *
 		 * @member {module:ui/labeledfield/labeledfieldview~LabeledFieldView}
 		 */
-		this.urlInputView = this._createUrlInput( protocol );
+		this.urlInputView = this._createUrlInput();
 
 		/**
 		 * The Save button view.
@@ -152,6 +153,8 @@ export default class LinkFormView extends View {
 
 			children: this.children
 		} );
+
+		injectCssTransitionDisabler( this );
 	}
 
 	/**
@@ -209,15 +212,13 @@ export default class LinkFormView extends View {
 	 * Creates a labeled input view.
 	 *
 	 * @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.
 	 */
-	_createUrlInput( protocol = 'https://' ) {
+	_createUrlInput() {
 		const t = this.locale.t;
 		const labeledInput = new LabeledFieldView( this.locale, createLabeledInputText );
 
 		labeledInput.label = t( 'Link URL' );
-		labeledInput.fieldView.placeholder = protocol + 'example.com';
 
 		return labeledInput;
 	}

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

@@ -1107,6 +1107,18 @@ describe( 'LinkUI', () => {
 				sinon.assert.calledOnce( selectSpy );
 			} );
 
+			it( 'should disable CSS transitions before showing the form to avoid unnecessary animations' +
+				'(and then enable them again)', () => {
+				const addSpy = sinon.spy( balloon, 'add' );
+				const disableCssTransitionsSpy = sinon.spy( formView, 'disableCssTransitions' );
+				const enableCssTransitionsSpy = sinon.spy( formView, 'enableCssTransitions' );
+				const selectSpy = sinon.spy( formView.urlInputView.fieldView, 'select' );
+
+				actionsView.fire( 'edit' );
+
+				sinon.assert.callOrder( disableCssTransitionsSpy, addSpy, selectSpy, enableCssTransitionsSpy );
+			} );
+
 			it( 'should execute unlink command on actionsView#unlink event', () => {
 				const executeSpy = testUtils.sinon.spy( editor, 'execute' );
 

+ 7 - 8
packages/ckeditor5-link/tests/ui/linkformview.js

@@ -82,12 +82,6 @@ describe( 'LinkFormView', () => {
 			expect( spy.calledOnce ).to.true;
 		} );
 
-		describe( 'url input view', () => {
-			it( 'has placeholder', () => {
-				expect( view.urlInputView.fieldView.placeholder ).to.equal( 'https://example.com' );
-			} );
-		} );
-
 		describe( 'template', () => {
 			it( 'has url input view', () => {
 				expect( view.template.children[ 0 ].get( 0 ) ).to.equal( view.urlInputView );
@@ -98,6 +92,10 @@ describe( 'LinkFormView', () => {
 				expect( view.template.children[ 0 ].get( 2 ) ).to.equal( view.cancelButtonView );
 			} );
 		} );
+
+		it( 'should implement the CSS transition disabling feature', () => {
+			expect( view.disableCssTransitions ).to.be.a( 'function' );
+		} );
 	} );
 
 	describe( 'render()', () => {
@@ -110,9 +108,10 @@ describe( 'LinkFormView', () => {
 		} );
 
 		it( 'should register child views\' #element in #focusTracker', () => {
-			const spy = testUtils.sinon.spy( FocusTracker.prototype, 'add' );
-
 			view = new LinkFormView( { t: () => {} }, { manualDecorators: [] } );
+
+			const spy = testUtils.sinon.spy( view.focusTracker, 'add' );
+
 			view.render();
 
 			sinon.assert.calledWithExactly( spy.getCall( 0 ), view.urlInputView.element );

+ 11 - 0
packages/ckeditor5-link/theme/linkform.css

@@ -31,4 +31,15 @@
  */
 .ck.ck-link-form_layout-vertical {
 	display: block;
+
+	/*
+	 * Whether the form is in the responsive mode or not, if there are decorator buttons
+	 * keep the top margin of action buttons medium.
+	 */
+	& .ck-button {
+		&.ck-button-save,
+		&.ck-button-cancel {
+			margin-top: var(--ck-spacing-medium);
+		}
+	}
 }

+ 35 - 1
packages/ckeditor5-markdown-gfm/docs/_snippets/features/markdown.html

@@ -1,4 +1,38 @@
-<div id="snippet-markdown">This is [CKEditor 5](https://ckeditor.com).</div>
+<div id="snippet-markdown">
+
+<h2>The Markdown language</h2>
+<p>Markdown is a lightweight markup language that you can use to add formatting elements to plain text documents. Created by <a href="https://daringfireball.net/projects/markdown/" target="_blank">John Gruber</a> in 2004, Markdown is now one of the world&rsquo;s most popular markup languages.</p>
+
+<h2>Editing with Markdown output</h2>
+<p>The <a href="https://ckeditor.com/">CKEditor 5 WYSIWYG</a> editor lets you use this flexible yet simple markup language in the GitHub flavor. The editor-produced Markdown output supports the most important features, like <a href="https://ckeditor.com/">links</a>, <strong>different</strong> kinds of <em>emphasis</em>, <code>inline code formatting</code> or code blocks:</p>
+
+<pre>
+<code class="language-css">p {
+  text-align: center;
+  color: red;
+}</code>
+</pre>
+
+<h2>Formatting blocks with Markdown</h2>
+<p>Markdown can be used to create various block-level features, such as:</p>
+<ul>
+	<li>Block quotes</li>
+	<li>Headings
+		<ol>
+			<li>Heading 1</li>
+			<li>Heading 2</li>
+			<li>Heading 3</li>
+		</ol>
+	</li>
+	<li>Lists, including nested ones
+		<ul>
+			<li>Numbered lists</li>
+			<li>Bulleted lists</li>
+		</ul>
+	</li>
+</ul>
+
+</div>
 
 <p>Output:</p>
 

+ 20 - 2
packages/ckeditor5-markdown-gfm/docs/_snippets/features/markdown.js

@@ -9,32 +9,50 @@ import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'
 
 import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
 import EasyImage from '@ckeditor/ckeditor5-easy-image/src/easyimage';
+import CodeBlock from '@ckeditor/ckeditor5-code-block/src/codeblock';
+import Code from '@ckeditor/ckeditor5-basic-styles/src/code';
+import TodoList from '@ckeditor/ckeditor5-list/src/todolist';
 import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config';
 
 import Markdown from '@ckeditor/ckeditor5-markdown-gfm/src/markdown';
 
 ClassicEditor
 	.create( document.querySelector( '#snippet-markdown' ), {
-		plugins: [ ArticlePluginSet, EasyImage, Markdown ],
+		plugins: [ ArticlePluginSet, EasyImage, Markdown, Code, CodeBlock, TodoList ],
 		toolbar: [
 			'heading',
 			'|',
 			'bold',
 			'italic',
 			'link',
+			'|',
 			'bulletedList',
 			'numberedList',
+			'todoList',
+			'|',
+			'code',
+			'codeBlock',
 			'|',
 			'outdent',
 			'indent',
 			'|',
+			'imageUpload',
 			'blockQuote',
+			'|',
 			'undo',
 			'redo'
 		],
 		image: {
 			toolbar: [ 'imageStyle:full', 'imageStyle:side', '|', 'imageTextAlternative' ]
 		},
+		codeBlock: {
+			languages: [
+				{ language: 'css', label: 'CSS' },
+				{ language: 'html', label: 'HTML' },
+				{ language: 'javascript', label: 'JavaScript' },
+				{ language: 'php', label: 'PHP' }
+			]
+		},
 		cloudServices: CS_CONFIG
 	} )
 	.then( editor => {
@@ -46,7 +64,7 @@ ClassicEditor
 			outputElement.innerText = editor.getData();
 		} );
 
-		// Set the initial data with delay so hightlight.js doesn't catch them.
+		// Set the initial data with delay so hightlight.js doesn't catch it.
 		setTimeout( () => {
 			outputElement.innerText = editor.getData();
 		}, 500 );

+ 19 - 9
packages/ckeditor5-markdown-gfm/docs/features/markdown.md

@@ -12,26 +12,40 @@ editor.setData( 'This is **bold**.' );
 ```
 
 <info-box info>
-	"GFM" stands for "GitHub Flavored Markdown" &mdash; a Markdown dialect used by [GitHub](https://github.com). Markdown lacks any formal specification and has many dialects, often incompatible with each other. When converting the output produced by this data processor make sure to use a compatible Markdown to HTML converter (e.g. the [marked](https://www.npmjs.com/package/marked) library).
+	"GFM" stands for "GitHub Flavored Markdown" &mdash; a Markdown dialect used by [GitHub](https://github.com). Markdown lacks any formal specification (although the [CommonMark](https://commonmark.org/) initiative aims to close this gap) and has many dialects, often incompatible with one another. When converting the output produced by this data processor, make sure to use a compatible Markdown-to-HTML converter (e.g. the [marked](https://www.npmjs.com/package/marked) library).
 </info-box>
 
 <info-box info>
-	**This feature is experimental!**
+	This feature is still experimental but a stable version is coming soon.
 
 	While the CKEditor 5 architecture supports changing the data format, in most scenarios we do recommend sticking to the default format which is HTML (supported by the {@link module:engine/dataprocessor/htmldataprocessor~HtmlDataProcessor}). HTML remains [the best standard for rich-text data](https://medium.com/content-uneditable/a-standard-for-rich-text-data-4b3a507af552).
 
-	And please do remember – using Markdown [does not automatically make your application/website secure](https://github.com/ckeditor/ckeditor5-markdown-gfm/issues/16#issuecomment-375752994).
+	And please do remember &mdash; using Markdown [does not automatically make your application or website secure](https://github.com/ckeditor/ckeditor5-markdown-gfm/issues/16#issuecomment-375752994).
 </info-box>
 
+## Demo
+
+The CKEditor 5 instance below is configured to output GitHub Flavored Markdown. Use the editor to create your content and see the Markdown output displayed as you type below the editor.
+
+{@snippet features/markdown}
+
+## Related features
+
+Some other ways to output the edited content include:
+
+* [Export to Word](https://ckeditor.com/docs/ckeditor5/latest/features/export-word.html) &ndash; Generate editable `.docx` files out of your editor-created content.
+* [Export to PDF](https://ckeditor.com/docs/ckeditor5/latest/features/export-pdf.html) &ndash; Generate portable PDF files out of your editor-created content.
+* {@link features/autoformat Autoformatting} &ndash; Use Markdown-like markers to automatically format your content as you type!
+
 ## Installation
 
-To enable this data processor in your editor install the [`@ckeditor/ckeditor5-markdown-gfm`](https://www.npmjs.com/package/@ckeditor/ckeditor5-markdown-gfm) package:
+To enable this data processor in your editor, install the [`@ckeditor/ckeditor5-markdown-gfm`](https://www.npmjs.com/package/@ckeditor/ckeditor5-markdown-gfm) package:
 
 ```
 npm install --save @ckeditor/ckeditor5-markdown-gfm
 ```
 
-Then, you can enable this data processor by using {@link module:markdown-gfm/markdown~Markdown} plugin which will change default {@link module:engine/dataprocessor/dataprocessor~DataProcessor data processor} with {@link module:markdown-gfm/gfmdataprocessor~GFMDataProcessor}:
+Then, you can enable this data processor by using the {@link module:markdown-gfm/markdown~Markdown} plugin which will change the default {@link module:engine/dataprocessor/dataprocessor~DataProcessor data processor} to the {@link module:markdown-gfm/gfmdataprocessor~GFMDataProcessor}:
 
 ```js
 import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
@@ -60,10 +74,6 @@ ClassicEditor
 
 ```
 
-## Demo
-
-{@snippet features/markdown}
-
 ## Contribute
 
 The source code of this feature is available on GitHub in https://github.com/ckeditor/ckeditor5/tree/master/packages/ckeditor5-markdown-gfm.

+ 15 - 0
packages/ckeditor5-media-embed/src/mediaembedui.js

@@ -53,6 +53,12 @@ export default class MediaEmbedUI extends Plugin {
 		} );
 	}
 
+	/**
+	 * @private
+	 * @param {module:ui/dropdown/dropdownview~DropdownView} dropdown
+	 * @param {module:ui/view~View} form
+	 * @param {module:media-embed/mediaembedcommand~MediaEmbedCommand} command
+	 */
 	_setUpDropdown( dropdown, form, command ) {
 		const editor = this.editor;
 		const t = editor.t;
@@ -71,6 +77,8 @@ export default class MediaEmbedUI extends Plugin {
 		// default action of the drop-down is executed (i.e. the panel showed up). Otherwise, the
 		// invisible form/input cannot be focused/selected.
 		button.on( 'open', () => {
+			form.disableCssTransitions();
+
 			// Make sure that each time the panel shows up, the URL field remains in sync with the value of
 			// the command. If the user typed in the input, then canceled (`urlInputView#fieldView#value` stays
 			// unaltered) and re-opened it without changing the value of the media command (e.g. because they
@@ -79,6 +87,7 @@ export default class MediaEmbedUI extends Plugin {
 			form.url = command.value || '';
 			form.urlInputView.fieldView.select();
 			form.focus();
+			form.enableCssTransitions();
 		}, { priority: 'low' } );
 
 		dropdown.on( 'submit', () => {
@@ -97,6 +106,12 @@ export default class MediaEmbedUI extends Plugin {
 		}
 	}
 
+	/**
+	 * @private
+	 * @param {module:ui/dropdown/dropdownview~DropdownView} dropdown
+	 * @param {module:ui/view~View} form
+	 * @param {module:media-embed/mediaembedcommand~MediaEmbedCommand} command
+	 */
 	_setUpForm( dropdown, form, command ) {
 		form.delegate( 'submit', 'cancel' ).to( dropdown );
 		form.urlInputView.bind( 'value' ).to( command, 'value' );

+ 3 - 1
packages/ckeditor5-media-embed/src/ui/mediaformview.js

@@ -19,6 +19,7 @@ import submitHandler from '@ckeditor/ckeditor5-ui/src/bindings/submithandler';
 import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker';
 import FocusCycler from '@ckeditor/ckeditor5-ui/src/focuscycler';
 import KeystrokeHandler from '@ckeditor/ckeditor5-utils/src/keystrokehandler';
+import injectCssTransitionDisabler from '@ckeditor/ckeditor5-ui/src/bindings/injectcsstransitiondisabler';
 
 import checkIcon from '@ckeditor/ckeditor5-core/theme/icons/check.svg';
 import cancelIcon from '@ckeditor/ckeditor5-core/theme/icons/cancel.svg';
@@ -147,6 +148,8 @@ export default class MediaFormView extends View {
 			]
 		} );
 
+		injectCssTransitionDisabler( this );
+
 		/**
 		 * The default info text for the {@link #urlInputView}.
 		 *
@@ -282,7 +285,6 @@ export default class MediaFormView extends View {
 
 		labeledInput.label = t( 'Media URL' );
 		labeledInput.infoText = this._urlInputViewInfoDefault;
-		inputField.placeholder = 'https://example.com';
 
 		inputField.on( 'input', () => {
 			// Display the tip text only when there's some value. Otherwise fall back to the default info text.

+ 9 - 11
packages/ckeditor5-media-embed/tests/automediaembed.js

@@ -11,8 +11,6 @@ import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictest
 import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import Link from '@ckeditor/ckeditor5-link/src/link';
-import List from '@ckeditor/ckeditor5-list/src/list';
-import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
 import Undo from '@ckeditor/ckeditor5-undo/src/undo';
 import Typing from '@ckeditor/ckeditor5-typing/src/typing';
 import Image from '@ckeditor/ckeditor5-image/src/image';
@@ -30,7 +28,7 @@ describe( 'AutoMediaEmbed - integration', () => {
 
 		return ClassicTestEditor
 			.create( editorElement, {
-				plugins: [ MediaEmbed, AutoMediaEmbed, Link, List, Bold, Typing, Image, ImageCaption ]
+				plugins: [ Typing, Paragraph, Link, Image, ImageCaption, MediaEmbed, AutoMediaEmbed ]
 			} )
 			.then( newEditor => {
 				editor = newEditor;
@@ -109,7 +107,7 @@ describe( 'AutoMediaEmbed - integration', () => {
 			);
 		} );
 
-		it( 'works for a full URL (https without "wwww" sub-domain)', () => {
+		it( 'works for a full URL (https without "www" sub-domain)', () => {
 			setData( editor.model, '<paragraph>[]</paragraph>' );
 			pasteHtml( editor, 'https://youtube.com/watch?v=H08tGjXNHO4' );
 
@@ -131,7 +129,7 @@ describe( 'AutoMediaEmbed - integration', () => {
 			);
 		} );
 
-		it( 'works for a full URL (http without "wwww" sub-domain)', () => {
+		it( 'works for a full URL (http without "www" sub-domain)', () => {
 			setData( editor.model, '<paragraph>[]</paragraph>' );
 			pasteHtml( editor, 'http://youtube.com/watch?v=H08tGjXNHO4' );
 
@@ -307,13 +305,13 @@ describe( 'AutoMediaEmbed - integration', () => {
 
 		it( 'does nothing if pasted a block of content that looks like a URL', () => {
 			setData( editor.model, '<paragraph>[]</paragraph>' );
-			pasteHtml( editor, '<ul><li>https://</li><li>youtube.com/watch?</li></ul><p>v=H08tGjXNHO4</p>' );
+			pasteHtml( editor, '<p>https://</p><p>youtube.com/watch?</p><p>v=H08tGjXNHO4</p>' );
 
 			clock.tick( 100 );
 
 			expect( getData( editor.model ) ).to.equal(
-				'<listItem listIndent="0" listType="bulleted">https://</listItem>' +
-				'<listItem listIndent="0" listType="bulleted">youtube.com/watch?</listItem>' +
+				'<paragraph>https://</paragraph>' +
+				'<paragraph>youtube.com/watch?</paragraph>' +
 				'<paragraph>v=H08tGjXNHO4[]</paragraph>'
 			);
 		} );
@@ -361,7 +359,7 @@ describe( 'AutoMediaEmbed - integration', () => {
 				editor.model,
 				'<paragraph>Foo. <$text linkHref="https://cksource.com">Bar</$text></paragraph>' +
 				'[<media url="https://open.spotify.com/album/2IXlgvecaDqOeF3viUZnPI?si=ogVw7KlcQAGZKK4Jz9QzvA"></media>]' +
-				'<paragraph><$text bold="true">Bar</$text>.</paragraph>'
+				'<paragraph>Bar.</paragraph>'
 			);
 
 			pasteHtml( editor, 'https://www.youtube.com/watch?v=H08tGjXNHO4' );
@@ -371,7 +369,7 @@ describe( 'AutoMediaEmbed - integration', () => {
 			expect( getData( editor.model ) ).to.equal(
 				'<paragraph>Foo. <$text linkHref="https://cksource.com">Bar</$text></paragraph>' +
 				'[<media url="https://www.youtube.com/watch?v=H08tGjXNHO4"></media>]' +
-				'<paragraph><$text bold="true">Bar</$text>.</paragraph>'
+				'<paragraph>Bar.</paragraph>'
 			);
 		} );
 
@@ -587,7 +585,7 @@ describe( 'AutoMediaEmbed - integration', () => {
 
 	it( 'should detach LiveRange', async () => {
 		const editor = await ClassicTestEditor.create( editorElement, {
-			plugins: [ MediaEmbed, AutoMediaEmbed, Link, List, Bold, Typing, Image, ImageCaption, Table ]
+			plugins: [ Typing, Paragraph, Link, Image, ImageCaption, Table, MediaEmbed, AutoMediaEmbed ]
 		} );
 
 		setData(

+ 10 - 0
packages/ckeditor5-media-embed/tests/mediaembedui.js

@@ -127,6 +127,16 @@ describe( 'MediaEmbedUI', () => {
 					button.fire( 'open' );
 					sinon.assert.calledOnce( spy );
 				} );
+
+				it( 'should disable CSS transitions to avoid unnecessary animations (and then enable them again)', () => {
+					const disableCssTransitionsSpy = sinon.spy( form, 'disableCssTransitions' );
+					const enableCssTransitionsSpy = sinon.spy( form, 'enableCssTransitions' );
+					const selectSpy = sinon.spy( form.urlInputView.fieldView, 'select' );
+
+					button.fire( 'open' );
+
+					sinon.assert.callOrder( disableCssTransitionsSpy, selectSpy, enableCssTransitionsSpy );
+				} );
 			} );
 		} );
 

+ 7 - 6
packages/ckeditor5-media-embed/tests/ui/mediaformview.js

@@ -78,11 +78,11 @@ describe( 'MediaFormView', () => {
 			expect( spy.calledOnce ).to.true;
 		} );
 
-		describe( 'url input view', () => {
-			it( 'has placeholder', () => {
-				expect( view.urlInputView.fieldView.placeholder ).to.equal( 'https://example.com' );
-			} );
+		it( 'should implement the CSS transition disabling feature', () => {
+			expect( view.disableCssTransitions ).to.be.a( 'function' );
+		} );
 
+		describe( 'url input view', () => {
 			it( 'has info text', () => {
 				expect( view.urlInputView.infoText ).to.match( /^Paste the media URL/ );
 			} );
@@ -122,9 +122,10 @@ describe( 'MediaFormView', () => {
 		} );
 
 		it( 'should register child views\' #element in #focusTracker', () => {
-			const spy = testUtils.sinon.spy( FocusTracker.prototype, 'add' );
-
 			view = new MediaFormView( [], { t: () => {} } );
+
+			const spy = testUtils.sinon.spy( view.focusTracker, 'add' );
+
 			view.render();
 
 			sinon.assert.calledWithExactly( spy.getCall( 0 ), view.urlInputView.element );

+ 96 - 7
packages/ckeditor5-paste-from-office/src/filters/list.js

@@ -56,13 +56,11 @@ export function transformListItemLikeElementsIntoLists( documentFragment, styles
 				const lastListItemChild = lastListItem.getChild( lastListItem.childCount - 1 );
 
 				currentList = insertNewEmptyList( listStyle, lastListItemChild, writer );
-
 				currentIndentation += 1;
 			} else if ( itemLikeElement.indent < currentIndentation ) {
 				const differentIndentation = currentIndentation - itemLikeElement.indent;
 
 				currentList = findParentListAtLevel( currentList, differentIndentation );
-
 				currentIndentation = parseInt( itemLikeElement.indent );
 			}
 
@@ -158,9 +156,9 @@ function findAllItemLikeElements( documentFragment, writer ) {
 // @param {String} stylesString CSS stylesheet.
 // @returns {Object} result
 // @returns {String} result.type List type, could be `ul` or `ol`.
-// @returns {String} result.style List style, for example: `decimal`, `lower-roman`, etc. It is extracted
-// directly from Word stylesheet without further processing and may be not compatible
-// with CSS `list-style-type` property accepted values.
+// @returns {String|null} result.style List style, for example: `decimal`, `lower-roman`, etc. It is extracted
+// directly from Word stylesheet and adjusted to represent proper values for the CSS `list-style-type` property.
+// If it cannot be adjusted, the `null` value is returned.
 function detectListStyle( listLikeItem, stylesString ) {
 	const listStyleRegexp = new RegExp( `@list l${ listLikeItem.id }:level${ listLikeItem.indent }\\s*({[^}]*)`, 'gi' );
 	const listStyleTypeRegex = /mso-level-number-format:([^;]*);/gi;
@@ -168,20 +166,105 @@ function detectListStyle( listLikeItem, stylesString ) {
 	const listStyleMatch = listStyleRegexp.exec( stylesString );
 
 	let listStyleType = 'decimal'; // Decimal is default one.
+	let type = 'ol'; // <ol> is default list.
+
 	if ( listStyleMatch && listStyleMatch[ 1 ] ) {
 		const listStyleTypeMatch = listStyleTypeRegex.exec( listStyleMatch[ 1 ] );
 
 		if ( listStyleTypeMatch && listStyleTypeMatch[ 1 ] ) {
 			listStyleType = listStyleTypeMatch[ 1 ].trim();
+			type = listStyleType !== 'bullet' && listStyleType !== 'image' ? 'ol' : 'ul';
+		}
+
+		// Styles for the numbered lists are always defined in Word CSS stylesheet.
+		// Unordered lists MAY contain a value for the Word CSS definition `mso-level-text` but sometimes
+		// the tag is missing. And because of that, we cannot depend on that. We need to predict the list style value based on
+		// the list style marker element.
+		if ( listStyleType === 'bullet' ) {
+			const bulletedStyle = findBulletedListStyle( listLikeItem.element );
+
+			if ( bulletedStyle ) {
+				listStyleType = bulletedStyle;
+			}
 		}
 	}
 
 	return {
-		type: listStyleType !== 'bullet' && listStyleType !== 'image' ? 'ol' : 'ul',
-		style: listStyleType
+		type,
+		style: mapListStyleDefinition( listStyleType )
 	};
 }
 
+// Tries extract the `list-style-type` value based on the marker element for bulleted list.
+//
+// @param {module:engine/view/element~Element} element
+// @returns {module:engine/view/element~Element|null}
+function findBulletedListStyle( element ) {
+	const listMarkerElement = findListMarkerNode( element );
+
+	if ( !listMarkerElement ) {
+		return null;
+	}
+
+	const listMarker = listMarkerElement._data;
+
+	if ( listMarker === 'o' ) {
+		return 'circle';
+	} else if ( listMarker === '·' ) {
+		return 'disc';
+	}
+	// Word returns '§' instead of '■' for the square list style.
+	else if ( listMarker === '§' ) {
+		return 'square';
+	}
+
+	return null;
+}
+
+// Tries to find a text node that represents the marker element (list-style-type).
+//
+// @param {module:engine/view/element~Element} element
+// @returns {module:engine/view/text~Text|null}
+function findListMarkerNode( element ) {
+	// If the first child is a text node, it is a value for the element.
+	if ( element.getChild( 0 ).is( '$text' ) ) {
+		return null;
+	}
+
+	const textNodeOrElement = element.getChild( 0 ).getChild( 0 );
+
+	if ( textNodeOrElement.is( '$text' ) ) {
+		return textNodeOrElement;
+	}
+
+	return textNodeOrElement.getChild( 0 );
+}
+
+// Parses the `list-style-type` value extracted directly from the Word CSS stylesheet and returns proper CSS definition.
+//
+// @param {String|null} value
+// @returns {String|null}
+function mapListStyleDefinition( value ) {
+	switch ( value ) {
+		case 'arabic-leading-zero':
+			return 'decimal-leading-zero';
+		case 'alpha-upper':
+			return 'upper-alpha';
+		case 'alpha-lower':
+			return 'lower-alpha';
+		case 'roman-upper':
+			return 'upper-roman';
+		case 'roman-lower':
+			return 'lower-roman';
+		case 'circle':
+		case 'disc':
+		case 'square':
+			return value;
+		default:
+			return null;
+	}
+}
+
 // Creates empty list of a given type and inserts it after a specified element.
 //
 // @param {Object} listStyle List style object which determines the type of newly created list.
@@ -197,6 +280,12 @@ function insertNewEmptyList( listStyle, element, writer ) {
 
 	writer.insertChild( position, list, parent );
 
+	// We do not support modifying the marker for particular list item.
+	// Set the value for the `list-style-type` property directly to the list container.
+	if ( listStyle.style ) {
+		writer.setStyle( 'list-style-type', listStyle.style, list );
+	}
+
 	return list;
 }
 

+ 2 - 2
packages/ckeditor5-paste-from-office/tests/_data/list/heading1/model.word2016.html

@@ -1,2 +1,2 @@
-<listItem listIndent="0" listType="numbered">H1 1</listItem>
-<listItem listIndent="0" listType="numbered">H1 2</listItem>
+<listItem listIndent="0" listStyle="default" listType="numbered">H1 1</listItem>
+<listItem listIndent="0" listStyle="default" listType="numbered">H1 2</listItem>

+ 2 - 2
packages/ckeditor5-paste-from-office/tests/_data/list/heading3-styled/model.word2016.html

@@ -1,5 +1,5 @@
-<listItem listIndent="0" listType="bulleted">H<$text bold="true">2 1</$text></listItem>
-<listItem listIndent="0" listType="bulleted">
+<listItem listIndent="0" listStyle="disc" listType="bulleted">H<$text bold="true">2 1</$text></listItem>
+<listItem listIndent="0" listStyle="disc" listType="bulleted">
 	<$text italic="true" underline="true">H</$text>
 	<$text underline="true">2</$text> 2
 </listItem>

+ 1 - 1
packages/ckeditor5-paste-from-office/tests/_data/list/heading3-styled/normalized.safari.word2016.html

@@ -1,4 +1,4 @@
-<ul>
+<ul style="list-style-type:disc">
 	<li style="margin-left:36.0pt;text-indent:-18.0pt;mso-list:l0 level1 lfo1">
 		<!--[if !supportLists]--><span style="font-family:Symbol;mso-fareast-font-family:Symbol;mso-bidi-font-family:
 	Symbol;mso-ansi-language:PL"></span><!--[endif]-->

+ 1 - 1
packages/ckeditor5-paste-from-office/tests/_data/list/heading3-styled/normalized.word2016.html

@@ -1,4 +1,4 @@
-<ul>
+<ul style="list-style-type:disc">
 	<li style='margin-left:36.0pt;text-indent:-18.0pt;mso-list:l0 level1 lfo1'>
 		<![if !supportLists]>
 		<span lang=PL style='font-family:Symbol;mso-fareast-font-family:Symbol;mso-bidi-font-family:Symbol;mso-ansi-language:PL'></span>

+ 1 - 1
packages/ckeditor5-paste-from-office/tests/_data/list/heading7/model.word2016.html

@@ -1 +1 @@
-<listItem listIndent="0" listType="numbered">H 7</listItem>
+<listItem listIndent="0" listStyle="default" listType="numbered">H 7</listItem>

+ 12 - 12
packages/ckeditor5-paste-from-office/tests/_data/list/many-one-item/model.word2016.html

@@ -1,21 +1,21 @@
-<listItem listIndent="0" listType="numbered">A</listItem>
-<listItem listIndent="0" listType="numbered">B</listItem>
-<listItem listIndent="0" listType="numbered">C</listItem>
-<listItem listIndent="0" listType="numbered">D</listItem>
-<listItem listIndent="0" listType="numbered">E</listItem>
-<listItem listIndent="0" listType="numbered">F</listItem>
-<listItem listIndent="0" listType="numbered">G</listItem>
+<listItem listIndent="0" listStyle="default" listType="numbered">A</listItem>
+<listItem listIndent="0" listStyle="default" listType="numbered">B</listItem>
+<listItem listIndent="0" listStyle="upper-roman" listType="numbered">C</listItem>
+<listItem listIndent="0" listStyle="upper-alpha" listType="numbered">D</listItem>
+<listItem listIndent="0" listStyle="lower-alpha" listType="numbered">E</listItem>
+<listItem listIndent="0" listStyle="lower-alpha" listType="numbered">F</listItem>
+<listItem listIndent="0" listStyle="lower-roman" listType="numbered">G</listItem>
 
 <paragraph></paragraph>
 
-<listItem listIndent="0" listType="bulleted">H</listItem>
-<listItem listIndent="0" listType="bulleted">I</listItem>
+<listItem listIndent="0" listStyle="disc" listType="bulleted">H</listItem>
+<listItem listIndent="0" listStyle="circle" listType="bulleted">I</listItem>
 
 <paragraph></paragraph>
 
-<listItem listIndent="0" listType="bulleted">J</listItem>
-<listItem listIndent="0" listType="bulleted">k</listItem>
+<listItem listIndent="0" listStyle="square" listType="bulleted">J</listItem>
+<listItem listIndent="0" listStyle="default" listType="bulleted">k</listItem>
 
 <paragraph></paragraph>
 
-<listItem listIndent="0" listType="numbered">h1</listItem>
+<listItem listIndent="0" listStyle="default" listType="numbered">h1</listItem>

+ 8 - 8
packages/ckeditor5-paste-from-office/tests/_data/list/many-one-item/normalized.safari.word2016.html

@@ -17,7 +17,7 @@
 	</li>
 </ol>
 
-<ol>
+<ol style="list-style-type:upper-roman">
 	<li class="MsoListParagraphCxSpMiddle" style="text-indent:-36.0pt;mso-text-indent-alt:
 	-18.0pt;mso-list:l7 level1 lfo3">
 		<!--[if !supportLists]--><span style="mso-fareast-font-family:
@@ -27,7 +27,7 @@
 	</li>
 </ol>
 
-<ol>
+<ol style="list-style-type:upper-alpha">
 	<li class="MsoListParagraphCxSpMiddle" style="text-indent:-18.0pt;mso-list:l10 level1 lfo4">
 		<!--[if !supportLists]--><span style="mso-fareast-font-family:Calibri;mso-fareast-theme-font:minor-latin;
 	mso-bidi-font-family:Calibri;mso-bidi-theme-font:minor-latin;mso-ansi-language:
@@ -36,7 +36,7 @@
 	</li>
 </ol>
 
-<ol>
+<ol style="list-style-type:lower-alpha">
 	<li class="MsoListParagraphCxSpMiddle" style="text-indent:-18.0pt;mso-list:l9 level1 lfo5">
 		<!--[if !supportLists]--><span style="mso-fareast-font-family:Calibri;mso-fareast-theme-font:minor-latin;
 	mso-bidi-font-family:Calibri;mso-bidi-theme-font:minor-latin;mso-ansi-language:
@@ -45,7 +45,7 @@
 	</li>
 </ol>
 
-<ol>
+<ol style="list-style-type:lower-alpha">
 	<li class="MsoListParagraphCxSpMiddle" style="text-indent:-18.0pt;mso-list:l4 level1 lfo6">
 		<!--[if !supportLists]--><span style="mso-fareast-font-family:Calibri;mso-fareast-theme-font:minor-latin;
 	mso-bidi-font-family:Calibri;mso-bidi-theme-font:minor-latin;mso-ansi-language:
@@ -54,7 +54,7 @@
 	</li>
 </ol>
 
-<ol>
+<ol style="list-style-type:lower-roman">
 	<li class="MsoListParagraphCxSpLast" style="text-indent:-36.0pt;mso-text-indent-alt:
 	-18.0pt;mso-list:l6 level1 lfo7">
 		<!--[if !supportLists]--><span style="mso-fareast-font-family:
@@ -66,7 +66,7 @@
 
 <p class="MsoNormal" style="margin: 0cm 0cm 8pt; line-height: 15.693333625793457px; font-size: 11pt; font-family: Calibri, sans-serif; caret-color: rgb(0, 0, 0); color: rgb(0, 0, 0); font-style: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: auto; word-spacing: 0px; -webkit-text-size-adjust: auto; -webkit-text-stroke-width: 0px; text-decoration: none;"><span></span></p>
 
-<ul>
+<ul style="list-style-type:disc">
 	<li class="MsoListParagraphCxSpFirst" style="text-indent:-18.0pt;mso-list:l1 level1 lfo8">
 		<!--[if !supportLists]--><span style="font-family:Symbol;mso-fareast-font-family:Symbol;mso-bidi-font-family:
 	Symbol;mso-ansi-language:PL"></span><!--[endif]-->
@@ -74,7 +74,7 @@
 	</li>
 </ul>
 
-<ul>
+<ul style="list-style-type:circle">
 	<li class="MsoListParagraphCxSpLast" style="text-indent:-18.0pt;mso-list:l3 level1 lfo9">
 		<!--[if !supportLists]--><span style="font-family:&quot;Courier New&quot;;mso-fareast-font-family:&quot;Courier New&quot;;
 	mso-ansi-language:PL"></span><!--[endif]-->
@@ -84,7 +84,7 @@
 
 <p class="MsoNormal" style="margin: 0cm 0cm 8pt; line-height: 15.693333625793457px; font-size: 11pt; font-family: Calibri, sans-serif; caret-color: rgb(0, 0, 0); color: rgb(0, 0, 0); font-style: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: auto; word-spacing: 0px; -webkit-text-size-adjust: auto; -webkit-text-stroke-width: 0px; text-decoration: none;"><span></span></p>
 
-<ul>
+<ul style="list-style-type:square">
 	<li class="MsoListParagraphCxSpFirst" style="text-indent:-18.0pt;mso-list:l8 level1 lfo10">
 		<!--[if !supportLists]--><span style="font-family:Wingdings;mso-fareast-font-family:Wingdings;mso-bidi-font-family:
 	Wingdings;mso-ansi-language:PL"></span><!--[endif]-->

+ 8 - 8
packages/ckeditor5-paste-from-office/tests/_data/list/many-one-item/normalized.word2016.html

@@ -16,7 +16,7 @@
 	</li>
 </ol>
 
-<ol>
+<ol style="list-style-type:upper-roman">
 	<li class=MsoListParagraphCxSpMiddle style='text-indent:-36.0pt;mso-text-indent-alt:-18.0pt;mso-list:l7 level1 lfo3'>
 		<![if !supportLists]>
 		<span lang=PL style='mso-fareast-font-family:Calibri;mso-fareast-theme-font:minor-latin;mso-bidi-font-family:Calibri;mso-bidi-theme-font:minor-latin;mso-ansi-language:PL'></span>
@@ -25,7 +25,7 @@
 	</li>
 </ol>
 
-<ol>
+<ol style="list-style-type:upper-alpha">
 	<li class=MsoListParagraphCxSpMiddle style='text-indent:-18.0pt;mso-list:l10 level1 lfo4'>
 		<![if !supportLists]>
 		<span lang=PL style='mso-fareast-font-family:Calibri;mso-fareast-theme-font:minor-latin;mso-bidi-font-family:Calibri;mso-bidi-theme-font:minor-latin;mso-ansi-language:PL'></span>
@@ -34,7 +34,7 @@
 	</li>
 </ol>
 
-<ol>
+<ol style="list-style-type:lower-alpha">
 	<li class=MsoListParagraphCxSpMiddle style='text-indent:-18.0pt;mso-list:l9 level1 lfo5'>
 		<![if !supportLists]>
 		<span lang=PL style='mso-fareast-font-family:Calibri;mso-fareast-theme-font:minor-latin;mso-bidi-font-family:Calibri;mso-bidi-theme-font:minor-latin;mso-ansi-language:PL'></span>
@@ -43,7 +43,7 @@
 	</li>
 </ol>
 
-<ol>
+<ol style="list-style-type:lower-alpha">
 	<li class=MsoListParagraphCxSpMiddle style='text-indent:-18.0pt;mso-list:l4 level1 lfo6'>
 		<![if !supportLists]>
 		<span lang=PL style='mso-fareast-font-family:Calibri;mso-fareast-theme-font:minor-latin;mso-bidi-font-family:Calibri;mso-bidi-theme-font:minor-latin;mso-ansi-language:PL'></span>
@@ -52,7 +52,7 @@
 	</li>
 </ol>
 
-<ol>
+<ol style="list-style-type:lower-roman">
 	<li class=MsoListParagraphCxSpLast style='text-indent:-36.0pt;mso-text-indent-alt:-18.0pt;mso-list:l6 level1 lfo7'>
 		<![if !supportLists]>
 		<span lang=PL style='mso-fareast-font-family:Calibri;mso-fareast-theme-font:minor-latin;mso-bidi-font-family:Calibri;mso-bidi-theme-font:minor-latin;mso-ansi-language:PL'></span>
@@ -63,7 +63,7 @@
 
 <p class=MsoNormal><span lang=PL style='mso-ansi-language:PL'></span></p>
 
-<ul>
+<ul style="list-style-type:disc">
 	<li class=MsoListParagraphCxSpFirst style='text-indent:-18.0pt;mso-list:l1 level1 lfo8'>
 		<![if !supportLists]>
 		<span lang=PL style='font-family:Symbol;mso-fareast-font-family:Symbol;mso-bidi-font-family:Symbol;mso-ansi-language:PL'></span>
@@ -72,7 +72,7 @@
 	</li>
 </ul>
 
-<ul>
+<ul style="list-style-type:circle">
 	<li class=MsoListParagraphCxSpLast style='text-indent:-18.0pt;mso-list:l3 level1 lfo9'>
 		<![if !supportLists]>
 		<span lang=PL style='font-family:"Courier New";mso-fareast-font-family:"Courier New";mso-ansi-language:PL'></span>
@@ -83,7 +83,7 @@
 
 <p class=MsoNormal><span lang=PL style='mso-ansi-language:PL'></span></p>
 
-<ul>
+<ul style="list-style-type:square">
 	<li class=MsoListParagraphCxSpFirst style='text-indent:-18.0pt;mso-list:l8 level1 lfo10'>
 		<![if !supportLists]>
 		<span lang=PL style='font-family:Wingdings;mso-fareast-font-family:Wingdings;mso-bidi-font-family:Wingdings;mso-ansi-language:PL'></span>

+ 4 - 4
packages/ckeditor5-paste-from-office/tests/_data/list/multiple-combined/model.word2016.html

@@ -1,4 +1,4 @@
-<listItem listIndent="0" listType="numbered">Item1</listItem>
-<listItem listIndent="0" listType="numbered">Item 2</listItem>
-<listItem listIndent="0" listType="numbered">Item 1</listItem>
-<listItem listIndent="0" listType="numbered">Item2</listItem>
+<listItem listIndent="0" listStyle="default" listType="numbered">Item1</listItem>
+<listItem listIndent="0" listStyle="default" listType="numbered">Item 2</listItem>
+<listItem listIndent="0" listStyle="default" listType="numbered">Item 1</listItem>
+<listItem listIndent="0" listStyle="default" listType="numbered">Item2</listItem>

+ 3 - 3
packages/ckeditor5-paste-from-office/tests/_data/list/multiple/model.word2016.html

@@ -1,6 +1,6 @@
-<listItem listIndent="0" listType="numbered">Item1</listItem>
-<listItem listIndent="0" listType="numbered">Item 2</listItem>
+<listItem listIndent="0" listStyle="default" listType="numbered">Item1</listItem>
+<listItem listIndent="0" listStyle="default" listType="numbered">Item 2</listItem>
 
 <paragraph>Some text</paragraph>
 
-<listItem listIndent="0" listType="bulleted">Bullet 1</listItem>
+<listItem listIndent="0" listStyle="disc" listType="bulleted">Bullet 1</listItem>

+ 1 - 1
packages/ckeditor5-paste-from-office/tests/_data/list/multiple/normalized.safari.word2016.html

@@ -16,7 +16,7 @@
 
 <p class="MsoNormal" style="margin: 0cm 0cm 8pt; line-height: 15.693333625793457px; font-size: 11pt; font-family: Calibri, sans-serif; caret-color: rgb(0, 0, 0); color: rgb(0, 0, 0); font-style: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: auto; word-spacing: 0px; -webkit-text-size-adjust: auto; -webkit-text-stroke-width: 0px; text-decoration: none;"><span>Some text<o:p></o:p></span></p>
 
-<ul>
+<ul style="list-style-type:disc">
 	<li class="MsoListParagraph" style="text-indent:-18.0pt;mso-list:l0 level1 lfo2">
 		<!--[if !supportLists]--><span style="font-family:Symbol;mso-fareast-font-family:Symbol;mso-bidi-font-family:
 	Symbol;mso-ansi-language:PL"></span><!--[endif]-->

+ 1 - 1
packages/ckeditor5-paste-from-office/tests/_data/list/multiple/normalized.word2016.html

@@ -16,7 +16,7 @@
 
 <p class=MsoNormal><span lang=PL style='mso-ansi-language:PL'>Some text<o:p></o:p></span></p>
 
-<ul>
+<ul style="list-style-type:disc">
 	<li class=MsoListParagraph style='text-indent:-18.0pt;mso-list:l0 level1 lfo2'>
 		<![if !supportLists]>
 		<span lang=PL style='font-family:Symbol;mso-fareast-font-family:Symbol;mso-bidi-font-family:Symbol;mso-ansi-language:PL'></span>

+ 6 - 6
packages/ckeditor5-paste-from-office/tests/_data/list/nested-mixed/model.word2016.html

@@ -1,6 +1,6 @@
-<listItem listIndent="0" listType="bulleted">A1</listItem>
-<listItem listIndent="1" listType="numbered">B2</listItem>
-<listItem listIndent="2" listType="numbered">C4</listItem>
-<listItem listIndent="2" listType="numbered">D3</listItem>
-<listItem listIndent="0" listType="bulleted">E1</listItem>
-<listItem listIndent="1" listType="numbered">F2</listItem>
+<listItem listIndent="0" listStyle="disc" listType="bulleted">A1</listItem>
+<listItem listIndent="1" listStyle="default" listType="numbered">B2</listItem>
+<listItem listIndent="2" listStyle="disc" listType="numbered">C4</listItem>
+<listItem listIndent="2" listStyle="disc" listType="numbered">D3</listItem>
+<listItem listIndent="0" listStyle="disc" listType="bulleted">E1</listItem>
+<listItem listIndent="1" listStyle="default" listType="numbered">F2</listItem>

+ 2 - 2
packages/ckeditor5-paste-from-office/tests/_data/list/nested-mixed/normalized.word2016.html

@@ -1,10 +1,10 @@
-<ul>
+<ul style="list-style-type:disc">
 	<li class="MsoListParagraphCxSpFirst" style="mso-list:l0 level1 lfo1;text-indent:-18.0pt">
 		<span style="font-family:Symbol;mso-bidi-font-family:Symbol;mso-fareast-font-family:Symbol"></span>A1<o:p></o:p>
 		<ol>
 			<li class="MsoListParagraphCxSpMiddle" style="margin-left:72.0pt;mso-add-space:auto;mso-list:l0 level2 lfo1;text-indent:-18.0pt">
 				<span style="mso-bidi-font-family:Calibri;mso-bidi-theme-font:minor-latin;mso-fareast-font-family:Calibri;mso-fareast-theme-font:minor-latin"></span>B2<o:p></o:p>
-				<ol>
+				<ol style="list-style-type:disc">
 					
 					<li class="MsoListParagraphCxSpMiddle" style="margin-left:144.0pt;mso-add-space:auto;mso-list:l0 level4 lfo1;text-indent:-18.0pt">
 						<span style="font-family:Symbol;mso-bidi-font-family:Symbol;mso-fareast-font-family:Symbol"></span>C4<o:p></o:p>

+ 9 - 9
packages/ckeditor5-paste-from-office/tests/_data/list/nested-multiple/model.word2016.html

@@ -1,17 +1,17 @@
-<listItem listIndent="0" listType="bulleted">A1</listItem>
-<listItem listIndent="1" listType="numbered">B3</listItem>
-<listItem listIndent="1" listType="numbered">C2</listItem>
+<listItem listIndent="0" listStyle="disc" listType="bulleted">A1</listItem>
+<listItem listIndent="1" listStyle="square" listType="numbered">B3</listItem>
+<listItem listIndent="1" listStyle="square" listType="numbered">C2</listItem>
 
 <paragraph></paragraph>
 <paragraph>Foo Bar...</paragraph>
 <paragraph></paragraph>
 
-<listItem listIndent="0" listType="numbered">A2</listItem>
-<listItem listIndent="0" listType="numbered">B1</listItem>
-<listItem listIndent="1" listType="numbered">C2</listItem>
+<listItem listIndent="0" listStyle="lower-alpha" listType="numbered">A2</listItem>
+<listItem listIndent="0" listStyle="lower-alpha" listType="numbered">B1</listItem>
+<listItem listIndent="1" listStyle="lower-alpha" listType="numbered">C2</listItem>
 
 <paragraph></paragraph>
 
-<listItem listIndent="0" listType="bulleted">A1</listItem>
-<listItem listIndent="1" listType="numbered">B2</listItem>
-<listItem listIndent="0" listType="bulleted">C1</listItem>
+<listItem listIndent="0" listStyle="disc" listType="bulleted">A1</listItem>
+<listItem listIndent="1" listStyle="default" listType="numbered">B2</listItem>
+<listItem listIndent="0" listStyle="disc" listType="bulleted">C1</listItem>

+ 6 - 6
packages/ckeditor5-paste-from-office/tests/_data/list/nested-multiple/normalized.safari.word2016.html

@@ -1,7 +1,7 @@
-<ul>
+<ul style="list-style-type:disc">
 	<li class="MsoListParagraphCxSpFirst" style="mso-list:l1 level1 lfo2;text-indent:-18.0pt">
 		<span style="font-family:Symbol;mso-bidi-font-family:Symbol;mso-fareast-font-family:Symbol"></span>A1<o:p></o:p>
-		<ol>
+		<ol style="list-style-type:square">
 			
           <li class="MsoListParagraphCxSpMiddle" style="margin-left:108.0pt;mso-add-space:auto;mso-list:l1 level3 lfo2;text-indent:-18.0pt">
             <span style="font-family:Wingdings;mso-bidi-font-family:Wingdings;mso-fareast-font-family:Wingdings"></span>B3<o:p></o:p>
@@ -18,14 +18,14 @@
 <p class="MsoNormal" style="-webkit-text-size-adjust:auto;-webkit-text-stroke-width:0px;caret-color:rgb(0, 0, 0);color:rgb(0, 0, 0);font-family:Calibri, sans-serif;font-size:medium;font-style:normal;font-variant-caps:normal;font-weight:normal;letter-spacing:normal;margin:0cm 0cm 0.0001pt;orphans:auto;text-align:start;text-decoration:none;text-indent:0px;text-transform:none;white-space:normal;widows:auto;word-spacing:0px">Foo Bar...<o:p></o:p></p>
 <p class="MsoNormal" style="-webkit-text-size-adjust:auto;-webkit-text-stroke-width:0px;caret-color:rgb(0, 0, 0);color:rgb(0, 0, 0);font-family:Calibri, sans-serif;font-size:medium;font-style:normal;font-variant-caps:normal;font-weight:normal;letter-spacing:normal;margin:0cm 0cm 0.0001pt;orphans:auto;text-align:start;text-decoration:none;text-indent:0px;text-transform:none;white-space:normal;widows:auto;word-spacing:0px"></p>
 
-<ol>
+<ol style="list-style-type:lower-alpha">
   <li class="MsoListParagraphCxSpFirst" style="margin-left:72.0pt;mso-add-space:auto;mso-list:l0 level2 lfo1;text-indent:-18.0pt">
     <span style="mso-bidi-font-family:Calibri;mso-bidi-theme-font:minor-latin;mso-fareast-font-family:Calibri;mso-fareast-theme-font:minor-latin"></span>A2<o:p></o:p>
   </li>
 
     <li class="MsoListParagraphCxSpMiddle" style="mso-list:l0 level1 lfo1;text-indent:-18.0pt">
 		<span style="mso-bidi-font-family:Calibri;mso-bidi-theme-font:minor-latin;mso-fareast-font-family:Calibri;mso-fareast-theme-font:minor-latin"></span>B1<o:p></o:p>
-		<ol>
+		<ol style="list-style-type:lower-alpha">
 			<li class="MsoListParagraphCxSpLast" style="margin-left:72.0pt;mso-add-space:auto;mso-list:l0 level2 lfo1;text-indent:-18.0pt">
 				<span style="mso-bidi-font-family:Calibri;mso-bidi-theme-font:minor-latin;mso-fareast-font-family:Calibri;mso-fareast-theme-font:minor-latin"></span>C2<o:p></o:p>
 			</li>
@@ -35,7 +35,7 @@
 
 <p class="MsoNormal" style="-webkit-text-size-adjust:auto;-webkit-text-stroke-width:0px;caret-color:rgb(0, 0, 0);color:rgb(0, 0, 0);font-family:Calibri, sans-serif;font-size:medium;font-style:normal;font-variant-caps:normal;font-weight:normal;letter-spacing:normal;margin:0cm 0cm 0.0001pt;orphans:auto;text-align:start;text-decoration:none;text-indent:0px;text-transform:none;white-space:normal;widows:auto;word-spacing:0px"></p>
 
-<ul>
+<ul style="list-style-type:disc">
 	<li class="MsoListParagraphCxSpFirst" style="mso-list:l1 level1 lfo2;text-indent:-18.0pt">
 		<span style="font-family:Symbol;mso-bidi-font-family:Symbol;mso-fareast-font-family:Symbol"></span>A1<o:p></o:p>
 		<ol>
@@ -48,4 +48,4 @@
 	<li class="MsoListParagraphCxSpLast" style="mso-list:l1 level1 lfo2;text-indent:-18.0pt">
 		<span style="font-family:Symbol;mso-bidi-font-family:Symbol;mso-fareast-font-family:Symbol"></span>C1<o:p></o:p>
 	</li>
-</ul>
+</ol>

+ 5 - 5
packages/ckeditor5-paste-from-office/tests/_data/list/nested-multiple/normalized.word2016.html

@@ -1,7 +1,7 @@
-<ul>
+<ul style="list-style-type:disc">
 	<li class="MsoListParagraphCxSpFirst" style="mso-list:l1 level1 lfo2;text-indent:-18.0pt">
 		<span style="font-family:Symbol;mso-bidi-font-family:Symbol;mso-fareast-font-family:Symbol"></span>A1<o:p></o:p>
-		<ol>
+		<ol style="list-style-type:square">
           <li class="MsoListParagraphCxSpMiddle" style="margin-left:108.0pt;mso-add-space:auto;mso-list:l1 level3 lfo2;text-indent:-18.0pt">
             <span style="font-family:Wingdings;mso-bidi-font-family:Wingdings;mso-fareast-font-family:Wingdings"></span>B3<o:p></o:p>
           </li>
@@ -17,14 +17,14 @@
 <p class="MsoNormal">Foo Bar...<o:p></o:p></p>
 <p class="MsoNormal"></p>
 
-<ol>
+<ol style="list-style-type:lower-alpha">
   <li class="MsoListParagraphCxSpFirst" style="margin-left:72.0pt;mso-add-space:auto;mso-list:l0 level2 lfo1;text-indent:-18.0pt">
     <span style="mso-bidi-font-family:Calibri;mso-bidi-theme-font:minor-latin;mso-fareast-font-family:Calibri;mso-fareast-theme-font:minor-latin"></span>A2<o:p></o:p>
   </li>
 
     <li class="MsoListParagraphCxSpMiddle" style="mso-list:l0 level1 lfo1;text-indent:-18.0pt">
 		<span style="mso-bidi-font-family:Calibri;mso-bidi-theme-font:minor-latin;mso-fareast-font-family:Calibri;mso-fareast-theme-font:minor-latin"></span>B1<o:p></o:p>
-		<ol>
+		<ol style="list-style-type:lower-alpha">
 			<li class="MsoListParagraphCxSpLast" style="margin-left:72.0pt;mso-add-space:auto;mso-list:l0 level2 lfo1;text-indent:-18.0pt">
 				<span style="mso-bidi-font-family:Calibri;mso-bidi-theme-font:minor-latin;mso-fareast-font-family:Calibri;mso-fareast-theme-font:minor-latin"></span>C2<o:p></o:p>
 			</li>
@@ -34,7 +34,7 @@
 
 <p class="MsoNormal"></p>
 
-<ul>
+<ul style="list-style-type:disc">
 	<li class="MsoListParagraphCxSpFirst" style="mso-list:l1 level1 lfo2;text-indent:-18.0pt">
 		<span style="font-family:Symbol;mso-bidi-font-family:Symbol;mso-fareast-font-family:Symbol"></span>A1<o:p></o:p>
 		<ol>

+ 7 - 7
packages/ckeditor5-paste-from-office/tests/_data/list/nested/model.word2016.html

@@ -1,7 +1,7 @@
-<listItem listIndent="0" listType="numbered">A1</listItem>
-<listItem listIndent="0" listType="numbered">B1</listItem>
-<listItem listIndent="1" listType="numbered">C2</listItem>
-<listItem listIndent="2" listType="numbered">D4</listItem>
-<listItem listIndent="1" listType="numbered">E2</listItem>
-<listItem listIndent="2" listType="numbered">F3</listItem>
-<listItem listIndent="0" listType="numbered">G1</listItem>
+<listItem listIndent="0" listStyle="default" listType="numbered">A1</listItem>
+<listItem listIndent="0" listStyle="default" listType="numbered">B1</listItem>
+<listItem listIndent="1" listStyle="lower-alpha" listType="numbered">C2</listItem>
+<listItem listIndent="2" listStyle="default" listType="numbered">D4</listItem>
+<listItem listIndent="1" listStyle="lower-alpha" listType="numbered">E2</listItem>
+<listItem listIndent="2" listStyle="lower-roman" listType="numbered">F3</listItem>
+<listItem listIndent="0" listStyle="default" listType="numbered">G1</listItem>

+ 2 - 2
packages/ckeditor5-paste-from-office/tests/_data/list/nested/normalized.word2016.html

@@ -5,7 +5,7 @@
 
 	<li class="MsoListParagraphCxSpMiddle" style="mso-list:l0 level1 lfo1;text-indent:-18.0pt">
 		<span style="mso-bidi-font-family:Calibri;mso-bidi-theme-font:minor-latin;mso-fareast-font-family:Calibri;mso-fareast-theme-font:minor-latin"></span>B1<o:p></o:p>
-		<ol>
+		<ol style="list-style-type:lower-alpha">
 			<li class="MsoListParagraphCxSpMiddle" style="margin-left:72.0pt;mso-add-space:auto;mso-list:l0 level2 lfo1;text-indent:-18.0pt">
 				<span style="mso-bidi-font-family:Calibri;mso-bidi-theme-font:minor-latin;mso-fareast-font-family:Calibri;mso-fareast-theme-font:minor-latin"></span>C2<o:p></o:p>
 				<ol>
@@ -17,7 +17,7 @@
 
 			<li class="MsoListParagraphCxSpMiddle" style="margin-left:72.0pt;mso-add-space:auto;mso-list:l0 level2 lfo1;text-indent:-18.0pt">
 				<span style="mso-bidi-font-family:Calibri;mso-bidi-theme-font:minor-latin;mso-fareast-font-family:Calibri;mso-fareast-theme-font:minor-latin"></span>E2<o:p></o:p>
-				<ol>
+				<ol style="list-style-type:lower-roman">
 					<li class="MsoListParagraphCxSpMiddle" style="margin-left:108.0pt;mso-add-space:auto;mso-list:l0 level3 lfo1;mso-text-indent-alt:-9.0pt;text-indent:-108.0pt">
 						<span style="mso-bidi-font-family:Calibri;mso-bidi-theme-font:minor-latin;mso-fareast-font-family:Calibri;mso-fareast-theme-font:minor-latin"></span>F3<o:p></o:p>
 					</li>

+ 12 - 12
packages/ckeditor5-paste-from-office/tests/_data/list/resume-template/model.safari.word2016.html

@@ -10,40 +10,40 @@
 
 <heading1>DEGREE | DATE EARNED | SCHOOL</heading1>
 
-<listItem listIndent="0" listType="bulleted">Major: Click here to enter text</listItem>
-<listItem listIndent="0" listType="bulleted">Minor: Click here to enter text</listItem>
-<listItem listIndent="0" listType="bulleted">Related coursework: Click here to enter text</listItem>
+<listItem listIndent="0" listStyle="disc" listType="bulleted">Major: Click here to enter text</listItem>
+<listItem listIndent="0" listStyle="disc" listType="bulleted">Minor: Click here to enter text</listItem>
+<listItem listIndent="0" listStyle="disc" listType="bulleted">Related coursework: Click here to enter text</listItem>
 
 <heading1>DEGREE | DATE EARNED | SCHOOL</heading1>
 
-<listItem listIndent="0" listType="bulleted">Major: Click here to enter text</listItem>
-<listItem listIndent="0" listType="bulleted">Minor: Click here to enter text</listItem>
-<listItem listIndent="0" listType="bulleted">Related coursework: Click here to enter text</listItem>
+<listItem listIndent="0" listStyle="disc" listType="bulleted">Major: Click here to enter text</listItem>
+<listItem listIndent="0" listStyle="disc" listType="bulleted">Minor: Click here to enter text</listItem>
+<listItem listIndent="0" listStyle="disc" listType="bulleted">Related coursework: Click here to enter text</listItem>
 
 <heading1>Skills & Abilities</heading1>
 
 <heading1>MANAGEMENT</heading1>
 
-<listItem listIndent="0" listType="bulleted">Think a document that looks this good has to be difficult to format? Think again! To easily apply any text formatting you see in this document with just a click, on the Home tab of the ribbon, check out Styles.</listItem>
+<listItem listIndent="0" listStyle="disc" listType="bulleted">Think a document that looks this good has to be difficult to format? Think again! To easily apply any text formatting you see in this document with just a click, on the Home tab of the ribbon, check out Styles.</listItem>
 
 <heading1>SALES</heading1>
 
-<listItem listIndent="0" listType="bulleted">Some of the sample text in this document indicates the name of the style applied, so that you can easily apply the same formatting again. For example, this is the List Bullet style.</listItem>
+<listItem listIndent="0" listStyle="disc" listType="bulleted">Some of the sample text in this document indicates the name of the style applied, so that you can easily apply the same formatting again. For example, this is the List Bullet style.</listItem>
 
 <heading1>COMMUNICATION</heading1>
 
-<listItem listIndent="0" listType="bulleted">You delivered that big presentation to rave reviews. Don’t be shy about it now! This is the place to show how well you work and play with others.</listItem>
+<listItem listIndent="0" listStyle="disc" listType="bulleted">You delivered that big presentation to rave reviews. Don’t be shy about it now! This is the place to show how well you work and play with others.</listItem>
 
 <heading1>LEADERSHIP</heading1>
 
-<listItem listIndent="0" listType="bulleted">Are you president of your fraternity, head of the condo board, or a team lead for your favorite charity? You’re a natural leader—tell it like it is!</listItem>
+<listItem listIndent="0" listStyle="disc" listType="bulleted">Are you president of your fraternity, head of the condo board, or a team lead for your favorite charity? You’re a natural leader—tell it like it is!</listItem>
 
 <heading1>Experience</heading1>
 
 <heading1>JOB TITLE | COMPANY | DATES FROM - TO</heading1>
 
-<listItem listIndent="0" listType="bulleted">This is the place for a brief summary of your key responsibilities and most stellar accomplishments.</listItem>
+<listItem listIndent="0" listStyle="disc" listType="bulleted">This is the place for a brief summary of your key responsibilities and most stellar accomplishments.</listItem>
 
 <heading1>JOB TITLE | COMPANY | DATES FROM - TO</heading1>
 
-<listItem listIndent="0" listType="bulleted">This is the place for a brief summary of your key responsibilities and most stellar accomplishments.</listItem>
+<listItem listIndent="0" listStyle="disc" listType="bulleted">This is the place for a brief summary of your key responsibilities and most stellar accomplishments.</listItem>

+ 12 - 12
packages/ckeditor5-paste-from-office/tests/_data/list/resume-template/model.word2016.html

@@ -10,40 +10,40 @@
 
 <heading1>Degree | Date Earned | School</heading1>
 
-<listItem listIndent="0" listType="bulleted">Major: Click here to enter text</listItem>
-<listItem listIndent="0" listType="bulleted">Minor: Click here to enter text</listItem>
-<listItem listIndent="0" listType="bulleted">Related coursework: Click here to enter text</listItem>
+<listItem listIndent="0" listStyle="disc" listType="bulleted">Major: Click here to enter text</listItem>
+<listItem listIndent="0" listStyle="disc" listType="bulleted">Minor: Click here to enter text</listItem>
+<listItem listIndent="0" listStyle="disc" listType="bulleted">Related coursework: Click here to enter text</listItem>
 
 <heading1>Degree | Date Earned | School</heading1>
 
-<listItem listIndent="0" listType="bulleted">Major: Click here to enter text</listItem>
-<listItem listIndent="0" listType="bulleted">Minor: Click here to enter text</listItem>
-<listItem listIndent="0" listType="bulleted">Related coursework: Click here to enter text</listItem>
+<listItem listIndent="0" listStyle="disc" listType="bulleted">Major: Click here to enter text</listItem>
+<listItem listIndent="0" listStyle="disc" listType="bulleted">Minor: Click here to enter text</listItem>
+<listItem listIndent="0" listStyle="disc" listType="bulleted">Related coursework: Click here to enter text</listItem>
 
 <heading1>Skills & Abilities</heading1>
 
 <heading1>Management</heading1>
 
-<listItem listIndent="0" listType="bulleted">Think a document that looks this good has to be difficult to format? Think again! To easily apply any text formatting you see in this document with just a click, on the Home tab of the ribbon, check out Styles.</listItem>
+<listItem listIndent="0" listStyle="disc" listType="bulleted">Think a document that looks this good has to be difficult to format? Think again! To easily apply any text formatting you see in this document with just a click, on the Home tab of the ribbon, check out Styles.</listItem>
 
 <heading1>Sales</heading1>
 
-<listItem listIndent="0" listType="bulleted">Some of the sample text in this document indicates the name of the style applied, so that you can easily apply the same formatting again. For example, this is the List Bullet style.</listItem>
+<listItem listIndent="0" listStyle="disc" listType="bulleted">Some of the sample text in this document indicates the name of the style applied, so that you can easily apply the same formatting again. For example, this is the List Bullet style.</listItem>
 
 <heading1>Communication</heading1>
 
-<listItem listIndent="0" listType="bulleted">You delivered that big presentation to rave reviews. Don’t be shy about it now! This is the place to show how well you work and play with others.</listItem>
+<listItem listIndent="0" listStyle="disc" listType="bulleted">You delivered that big presentation to rave reviews. Don’t be shy about it now! This is the place to show how well you work and play with others.</listItem>
 
 <heading1>Leadership</heading1>
 
-<listItem listIndent="0" listType="bulleted">Are you president of your fraternity, head of the condo board, or a team lead for your favorite charity? You’re a natural leader—tell it like it is!</listItem>
+<listItem listIndent="0" listStyle="disc" listType="bulleted">Are you president of your fraternity, head of the condo board, or a team lead for your favorite charity? You’re a natural leader—tell it like it is!</listItem>
 
 <heading1>Experience</heading1>
 
 <heading1>Job Title | Company | Dates From - To</heading1>
 
-<listItem listIndent="0" listType="bulleted">This is the place for a brief summary of your key responsibilities and most stellar accomplishments.</listItem>
+<listItem listIndent="0" listStyle="disc" listType="bulleted">This is the place for a brief summary of your key responsibilities and most stellar accomplishments.</listItem>
 
 <heading1>Job Title | Company | Dates From - To</heading1>
 
-<listItem listIndent="0" listType="bulleted">This is the place for a brief summary of your key responsibilities and most stellar accomplishments.</listItem>
+<listItem listIndent="0" listStyle="disc" listType="bulleted">This is the place for a brief summary of your key responsibilities and most stellar accomplishments.</listItem>

+ 8 - 8
packages/ckeditor5-paste-from-office/tests/_data/list/resume-template/normalized.safari.word2016.html

@@ -45,7 +45,7 @@
 	</span>
 </h2>
 
-<ul>
+<ul style="list-style-type:disc">
 	<li class="MsoListBulletCxSpFirst" style="mso-list:l0 level1 lfo1">
 		<span lang="EN-US" style="mso-ascii-font-family:Cambria;mso-bidi-font-family:Cambria;mso-fareast-font-family:Cambria;mso-hansi-font-family:Cambria"></span>
 		<span lang="EN-US">Major: <w:sdt docpart="1EDC150D3B06E54E95D71F92040E448A" id="1821224400" sdttag="Major:" showingplchdr="t" temporary="t" text="t" title="Major:">Click here to enter text</w:sdt><o:p></o:p></span>
@@ -68,7 +68,7 @@
 	</span>
 </h2>
 
-<ul>
+<ul style="list-style-type:disc">
 	<li class="MsoListBulletCxSpFirst" style="mso-list:l0 level1 lfo1">
 		<span lang="EN-US" style="mso-ascii-font-family:Cambria;mso-bidi-font-family:Cambria;mso-fareast-font-family:Cambria;mso-hansi-font-family:Cambria"></span>
 		<span lang="EN-US">Major: <w:sdt docpart="9EAA648D6489514EB79336B1A935D9AC" id="1046181329" sdttag="Major:" showingplchdr="t" temporary="t" text="t" title="Major:">Click here to enter text</w:sdt><o:p></o:p></span>
@@ -98,7 +98,7 @@
 </w:sdt>
 
 <w:sdt docpart="07C08B1CF5695F41BAB584E7E2D2D094" id="-1177730712" sdttag="Management Skills:" showingplchdr="t" style="-webkit-text-size-adjust:auto;-webkit-text-stroke-width:0px;caret-color:rgb(0, 0, 0);color:rgb(0, 0, 0);font-family:-webkit-standard;font-style:normal;font-variant-caps:normal;font-weight:normal;letter-spacing:normal;orphans:auto;text-align:start;text-decoration:none;text-indent:0px;text-transform:none;white-space:normal;widows:auto;word-spacing:0px" temporary="t" title="Management Skills:">
-	<ul>
+	<ul style="list-style-type:disc">
 		<li class="MsoListBullet" style="mso-list:l0 level1 lfo1">
 			<span lang="EN-US" style="mso-ascii-font-family:Cambria;mso-bidi-font-family:Cambria;mso-fareast-font-family:Cambria;mso-hansi-font-family:Cambria"></span>
 			<span lang="EN-US">Think a document that looks this good has to be difficult to format? Think again! To easily apply any text formatting you see in this document with just a click, on the Home tab of the ribbon, check out Styles.<o:p></o:p><w:sdtpr></w:sdtpr></span>
@@ -113,7 +113,7 @@
 </w:sdt>
 
 <w:sdt docpart="952E9B3BDFFE494DA37978C6F4D15566" id="1544489962" sdttag="Sales Skills:" showingplchdr="t" style="-webkit-text-size-adjust:auto;-webkit-text-stroke-width:0px;caret-color:rgb(0, 0, 0);color:rgb(0, 0, 0);font-family:-webkit-standard;font-style:normal;font-variant-caps:normal;font-weight:normal;letter-spacing:normal;orphans:auto;text-align:start;text-decoration:none;text-indent:0px;text-transform:none;white-space:normal;widows:auto;word-spacing:0px" temporary="t" title="Sales Skills:">
-	<ul>
+	<ul style="list-style-type:disc">
 		<li class="MsoListBullet" style="mso-list:l0 level1 lfo1">
 			<span lang="EN-US" style="mso-ascii-font-family:Cambria;mso-bidi-font-family:Cambria;mso-fareast-font-family:Cambria;mso-hansi-font-family:Cambria"></span>
 			<span lang="EN-US">Some of the sample text in this document indicates the name of the style applied, so that you can easily apply the same formatting again. For example, this is the List Bullet style.<o:p></o:p><w:sdtpr></w:sdtpr></span>
@@ -129,7 +129,7 @@
 
 <span style="-webkit-text-size-adjust:auto;-webkit-text-stroke-width:0px;caret-color:rgb(0, 0, 0);color:rgb(0, 0, 0);display:inline !important;float:none;font-family:-webkit-standard;font-size:medium;font-style:normal;font-variant-caps:normal;font-weight:normal;letter-spacing:normal;orphans:auto;text-align:start;text-decoration:none;text-indent:0px;text-transform:none;white-space:normal;widows:auto;word-spacing:0px"></span>
 
-<ul>
+<ul style="list-style-type:disc">
 	<li class="MsoListBullet" style="mso-list:l0 level1 lfo1">
 		<span lang="EN-US" style="mso-ascii-font-family:Cambria;mso-bidi-font-family:Cambria;mso-fareast-font-family:Cambria;mso-hansi-font-family:Cambria"></span>
 		<span lang="EN-US">
@@ -146,7 +146,7 @@
 
 <span style="-webkit-text-size-adjust:auto;-webkit-text-stroke-width:0px;caret-color:rgb(0, 0, 0);color:rgb(0, 0, 0);display:inline !important;float:none;font-family:-webkit-standard;font-size:medium;font-style:normal;font-variant-caps:normal;font-weight:normal;letter-spacing:normal;orphans:auto;text-align:start;text-decoration:none;text-indent:0px;text-transform:none;white-space:normal;widows:auto;word-spacing:0px"></span>
 
-<ul>
+<ul style="list-style-type:disc">
 	<li class="MsoListBullet" style="mso-list:l0 level1 lfo1">
 		<span lang="EN-US" style="mso-ascii-font-family:Cambria;mso-bidi-font-family:Cambria;mso-fareast-font-family:Cambria;mso-hansi-font-family:Cambria"></span>
 		<span lang="EN-US">
@@ -170,7 +170,7 @@
 </h2>
 
 <w:sdt docpart="723EDD3B9355AB408B93DC96E350C9C8" id="-513455036" sdttag="Key responsibilities for Job Title 1:" showingplchdr="t" style="-webkit-text-size-adjust:auto;-webkit-text-stroke-width:0px;caret-color:rgb(0, 0, 0);color:rgb(0, 0, 0);font-family:-webkit-standard;font-style:normal;font-variant-caps:normal;font-weight:normal;letter-spacing:normal;orphans:auto;text-align:start;text-decoration:none;text-indent:0px;text-transform:none;white-space:normal;widows:auto;word-spacing:0px" temporary="t" title="Key responsibilities for Job Title 1:">
-	<ul>
+	<ul style="list-style-type:disc">
 		<li class="MsoListBullet" style="mso-list:l0 level1 lfo1">
 			<span lang="EN-US" style="mso-ascii-font-family:Cambria;mso-bidi-font-family:Cambria;mso-fareast-font-family:Cambria;mso-hansi-font-family:Cambria"></span>
 			<span lang="EN-US">This is the place for a brief summary of your key responsibilities and most stellar accomplishments.<o:p></o:p><w:sdtpr></w:sdtpr></span>
@@ -187,7 +187,7 @@
 </h2>
 
 <w:sdt docpart="35452DC21B458F4990E9AD6B0D21FF93" id="2140524828" sdttag="Key responsibilities for Job Title 2:" showingplchdr="t" style="-webkit-text-size-adjust:auto;-webkit-text-stroke-width:0px;caret-color:rgb(0, 0, 0);color:rgb(0, 0, 0);font-family:-webkit-standard;font-style:normal;font-variant-caps:normal;font-weight:normal;letter-spacing:normal;orphans:auto;text-align:start;text-decoration:none;text-indent:0px;text-transform:none;white-space:normal;widows:auto;word-spacing:0px" temporary="t" title="Key responsibilities for Job Title 2:">
-	<ul>
+	<ul style="list-style-type:disc">
 		<li class="MsoListBullet" style="mso-list:l0 level1 lfo1">
 			<span lang="EN-US" style="mso-ascii-font-family:Cambria;mso-bidi-font-family:Cambria;mso-fareast-font-family:Cambria;mso-hansi-font-family:Cambria"></span>
 			<span lang="EN-US">This is the place for a brief summary of your key responsibilities and most stellar accomplishments.<o:p></o:p><w:sdtpr></w:sdtpr></span>

+ 8 - 8
packages/ckeditor5-paste-from-office/tests/_data/list/resume-template/normalized.word2016.html

@@ -42,7 +42,7 @@
 	</span>
 </h2>
 
-<ul>
+<ul style="list-style-type:disc">
 	<li class="MsoListBulletCxSpFirst" style="mso-list:l0 level1 lfo1">
 		<span lang="EN-US" style="mso-ascii-font-family:Cambria;mso-bidi-font-family:Cambria;mso-fareast-font-family:Cambria;mso-hansi-font-family:Cambria"></span>
 		<span lang="EN-US">Major: <w:sdt docpart="11254AB6F6AF3048B0FB77B19E14A276" id="1821224400" sdttag="Major:" showingplchdr="t" temporary="t" text="t" title="Major:">Click here to enter text</w:sdt><o:p></o:p></span>
@@ -65,7 +65,7 @@
 	</span>
 </h2>
 
-<ul>
+<ul style="list-style-type:disc">
 	<li class="MsoListBulletCxSpFirst" style="mso-list:l0 level1 lfo1">
 		<span lang="EN-US" style="mso-ascii-font-family:Cambria;mso-bidi-font-family:Cambria;mso-fareast-font-family:Cambria;mso-hansi-font-family:Cambria"></span>
 		<span lang="EN-US">Major: <w:sdt docpart="2D9029A2B401CC42944B7EB502F83824" id="1046181329" sdttag="Major:" showingplchdr="t" temporary="t" text="t" title="Major:">Click here to enter text</w:sdt><o:p></o:p></span>
@@ -95,7 +95,7 @@
 </w:sdt>
 
 <w:sdt docpart="C51B29163715CD4F99529A18C891B99B" id="-1177730712" sdttag="Management Skills:" showingplchdr="t" temporary="t" title="Management Skills:">
-	<ul>
+	<ul style="list-style-type:disc">
 		<li class="MsoListBullet" style="mso-list:l0 level1 lfo1">
 			<span lang="EN-US" style="mso-ascii-font-family:Cambria;mso-bidi-font-family:Cambria;mso-fareast-font-family:Cambria;mso-hansi-font-family:Cambria"></span>
 			<span lang="EN-US">Think a document that looks this good has to be difficult to format? Think again! To easily apply any text formatting you see in this document with just a click, on the Home tab of the ribbon, check out Styles.<o:p></o:p><w:sdtpr></w:sdtpr></span>
@@ -110,7 +110,7 @@
 </w:sdt>
 
 <w:sdt docpart="92F72DE9A594584B9C488FED96B568B4" id="1544489962" sdttag="Sales Skills:" showingplchdr="t" temporary="t" title="Sales Skills:">
-	<ul>
+	<ul style="list-style-type:disc">
 		<li class="MsoListBullet" style="mso-list:l0 level1 lfo1">
 			<span lang="EN-US" style="mso-ascii-font-family:Cambria;mso-bidi-font-family:Cambria;mso-fareast-font-family:Cambria;mso-hansi-font-family:Cambria"></span>
 			<span lang="EN-US">Some of the sample text in this document indicates the name of the style applied, so that you can easily apply the same formatting again. For example, this is the List Bullet style.<o:p></o:p><w:sdtpr></w:sdtpr></span>
@@ -124,7 +124,7 @@
 	</h2>
 </w:sdt>
 
-<ul>
+<ul style="list-style-type:disc">
 	<li class="MsoListBullet" style="mso-list:l0 level1 lfo1">
 		<span lang="EN-US" style="mso-ascii-font-family:Cambria;mso-bidi-font-family:Cambria;mso-fareast-font-family:Cambria;mso-hansi-font-family:Cambria"></span>
 		<span lang="EN-US"><w:sdt docpart="1018054470CB4B4B93F5C30010DA2E3A" id="-1819335404" sdttag="Communication Skills:" showingplchdr="t" temporary="t" title="Communication Skills:">You delivered that big presentation to rave reviews. Don’t be shy about it now! This is the place to show how well you work and play with others.</w:sdt><o:p></o:p></span>
@@ -137,7 +137,7 @@
 	</h2>
 </w:sdt>
 
-<ul>
+<ul style="list-style-type:disc">
 	<li class="MsoListBullet" style="mso-list:l0 level1 lfo1">
 		<span lang="EN-US" style="mso-ascii-font-family:Cambria;mso-bidi-font-family:Cambria;mso-fareast-font-family:Cambria;mso-hansi-font-family:Cambria"></span>
 		<span lang="EN-US"><w:sdt docpart="D969CD1F1B5AC04493396C70BC53F75B" id="-1072199855" sdttag="Leadership Skills:" showingplchdr="t" temporary="t" title="Leadership Skills:">Are you president of your fraternity, head of the condo board, or a team lead for your favorite charity? You’re a natural leader—tell it like it is!</w:sdt><o:p></o:p></span>
@@ -157,7 +157,7 @@
 </h2>
 
 <w:sdt docpart="6484D2D9F5586D4E94F9BC3CAEDFDE20" id="-513455036" sdttag="Key responsibilities for Job Title 1:" showingplchdr="t" temporary="t" title="Key responsibilities for Job Title 1:">
-	<ul>
+	<ul style="list-style-type:disc">
 		<li class="MsoListBullet" style="mso-list:l0 level1 lfo1">
 			<span lang="EN-US" style="mso-ascii-font-family:Cambria;mso-bidi-font-family:Cambria;mso-fareast-font-family:Cambria;mso-hansi-font-family:Cambria"></span>
 			<span lang="EN-US">This is the place for a brief summary of your key responsibilities and most stellar accomplishments.<o:p></o:p><w:sdtpr></w:sdtpr></span>
@@ -172,7 +172,7 @@
 </h2>
 
 <w:sdt docpart="290D27AE8D9A964ABE103E88C285CB84" id="2140524828" sdttag="Key responsibilities for Job Title 2:" showingplchdr="t" temporary="t" title="Key responsibilities for Job Title 2:">
-	<ul>
+	<ul style="list-style-type:disc">
 		<li class="MsoListBullet" style="mso-list:l0 level1 lfo1">
 			<span lang="EN-US" style="mso-ascii-font-family:Cambria;mso-bidi-font-family:Cambria;mso-fareast-font-family:Cambria;mso-hansi-font-family:Cambria"></span>
 			<span lang="EN-US">This is the place for a brief summary of your key responsibilities and most stellar accomplishments.<o:p></o:p><w:sdtpr></w:sdtpr></span>

+ 2 - 2
packages/ckeditor5-paste-from-office/tests/_data/list/simple/model.word2016.html

@@ -1,2 +1,2 @@
-<listItem listIndent="0" listType="numbered">Item1</listItem>
-<listItem listIndent="0" listType="numbered">Item 2</listItem>
+<listItem listIndent="0" listStyle="default" listType="numbered">Item1</listItem>
+<listItem listIndent="0" listStyle="default" listType="numbered">Item 2</listItem>

+ 3 - 3
packages/ckeditor5-paste-from-office/tests/_data/list/styled/model.safari.word2016.html

@@ -1,8 +1,8 @@
 <paragraph>List:</paragraph>
 
-<listItem listIndent="0" listType="bulleted">B<$text bold="true">old</$text></listItem>
-<listItem listIndent="0" listType="bulleted"><$text linkHref="https://cksource.com/" underline="true">Lin</$text>k</listItem>
-<listItem listIndent="0" listType="bulleted">
+<listItem listIndent="0" listStyle="disc" listType="bulleted">B<$text bold="true">old</$text></listItem>
+<listItem listIndent="0" listStyle="disc" listType="bulleted"><$text linkHref="https://cksource.com/" underline="true">Lin</$text>k</listItem>
+<listItem listIndent="0" listStyle="disc" listType="bulleted">
 	<$text italic="true">M</$text><$text bold="true" italic="true">ul</$text>
 <$text bold="true" italic="true" underline="true">tip</$text>
 <$text italic="true" underline="true">le</$text>

+ 3 - 3
packages/ckeditor5-paste-from-office/tests/_data/list/styled/model.word2016.html

@@ -1,8 +1,8 @@
 <paragraph>List:</paragraph>
 
-<listItem listIndent="0" listType="bulleted">B<$text bold="true">old</$text></listItem>
-<listItem listIndent="0" listType="bulleted"><$text linkHref="https://cksource.com">Lin</$text>k</listItem>
-<listItem listIndent="0" listType="bulleted">
+<listItem listIndent="0" listStyle="disc" listType="bulleted">B<$text bold="true">old</$text></listItem>
+<listItem listIndent="0" listStyle="disc" listType="bulleted"><$text linkHref="https://cksource.com">Lin</$text>k</listItem>
+<listItem listIndent="0" listStyle="disc" listType="bulleted">
 	<$text italic="true">M</$text><$text bold="true" italic="true">ul</$text>
 	<$text bold="true" italic="true" underline="true">tip</$text>
 	<$text italic="true" underline="true">le</$text>

+ 1 - 1
packages/ckeditor5-paste-from-office/tests/_data/list/styled/normalized.safari.word2016.html

@@ -2,7 +2,7 @@
 	<span>List:<o:p></o:p></span>
 </p>
 
-<ul>
+<ul style="list-style-type:disc">
 	<li class="MsoListParagraphCxSpFirst" style="text-indent:-18.0pt;mso-list:l0 level1 lfo1">
 		<!--[if !supportLists]--><span style="font-family:Symbol;mso-fareast-font-family:Symbol;mso-bidi-font-family:
 	Symbol;mso-ansi-language:PL"></span><!--[endif]-->

+ 1 - 1
packages/ckeditor5-paste-from-office/tests/_data/list/styled/normalized.word2016.html

@@ -2,7 +2,7 @@
 	<span lang=PL style='mso-ansi-language:PL'>List:<o:p></o:p></span>
 </p>
 
-<ul>
+<ul style="list-style-type:disc">
 	<li class=MsoListParagraphCxSpFirst style='text-indent:-18.0pt;mso-list:l0 level1 lfo1'>
 		<![if !supportLists]>
 		<span lang=PL style='font-family:Symbol;mso-fareast-font-family:Symbol;mso-bidi-font-family:Symbol;mso-ansi-language:PL'></span>

+ 2 - 1
packages/ckeditor5-paste-from-office/tests/data/integration.js

@@ -13,6 +13,7 @@ import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
 import Link from '@ckeditor/ckeditor5-link/src/link';
 import Strikethrough from '@ckeditor/ckeditor5-basic-styles/src/strikethrough';
 import List from '@ckeditor/ckeditor5-list/src/list';
+import ListStyle from '@ckeditor/ckeditor5-list/src/liststyle';
 import Image from '@ckeditor/ckeditor5-image/src/image';
 import Table from '@ckeditor/ckeditor5-table/src/table';
 import TableProperties from '@ckeditor/ckeditor5-table/src/tableproperties';
@@ -71,7 +72,7 @@ describe( 'PasteFromOffice - integration', () => {
 		type: 'integration',
 		browsers,
 		editorConfig: {
-			plugins: [ Clipboard, Paragraph, Heading, Bold, Italic, Underline, Link, List, PasteFromOffice ]
+			plugins: [ Clipboard, Paragraph, Heading, Bold, Italic, Underline, Link, List, ListStyle, PasteFromOffice ]
 		},
 		skip: {
 			safari: [ 'heading3Styled' ] // Skip due to spacing issue (#13).

+ 165 - 0
packages/ckeditor5-paste-from-office/tests/filters/list.js

@@ -182,6 +182,171 @@ describe( 'PasteFromOffice - filters', () => {
 						'</ol>' );
 				} );
 			} );
+
+			describe( 'list styles', () => {
+				const level1 = 'style="mso-list:l0 level1 lfo0"';
+
+				describe( 'ordered list', () => {
+					it( 'converts "roman-lower" style to proper CSS attribute', () => {
+						const styles = '@list l0:level1\n' +
+							'{mso-level-number-format:roman-lower;}';
+
+						const html = `<p ${ level1 }>Foo</p>`;
+						const view = htmlDataProcessor.toView( html );
+
+						transformListItemLikeElementsIntoLists( view, styles );
+
+						expect( stringify( view ) ).to.equal(
+							`<ol style="list-style-type:lower-roman"><li ${ level1 }>Foo</li></ol>`
+						);
+					} );
+
+					it( 'converts "alpha-upper" style to proper CSS attribute', () => {
+						const styles = '@list l0:level1\n' +
+							'{mso-level-number-format:alpha-upper;}';
+
+						const html = `<p ${ level1 }>Foo</p>`;
+						const view = htmlDataProcessor.toView( html );
+
+						transformListItemLikeElementsIntoLists( view, styles );
+
+						expect( stringify( view ) ).to.equal(
+							`<ol style="list-style-type:upper-alpha"><li ${ level1 }>Foo</li></ol>`
+						);
+					} );
+
+					it( 'converts "alpha-lower" style to proper CSS attribute', () => {
+						const styles = '@list l0:level1\n' +
+							'{mso-level-number-format:alpha-lower;}';
+
+						const html = `<p ${ level1 }>Foo</p>`;
+						const view = htmlDataProcessor.toView( html );
+
+						transformListItemLikeElementsIntoLists( view, styles );
+
+						expect( stringify( view ) ).to.equal(
+							`<ol style="list-style-type:lower-alpha"><li ${ level1 }>Foo</li></ol>`
+						);
+					} );
+					it( 'converts "roman-upper" style to proper CSS attribute', () => {
+						const styles = '@list l0:level1\n' +
+							'{mso-level-number-format:roman-upper;}';
+
+						const html = `<p ${ level1 }>Foo</p>`;
+						const view = htmlDataProcessor.toView( html );
+
+						transformListItemLikeElementsIntoLists( view, styles );
+
+						expect( stringify( view ) ).to.equal(
+							`<ol style="list-style-type:upper-roman"><li ${ level1 }>Foo</li></ol>`
+						);
+					} );
+
+					it( 'converts "arabic-leading-zero" style to proper CSS attribute', () => {
+						const styles = '@list l0:level1\n' +
+							'{mso-level-number-format:arabic-leading-zero;}';
+
+						const html = `<p ${ level1 }>Foo</p>`;
+						const view = htmlDataProcessor.toView( html );
+
+						transformListItemLikeElementsIntoLists( view, styles );
+
+						expect( stringify( view ) ).to.equal(
+							`<ol style="list-style-type:decimal-leading-zero"><li ${ level1 }>Foo</li></ol>`
+						);
+					} );
+				} );
+
+				describe( 'unordered list', () => {
+					it( 'converts "circle" style to proper CSS attribute', () => {
+						const styles = '@list l0:level1\n' +
+							'{mso-level-number-format:bullet;}';
+
+						const html = `<p class=MsoListBulletCxSpFirst ${ level1 }>` +
+							'<span lang=\'EN-US\'><span style=\'mso-list:Ignore\'>o<span>&nbsp;&nbsp;</span></span></span></span>' +
+							'<span>Foo</span>' +
+							'</p>';
+
+						const view = htmlDataProcessor.toView( html );
+
+						transformListItemLikeElementsIntoLists( view, styles );
+
+						expect( stringify( view ) ).to.equal(
+							'<ul style="list-style-type:circle">' +
+								`<li class="MsoListBulletCxSpFirst" ${ level1 }>` +
+									'<span lang="EN-US"></span><span>Foo</span>' +
+								'</li>' +
+							'</ul>'
+						);
+					} );
+
+					it( 'converts "disc" style to proper CSS attribute', () => {
+						const styles = '@list l0:level1\n' +
+							'{mso-level-number-format:bullet;}';
+
+						const html = `<p class=MsoListBulletCxSpFirst ${ level1 }>` +
+							'<span lang=\'EN-US\'><span style=\'mso-list:Ignore\'>·<span>&nbsp;&nbsp;</span></span></span></span>' +
+							'<span>Foo</span>' +
+							'</p>';
+
+						const view = htmlDataProcessor.toView( html );
+
+						transformListItemLikeElementsIntoLists( view, styles );
+
+						expect( stringify( view ) ).to.equal(
+							'<ul style="list-style-type:disc">' +
+							`<li class="MsoListBulletCxSpFirst" ${ level1 }>` +
+							'<span lang="EN-US"></span><span>Foo</span>' +
+							'</li>' +
+							'</ul>'
+						);
+					} );
+
+					it( 'converts "square" style to proper CSS attribute', () => {
+						const styles = '@list l0:level1\n' +
+							'{mso-level-number-format:bullet;}';
+
+						const html = `<p class=MsoListBulletCxSpFirst ${ level1 }>` +
+							'<span lang=\'EN-US\'><span style=\'mso-list:Ignore\'>§<span>&nbsp;&nbsp;</span></span></span></span>' +
+							'<span>Foo</span>' +
+							'</p>';
+
+						const view = htmlDataProcessor.toView( html );
+
+						transformListItemLikeElementsIntoLists( view, styles );
+
+						expect( stringify( view ) ).to.equal(
+							'<ul style="list-style-type:square">' +
+							`<li class="MsoListBulletCxSpFirst" ${ level1 }>` +
+							'<span lang="EN-US"></span><span>Foo</span>' +
+							'</li>' +
+							'</ul>'
+						);
+					} );
+
+					it( 'ignores the marker if cannot be translated to list style feature', () => {
+						const styles = '@list l0:level1\n' +
+							'{mso-level-number-format:bullet;}';
+
+						const html = `<p class=MsoListBulletCxSpFirst ${ level1 }>` +
+							'<span lang=\'EN-US\'><span style=\'mso-list:Ignore\'>+<span>&nbsp;&nbsp;</span></span></span></span>' +
+							'<span>Foo</span>' +
+							'</p>';
+
+						const view = htmlDataProcessor.toView( html );
+
+						transformListItemLikeElementsIntoLists( view, styles );
+
+						expect( stringify( view ) ).to.equal(
+							'<ul>' +
+							`<li class="MsoListBulletCxSpFirst" ${ level1 }>` +
+							'<span lang="EN-US"></span><span>Foo</span>' +
+							'</li>' +
+							'</ul>'
+						);
+					} );
+				} );
+			} );
 		} );
 	} );
 

+ 3 - 3
packages/ckeditor5-table/src/tablecellproperties/tablecellpropertiesui.js

@@ -292,6 +292,9 @@ export default class TableCellPropertiesUI extends Plugin {
 	_showView() {
 		const editor = this.editor;
 
+		// Update the view with the model values.
+		this._fillViewFormFromCommandValues();
+
 		this._balloon.add( {
 			view: this.view,
 			position: getBalloonCellPositionData( editor )
@@ -300,9 +303,6 @@ export default class TableCellPropertiesUI extends Plugin {
 		// Create a new batch. Clicking "Cancel" will undo this batch.
 		this._undoStepBatch = editor.model.createBatch();
 
-		// Update the view with the model values.
-		this._fillViewFormFromCommandValues();
-
 		// Basic a11y.
 		this.view.focus();
 	}

+ 24 - 7
packages/ckeditor5-table/src/tablecellproperties/ui/tablecellpropertiesview.js

@@ -166,6 +166,7 @@ export default class TableCellPropertiesView extends View {
 		this.options = options;
 
 		const { borderStyleDropdown, borderWidthInput, borderColorInput, borderRowLabel } = this._createBorderFields();
+		const { backgroundRowLabel, backgroundInput } = this._createBackgroundFields();
 		const { widthInput, operatorLabel, heightInput, dimensionsLabel } = this._createDimensionFields();
 		const { horizontalAlignmentToolbar, verticalAlignmentToolbar, alignmentLabel } = this._createAlignmentFields();
 
@@ -223,7 +224,7 @@ export default class TableCellPropertiesView extends View {
 		 * @readonly
 		 * @member {module:table/ui/colorinputview~ColorInputView}
 		 */
-		this.backgroundInput = this._createBackgroundField();
+		this.backgroundInput = backgroundInput;
 
 		/**
 		 * An input that allows specifying the table cell padding.
@@ -332,9 +333,12 @@ export default class TableCellPropertiesView extends View {
 
 		// Background.
 		this.children.add( new FormRowView( locale, {
+			labelView: backgroundRowLabel,
 			children: [
-				this.backgroundInput
-			]
+				backgroundRowLabel,
+				backgroundInput
+			],
+			class: 'ck-table-form__background-row'
 		} ) );
 
 		// Dimensions row and padding.
@@ -486,6 +490,8 @@ export default class TableCellPropertiesView extends View {
 			this.borderStyle = evt.source._borderStyleValue;
 		} );
 
+		borderStyleDropdown.bind( 'isEmpty' ).to( this, 'borderStyle', value => !value );
+
 		addListToDropdown( borderStyleDropdown.fieldView, getBorderStyleDefinitions( this ) );
 
 		// -- Width ---------------------------------------------------
@@ -542,11 +548,19 @@ export default class TableCellPropertiesView extends View {
 	 * * {@link #backgroundInput}.
 	 *
 	 * @private
-	 * @returns {module:ui/labeledfield/labeledfieldview~LabeledFieldView}
+	 * @returns {Object.<String,module:ui/view~View>}
 	 */
-	_createBackgroundField() {
+	_createBackgroundFields() {
 		const locale = this.locale;
 		const t = this.t;
+
+		// -- Group label ---------------------------------------------
+
+		const backgroundRowLabel = new LabelView( locale );
+		backgroundRowLabel.text = t( 'Background' );
+
+		// -- Background color input -----------------------------------
+
 		const colorInputCreator = getLabeledColorInputCreator( {
 			colorConfig: this.options.backgroundColors,
 			columns: 5
@@ -555,7 +569,7 @@ export default class TableCellPropertiesView extends View {
 		const backgroundInput = new LabeledFieldView( locale, colorInputCreator );
 
 		backgroundInput.set( {
-			label: t( 'Background' ),
+			label: t( 'Color' ),
 			class: 'ck-table-cell-properties-form__background'
 		} );
 
@@ -564,7 +578,10 @@ export default class TableCellPropertiesView extends View {
 			this.backgroundColor = backgroundInput.fieldView.value;
 		} );
 
-		return backgroundInput;
+		return {
+			backgroundRowLabel,
+			backgroundInput
+		};
 	}
 
 	/**

+ 3 - 3
packages/ckeditor5-table/src/tableproperties/tablepropertiesui.js

@@ -282,6 +282,9 @@ export default class TablePropertiesUI extends Plugin {
 	_showView() {
 		const editor = this.editor;
 
+		// Update the view with the model values.
+		this._fillViewFormFromCommandValues();
+
 		this._balloon.add( {
 			view: this.view,
 			position: getBalloonTablePositionData( editor )
@@ -290,9 +293,6 @@ export default class TablePropertiesUI extends Plugin {
 		// Create a new batch. Clicking "Cancel" will undo this batch.
 		this._undoStepBatch = editor.model.createBatch();
 
-		// Update the view with the model values.
-		this._fillViewFormFromCommandValues();
-
 		// Basic a11y.
 		this.view.focus();
 	}

+ 26 - 10
packages/ckeditor5-table/src/tableproperties/ui/tablepropertiesview.js

@@ -141,6 +141,7 @@ export default class TablePropertiesView extends View {
 		this.options = options;
 
 		const { borderStyleDropdown, borderWidthInput, borderColorInput, borderRowLabel } = this._createBorderFields();
+		const { backgroundRowLabel, backgroundInput } = this._createBackgroundFields();
 		const { widthInput, operatorLabel, heightInput, dimensionsLabel } = this._createDimensionFields();
 		const { alignmentToolbar, alignmentLabel } = this._createAlignmentFields();
 
@@ -198,7 +199,7 @@ export default class TablePropertiesView extends View {
 		 * @readonly
 		 * @member {module:table/ui/colorinputview~ColorInputView}
 		 */
-		this.backgroundInput = this._createBackgroundField();
+		this.backgroundInput = backgroundInput;
 
 		/**
 		 * An input that allows specifying the table width.
@@ -218,7 +219,6 @@ export default class TablePropertiesView extends View {
 
 		/**
 		 * A toolbar with buttons that allow changing the alignment of an entire table.
-		 *
 		 * @readonly
 		 * @member {module:ui/toolbar/toolbar~ToolbarView}
 		 */
@@ -291,9 +291,12 @@ export default class TablePropertiesView extends View {
 
 		// Background row.
 		this.children.add( new FormRowView( locale, {
+			labelView: backgroundRowLabel,
 			children: [
-				this.backgroundInput
-			]
+				backgroundRowLabel,
+				backgroundInput
+			],
+			class: 'ck-table-form__background-row'
 		} ) );
 
 		this.children.add( new FormRowView( locale, {
@@ -433,6 +436,8 @@ export default class TablePropertiesView extends View {
 			this.borderStyle = evt.source._borderStyleValue;
 		} );
 
+		borderStyleDropdown.bind( 'isEmpty' ).to( this, 'borderStyle', value => !value );
+
 		addListToDropdown( borderStyleDropdown.fieldView, getBorderStyleDefinitions( this ) );
 
 		// -- Width ---------------------------------------------------
@@ -489,20 +494,28 @@ export default class TablePropertiesView extends View {
 	 * * {@link #backgroundInput}.
 	 *
 	 * @private
-	 * @returns {module:ui/labeledfield/labeledfieldview~LabeledFieldView}
+	 * @returns {Object.<String,module:ui/view~View>}
 	 */
-	_createBackgroundField() {
+	_createBackgroundFields() {
+		const locale = this.locale;
+		const t = this.t;
+
+		// -- Group label ---------------------------------------------
+
+		const backgroundRowLabel = new LabelView( locale );
+		backgroundRowLabel.text = t( 'Background' );
+
+		// -- Background color input -----------------------------------
+
 		const backgroundInputCreator = getLabeledColorInputCreator( {
 			colorConfig: this.options.backgroundColors,
 			columns: 5
 		} );
-		const locale = this.locale;
-		const t = this.t;
 
 		const backgroundInput = new LabeledFieldView( locale, backgroundInputCreator );
 
 		backgroundInput.set( {
-			label: t( 'Background' ),
+			label: t( 'Color' ),
 			class: 'ck-table-properties-form__background'
 		} );
 
@@ -511,7 +524,10 @@ export default class TablePropertiesView extends View {
 			this.backgroundColor = backgroundInput.fieldView.value;
 		} );
 
-		return backgroundInput;
+		return {
+			backgroundRowLabel,
+			backgroundInput
+		};
 	}
 
 	/**

+ 25 - 4
packages/ckeditor5-table/src/ui/colorinputview.js

@@ -73,6 +73,27 @@ export default class ColorInputView extends View {
 		 */
 		this.set( 'hasError', false );
 
+		/**
+		 * An observable flag set to `true` when the input is focused by the user.
+		 * `false` otherwise.
+		 *
+		 * @readonly
+		 * @observable
+		 * @member {Boolean} #isFocused
+		 * @default false
+		 */
+		this.set( 'isFocused', false );
+
+		/**
+		 * An observable flag set to `true` when the input contains no text.
+		 *
+		 * @readonly
+		 * @observable
+		 * @member {Boolean} #isEmpty
+		 * @default true
+		 */
+		this.set( 'isEmpty', true );
+
 		/**
 		 * The `id` of the element describing this field. When the field has
 		 * some error, it helps screen readers read the error text.
@@ -128,8 +149,8 @@ export default class ColorInputView extends View {
 				'aria-describedby': bind.to( 'ariaDescribedById' )
 			},
 			children: [
-				this._inputView,
-				this._dropdownView
+				this._dropdownView,
+				this._inputView
 			]
 		} );
 
@@ -214,8 +235,8 @@ export default class ColorInputView extends View {
 		} );
 
 		inputView.value = this.value;
-		inputView.bind( 'isReadOnly' ).to( this );
-		inputView.bind( 'hasError' ).to( this );
+		inputView.bind( 'isReadOnly', 'hasError' ).to( this );
+		this.bind( 'isFocused', 'isEmpty' ).to( inputView );
 
 		inputView.on( 'input', () => {
 			const inputValue = inputView.element.value;

+ 3 - 1
packages/ckeditor5-table/src/utils/ui/table-properties.js

@@ -362,7 +362,7 @@ export function getLabeledColorInputCreator( options ) {
 		} );
 
 		inputView.bind( 'isReadOnly' ).to( labeledFieldView, 'isEnabled', value => !value );
-		inputView.bind( 'errorText' ).to( labeledFieldView );
+		inputView.bind( 'hasError' ).to( labeledFieldView, 'errorText', value => !!value );
 
 		inputView.on( 'input', () => {
 			// UX: Make the error text disappear and disable the error indicator as the user
@@ -370,6 +370,8 @@ export function getLabeledColorInputCreator( options ) {
 			labeledFieldView.errorText = null;
 		} );
 
+		labeledFieldView.bind( 'isEmpty', 'isFocused' ).to( inputView );
+
 		return inputView;
 	};
 }

+ 14 - 0
packages/ckeditor5-table/tests/tablecellproperties/tablecellpropertiesui.js

@@ -566,6 +566,20 @@ describe( 'table cell properties', () => {
 			} );
 
 			describe( 'initial data', () => {
+				it( 'should be set before adding the form to the the balloon to avoid unnecessary input animations', () => {
+					const balloonAddSpy = testUtils.sinon.spy( editor.plugins.get( ContextualBalloon ), 'add' );
+					const borderStyleChangeSpy = testUtils.sinon.spy();
+
+					tableCellPropertiesView.on( 'change:borderStyle', borderStyleChangeSpy );
+
+					editor.commands.get( 'tableCellBorderStyle' ).value = 'a';
+					tableCellPropertiesButton.fire( 'execute' );
+
+					sinon.assert.calledOnce( borderStyleChangeSpy );
+					sinon.assert.calledOnce( balloonAddSpy );
+					sinon.assert.callOrder( borderStyleChangeSpy, balloonAddSpy );
+				} );
+
 				it( 'should be set from the command values', () => {
 					editor.commands.get( 'tableCellBorderStyle' ).value = 'a';
 					editor.commands.get( 'tableCellBorderColor' ).value = 'b';

+ 14 - 2
packages/ckeditor5-table/tests/tablecellproperties/ui/tablecellpropertiesview.js

@@ -149,6 +149,14 @@ describe( 'table cell properties', () => {
 							expect( labeledDropdown.fieldView.buttonView.label ).to.equal( 'Dashed' );
 						} );
 
+						it( 'should bind #isEmpty to #borderStyle property', () => {
+							view.borderStyle = 'dotted';
+							expect( labeledDropdown.isEmpty ).to.be.false;
+
+							view.borderStyle = null;
+							expect( labeledDropdown.isEmpty ).to.be.true;
+						} );
+
 						it( 'should change #borderStyle when executed', () => {
 							labeledDropdown.fieldView.listView.items.first.children.first.fire( 'execute' );
 							expect( view.borderStyle ).to.equal( '' );
@@ -281,7 +289,11 @@ describe( 'table cell properties', () => {
 						const row = view.element.childNodes[ 2 ];
 
 						expect( row.classList.contains( 'ck-form__row' ) ).to.be.true;
-						expect( row.childNodes[ 0 ] ).to.equal( view.backgroundInput.element );
+						expect( row.classList.contains( 'ck-table-form__background-row' ) ).to.be.true;
+
+						expect( row.classList.contains( 'ck-form__row' ) ).to.be.true;
+						expect( row.childNodes[ 0 ].textContent ).to.equal( 'Background' );
+						expect( row.childNodes[ 1 ] ).to.equal( view.backgroundInput.element );
 					} );
 
 					describe( 'background color input', () => {
@@ -293,7 +305,7 @@ describe( 'table cell properties', () => {
 
 						it( 'should be created', () => {
 							expect( labeledInput.fieldView ).to.be.instanceOf( ColorInputView );
-							expect( labeledInput.label ).to.equal( 'Background' );
+							expect( labeledInput.label ).to.equal( 'Color' );
 							expect( labeledInput.class ).to.equal( 'ck-table-cell-properties-form__background' );
 						} );
 

+ 14 - 0
packages/ckeditor5-table/tests/tableproperties/tablepropertiesui.js

@@ -508,6 +508,20 @@ describe( 'table properties', () => {
 			} );
 
 			describe( 'initial data', () => {
+				it( 'should be set before adding the form to the the balloon to avoid unnecessary input animations', () => {
+					const balloonAddSpy = testUtils.sinon.spy( editor.plugins.get( ContextualBalloon ), 'add' );
+					const borderStyleChangeSpy = testUtils.sinon.spy();
+
+					tablePropertiesView.on( 'change:borderStyle', borderStyleChangeSpy );
+
+					editor.commands.get( 'tableBorderStyle' ).value = 'a';
+					tablePropertiesButton.fire( 'execute' );
+
+					sinon.assert.calledOnce( borderStyleChangeSpy );
+					sinon.assert.calledOnce( balloonAddSpy );
+					sinon.assert.callOrder( borderStyleChangeSpy, balloonAddSpy );
+				} );
+
 				it( 'should be set from the command values', () => {
 					editor.commands.get( 'tableBorderStyle' ).value = 'a';
 					editor.commands.get( 'tableBorderColor' ).value = 'b';

+ 14 - 2
packages/ckeditor5-table/tests/tableproperties/ui/tablepropertiesview.js

@@ -149,6 +149,14 @@ describe( 'table properties', () => {
 							expect( labeledDropdown.fieldView.buttonView.label ).to.equal( 'Dashed' );
 						} );
 
+						it( 'should bind #isEmpty to #borderStyle property', () => {
+							view.borderStyle = 'dotted';
+							expect( labeledDropdown.isEmpty ).to.be.false;
+
+							view.borderStyle = null;
+							expect( labeledDropdown.isEmpty ).to.be.true;
+						} );
+
 						it( 'should change #borderStyle when executed', () => {
 							labeledDropdown.fieldView.listView.items.first.children.first.fire( 'execute' );
 							expect( view.borderStyle ).to.equal( '' );
@@ -285,7 +293,11 @@ describe( 'table properties', () => {
 						const row = view.element.childNodes[ 2 ];
 
 						expect( row.classList.contains( 'ck-form__row' ) ).to.be.true;
-						expect( row.childNodes[ 0 ] ).to.equal( view.backgroundInput.element );
+						expect( row.classList.contains( 'ck-table-form__background-row' ) ).to.be.true;
+
+						expect( row.classList.contains( 'ck-form__row' ) ).to.be.true;
+						expect( row.childNodes[ 0 ].textContent ).to.equal( 'Background' );
+						expect( row.childNodes[ 1 ] ).to.equal( view.backgroundInput.element );
 					} );
 
 					describe( 'background color input', () => {
@@ -297,7 +309,7 @@ describe( 'table properties', () => {
 
 						it( 'should be created', () => {
 							expect( labeledInput.fieldView ).to.be.instanceOf( ColorInputView );
-							expect( labeledInput.label ).to.equal( 'Background' );
+							expect( labeledInput.label ).to.equal( 'Color' );
 							expect( labeledInput.class ).to.equal( 'ck-table-properties-form__background' );
 						} );
 

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