瀏覽代碼

Merge branch 'master' into i/1098-accessible-input-labels

Piotrek Koszuliński 5 年之前
父節點
當前提交
5b7f2d945b
共有 100 個文件被更改,包括 1475 次插入258 次删除
  1. 1 1
      docs/_snippets/framework/tutorials/block-widget.js
  2. 1 1
      docs/_snippets/framework/tutorials/inline-widget.js
  3. 1 1
      docs/_snippets/framework/tutorials/using-react-in-widget.html
  4. 2 2
      docs/framework/guides/architecture/ui-library.md
  5. 1 1
      docs/framework/guides/deep-dive/localization.md
  6. 2 2
      docs/framework/guides/tutorials/implementing-an-inline-widget.md
  7. 2 2
      docs/framework/guides/tutorials/using-react-in-a-widget.md
  8. 1 0
      package.json
  9. 1 1
      packages/ckeditor5-alignment/src/alignmentui.js
  10. 1 1
      packages/ckeditor5-basic-styles/src/bold/boldui.js
  11. 1 1
      packages/ckeditor5-basic-styles/src/code/codeediting.js
  12. 1 1
      packages/ckeditor5-basic-styles/src/code/codeui.js
  13. 1 1
      packages/ckeditor5-basic-styles/src/italic/italicui.js
  14. 1 1
      packages/ckeditor5-basic-styles/src/strikethrough/strikethroughui.js
  15. 1 1
      packages/ckeditor5-basic-styles/src/subscript/subscriptui.js
  16. 1 1
      packages/ckeditor5-basic-styles/src/superscript/superscriptui.js
  17. 1 1
      packages/ckeditor5-basic-styles/src/underline/underlineui.js
  18. 1 1
      packages/ckeditor5-basic-styles/tests/code/codeediting.js
  19. 1 1
      packages/ckeditor5-block-quote/src/blockquoteui.js
  20. 1 1
      packages/ckeditor5-ckfinder/src/ckfinderui.js
  21. 0 0
      packages/ckeditor5-clipboard/docs/_snippets/features/build-paste-source.html
  22. 19 0
      packages/ckeditor5-clipboard/docs/_snippets/features/build-paste-source.js
  23. 14 0
      packages/ckeditor5-clipboard/docs/_snippets/features/paste-plain-text.html
  24. 48 0
      packages/ckeditor5-clipboard/docs/_snippets/features/paste-plain-text.js
  25. 66 0
      packages/ckeditor5-clipboard/docs/features/paste-plain-text.md
  26. 35 35
      packages/ckeditor5-clipboard/src/clipboardobserver.js
  27. 1 1
      packages/ckeditor5-clipboard/src/pasteplaintext.js
  28. 2 2
      packages/ckeditor5-code-block/src/codeblockui.js
  29. 7 0
      packages/ckeditor5-core/src/editor/editor.js
  30. 12 0
      packages/ckeditor5-core/tests/editor/editor.js
  31. 1 1
      packages/ckeditor5-core/tests/manual/readonly.js
  32. 1 1
      packages/ckeditor5-font/src/fontfamily/fontfamilyui.js
  33. 1 1
      packages/ckeditor5-font/src/fontsize/fontsizeui.js
  34. 1 1
      packages/ckeditor5-font/src/ui/colorui.js
  35. 1 1
      packages/ckeditor5-heading/src/headingbuttonsui.js
  36. 1 1
      packages/ckeditor5-heading/src/headingui.js
  37. 2 2
      packages/ckeditor5-highlight/src/highlightui.js
  38. 1 1
      packages/ckeditor5-horizontal-line/src/horizontallineui.js
  39. 1 1
      packages/ckeditor5-html-embed/src/htmlembedui.js
  40. 2 0
      packages/ckeditor5-image/docs/_snippets/features/build-image-source.js
  41. 1 1
      packages/ckeditor5-image/docs/_snippets/features/image-caption.js
  42. 3 0
      packages/ckeditor5-image/docs/_snippets/features/image-insert-via-pasting-url-into-editor.html
  43. 20 0
      packages/ckeditor5-image/docs/_snippets/features/image-insert-via-pasting-url-into-editor.js
  44. 1 1
      packages/ckeditor5-image/docs/_snippets/features/image-insert-via-url.js
  45. 1 1
      packages/ckeditor5-image/docs/_snippets/features/image-link.js
  46. 1 1
      packages/ckeditor5-image/docs/_snippets/features/image-resize-buttons-dropdown.js
  47. 1 1
      packages/ckeditor5-image/docs/_snippets/features/image-resize-buttons.js
  48. 1 1
      packages/ckeditor5-image/docs/_snippets/features/image-resize-px.js
  49. 1 1
      packages/ckeditor5-image/docs/_snippets/features/image-resize.js
  50. 1 1
      packages/ckeditor5-image/docs/_snippets/features/image-style-presentational.js
  51. 1 1
      packages/ckeditor5-image/docs/_snippets/features/image-style.js
  52. 1 1
      packages/ckeditor5-image/docs/_snippets/features/image-toolbar.js
  53. 1 1
      packages/ckeditor5-image/docs/_snippets/features/image.js
  54. 1 0
      packages/ckeditor5-image/docs/api/image.md
  55. 17 0
      packages/ckeditor5-image/docs/features/image.md
  56. 174 0
      packages/ckeditor5-image/src/autoimage.js
  57. 4 6
      packages/ckeditor5-image/src/image/imageinsertcommand.js
  58. 13 12
      packages/ckeditor5-image/src/image/utils.js
  59. 1 1
      packages/ckeditor5-image/src/imageinsert/imageinsertui.js
  60. 1 1
      packages/ckeditor5-image/src/imageresize/imageresizebuttons.js
  61. 38 12
      packages/ckeditor5-image/src/imageresize/imageresizehandles.js
  62. 1 1
      packages/ckeditor5-image/src/imagestyle/imagestyleui.js
  63. 1 1
      packages/ckeditor5-image/src/imagetextalternative/imagetextalternativeui.js
  64. 6 9
      packages/ckeditor5-image/src/imageupload/imageuploadcommand.js
  65. 555 0
      packages/ckeditor5-image/tests/autoimage.js
  66. 3 9
      packages/ckeditor5-image/tests/image/utils.js
  67. 37 0
      packages/ckeditor5-image/tests/imageresize/_utils/utils.js
  68. 1 4
      packages/ckeditor5-image/tests/imageresize/imageresizeediting.js
  69. 78 17
      packages/ckeditor5-image/tests/imageresize/imageresizehandles.js
  70. 3 2
      packages/ckeditor5-image/tests/manual/imageinsertviaurl.js
  71. 5 0
      packages/ckeditor5-image/tests/manual/imageinsertviaurl.md
  72. 1 1
      packages/ckeditor5-indent/src/indentui.js
  73. 2 2
      packages/ckeditor5-link/src/linkui.js
  74. 2 2
      packages/ckeditor5-list/src/liststyleui.js
  75. 1 1
      packages/ckeditor5-list/src/utils.js
  76. 35 1
      packages/ckeditor5-markdown-gfm/docs/_snippets/features/markdown.html
  77. 20 2
      packages/ckeditor5-markdown-gfm/docs/_snippets/features/markdown.js
  78. 19 9
      packages/ckeditor5-markdown-gfm/docs/features/markdown.md
  79. 1 1
      packages/ckeditor5-media-embed/src/mediaembedui.js
  80. 9 11
      packages/ckeditor5-media-embed/tests/automediaembed.js
  81. 1 1
      packages/ckeditor5-mention/src/mentionui.js
  82. 1 1
      packages/ckeditor5-page-break/src/pagebreakui.js
  83. 7 1
      packages/ckeditor5-paste-from-office/docs/features/paste-from-google-docs.md
  84. 7 1
      packages/ckeditor5-paste-from-office/docs/features/paste-from-word.md
  85. 96 7
      packages/ckeditor5-paste-from-office/src/filters/list.js
  86. 2 2
      packages/ckeditor5-paste-from-office/tests/_data/list/heading1/model.word2016.html
  87. 2 2
      packages/ckeditor5-paste-from-office/tests/_data/list/heading3-styled/model.word2016.html
  88. 1 1
      packages/ckeditor5-paste-from-office/tests/_data/list/heading3-styled/normalized.safari.word2016.html
  89. 1 1
      packages/ckeditor5-paste-from-office/tests/_data/list/heading3-styled/normalized.word2016.html
  90. 1 1
      packages/ckeditor5-paste-from-office/tests/_data/list/heading7/model.word2016.html
  91. 12 12
      packages/ckeditor5-paste-from-office/tests/_data/list/many-one-item/model.word2016.html
  92. 8 8
      packages/ckeditor5-paste-from-office/tests/_data/list/many-one-item/normalized.safari.word2016.html
  93. 8 8
      packages/ckeditor5-paste-from-office/tests/_data/list/many-one-item/normalized.word2016.html
  94. 4 4
      packages/ckeditor5-paste-from-office/tests/_data/list/multiple-combined/model.word2016.html
  95. 3 3
      packages/ckeditor5-paste-from-office/tests/_data/list/multiple/model.word2016.html
  96. 1 1
      packages/ckeditor5-paste-from-office/tests/_data/list/multiple/normalized.safari.word2016.html
  97. 1 1
      packages/ckeditor5-paste-from-office/tests/_data/list/multiple/normalized.word2016.html
  98. 6 6
      packages/ckeditor5-paste-from-office/tests/_data/list/nested-mixed/model.word2016.html
  99. 2 2
      packages/ckeditor5-paste-from-office/tests/_data/list/nested-mixed/normalized.word2016.html
  100. 9 9
      packages/ckeditor5-paste-from-office/tests/_data/list/nested-multiple/model.word2016.html

+ 1 - 1
docs/_snippets/framework/tutorials/block-widget.js

@@ -54,7 +54,7 @@ class SimpleBoxUI extends Plugin {
 			// Execute the command when the button is clicked (executed).
 			this.listenTo( buttonView, 'execute', () => {
 				editor.execute( 'insertSimpleBox' );
-				editor.editing.view.focus();
+				editor.focus();
 			} );
 
 			return buttonView;

+ 1 - 1
docs/_snippets/framework/tutorials/inline-widget.js

@@ -82,7 +82,7 @@ class PlaceholderUI extends Plugin {
 			// Execute the command when the dropdown item is clicked (executed).
 			this.listenTo( dropdownView, 'execute', evt => {
 				editor.execute( 'placeholder', { value: evt.source.commandParam } );
-				editor.editing.view.focus();
+				editor.focus();
 			} );
 
 			return dropdownView;

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

@@ -311,7 +311,7 @@ class App extends React.Component {
 				products={this.props.products}
 				onClick={( id ) => {
 					this.editor.execute( 'insertProduct', id );
-					this.editor.editing.view.focus();
+					this.editor.focus();
 				}}
 			/>
 		];

+ 2 - 2
docs/framework/guides/architecture/ui-library.md

@@ -443,13 +443,13 @@ dropdownView.bind( 'isEnabled' ).toMany( buttons, 'isEnabled',
 
 ### Best practices
 
-It is advised that for the best user experience the editing view gets {@link module:engine/view/view~View#focus focused} upon any user action (e.g. executing a command) to make sure the editor retains focus:
+It is advised that for the best user experience the editor gets {@link module:core/editor/editor~Editor#focus focused} upon any user action (e.g. executing a command):
 
 ```js
 // Execute some action on dropdown#execute event.
 dropdownView.buttonView.on( 'execute', () => {
 	editor.execute( 'command', { value: ... } );
-	editor.editing.view.focus();
+	editor.focus();
 } );
 ```
 

+ 1 - 1
docs/framework/guides/deep-dive/localization.md

@@ -103,7 +103,7 @@ editor.ui.componentFactory.add( 'smilingFaceEmoji', locale => {
 
 	buttonView.on( 'execute', () => {
 		editor.execute( 'insertSmilingFaceEmoji' );
-		editor.editing.view.focus();
+		editor.focus();
 	} );
 } );
 // ...

+ 2 - 2
docs/framework/guides/tutorials/implementing-an-inline-widget.md

@@ -591,7 +591,7 @@ export default class PlaceholderUI extends Plugin {
 			// Execute the command when the dropdown item is clicked (executed).
 			this.listenTo( dropdownView, 'execute', evt => {
 				editor.execute( 'placeholder', { value: evt.source.commandParam } );
-				editor.editing.view.focus();
+				editor.focus();
 			} );
 
 			return dropdownView;
@@ -819,7 +819,7 @@ class PlaceholderUI extends Plugin {
 			// Execute the command when the dropdown item is clicked (executed).
 			this.listenTo( dropdownView, 'execute', evt => {
 				editor.execute( 'placeholder', { value: evt.source.commandParam } );
-				editor.editing.view.focus();
+				editor.focus();
 			} );
 
 			return dropdownView;

+ 2 - 2
docs/framework/guides/tutorials/using-react-in-a-widget.md

@@ -636,7 +636,7 @@ class App extends React.Component {
 				products={this.props.products}
 				onClick={( id ) => {
 					this.editor.execute( 'insertProduct', id );
-					this.editor.editing.view.focus();
+					this.editor.focus();
 				}}
 			/>
 		];
@@ -1047,7 +1047,7 @@ class App extends React.Component {
 				products={this.props.products}
 				onClick={( id ) => {
 					this.editor.execute( 'insertProduct', id );
-					this.editor.editing.view.focus();
+					this.editor.focus();
 				}}
 			/>
 		];

+ 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-alignment/src/alignmentui.js

@@ -152,7 +152,7 @@ export default class AlignmentUI extends Plugin {
 			// Execute command.
 			this.listenTo( buttonView, 'execute', () => {
 				editor.execute( 'alignment', { value: option } );
-				editor.editing.view.focus();
+				editor.focus();
 			} );
 
 			return buttonView;

+ 1 - 1
packages/ckeditor5-basic-styles/src/bold/boldui.js

@@ -45,7 +45,7 @@ export default class BoldUI extends Plugin {
 			// Execute command.
 			this.listenTo( view, 'execute', () => {
 				editor.execute( BOLD );
-				editor.editing.view.focus();
+				editor.focus();
 			} );
 
 			return view;

+ 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/src/code/codeui.js

@@ -46,7 +46,7 @@ export default class CodeUI extends Plugin {
 			// Execute command.
 			this.listenTo( view, 'execute', () => {
 				editor.execute( CODE );
-				editor.editing.view.focus();
+				editor.focus();
 			} );
 
 			return view;

+ 1 - 1
packages/ckeditor5-basic-styles/src/italic/italicui.js

@@ -45,7 +45,7 @@ export default class ItalicUI extends Plugin {
 			// Execute command.
 			this.listenTo( view, 'execute', () => {
 				editor.execute( ITALIC );
-				editor.editing.view.focus();
+				editor.focus();
 			} );
 
 			return view;

+ 1 - 1
packages/ckeditor5-basic-styles/src/strikethrough/strikethroughui.js

@@ -45,7 +45,7 @@ export default class StrikethroughUI extends Plugin {
 			// Execute command.
 			this.listenTo( view, 'execute', () => {
 				editor.execute( STRIKETHROUGH );
-				editor.editing.view.focus();
+				editor.focus();
 			} );
 
 			return view;

+ 1 - 1
packages/ckeditor5-basic-styles/src/subscript/subscriptui.js

@@ -44,7 +44,7 @@ export default class SubscriptUI extends Plugin {
 			// Execute command.
 			this.listenTo( view, 'execute', () => {
 				editor.execute( SUBSCRIPT );
-				editor.editing.view.focus();
+				editor.focus();
 			} );
 
 			return view;

+ 1 - 1
packages/ckeditor5-basic-styles/src/superscript/superscriptui.js

@@ -44,7 +44,7 @@ export default class SuperscriptUI extends Plugin {
 			// Execute command.
 			this.listenTo( view, 'execute', () => {
 				editor.execute( SUPERSCRIPT );
-				editor.editing.view.focus();
+				editor.focus();
 			} );
 
 			return view;

+ 1 - 1
packages/ckeditor5-basic-styles/src/underline/underlineui.js

@@ -45,7 +45,7 @@ export default class UnderlineUI extends Plugin {
 			// Execute command.
 			this.listenTo( view, 'execute', () => {
 				editor.execute( UNDERLINE );
-				editor.editing.view.focus();
+				editor.focus();
 			} );
 
 			return view;

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

+ 1 - 1
packages/ckeditor5-block-quote/src/blockquoteui.js

@@ -45,7 +45,7 @@ export default class BlockQuoteUI extends Plugin {
 			// Execute command.
 			this.listenTo( buttonView, 'execute', () => {
 				editor.execute( 'blockQuote' );
-				editor.editing.view.focus();
+				editor.focus();
 			} );
 
 			return buttonView;

+ 1 - 1
packages/ckeditor5-ckfinder/src/ckfinderui.js

@@ -48,7 +48,7 @@ export default class CKFinderUI extends Plugin {
 
 			button.on( 'execute', () => {
 				editor.execute( 'ckfinder' );
-				editor.editing.view.focus();
+				editor.focus();
 			} );
 
 			return button;

+ 0 - 0
packages/ckeditor5-clipboard/docs/_snippets/features/build-paste-source.html


+ 19 - 0
packages/ckeditor5-clipboard/docs/_snippets/features/build-paste-source.js

@@ -0,0 +1,19 @@
+/**
+ * @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 window */
+
+import ClassicEditor from '@ckeditor/ckeditor5-build-classic/src/ckeditor';
+import Strikethrough from '@ckeditor/ckeditor5-basic-styles/src/strikethrough';
+import Underline from '@ckeditor/ckeditor5-basic-styles/src/underline';
+import Font from '@ckeditor/ckeditor5-font/src/font';
+
+ClassicEditor.builtinPlugins.push(
+	Strikethrough,
+	Underline,
+	Font
+);
+
+window.ClassicEditor = ClassicEditor;

+ 14 - 0
packages/ckeditor5-clipboard/docs/_snippets/features/paste-plain-text.html

@@ -0,0 +1,14 @@
+<div id="snippet-paste-plain-text">
+	<h2>On emphasis</h2>
+	<p>In typography, emphasis is the strengthening of words in a text with a font in a different style from the rest of the text, to highlight them. This may include text in <em>italics</em>, <strong>bold</strong>, <u>underlined</u> or <s>strikethrough</s> words or whole paragraphs.</p>
+	<h3>Italic type</h3>
+	<p><em>Italic type is a cursive font based on a stylized form of calligraphic handwriting. Owing to the influence from calligraphy, italics normally slant slightly to the right. One manual of English usage described italics as &quot;the print equivalent of underlining&quot;; in other words, underscore in a manuscript directs a typesetter to use italic.</em></p>
+	<p><em>Italics are a way to emphasize key points in a printed text, to identify many types of creative works, to cite foreign words or phrases, or, when quoting a speaker, a way to show which words they stressed.</em></p>
+	<h3>Bold type</h3>
+	<p><strong>A bold font weight makes the letters thicker than the surrounding text. It adds certain blackness to the type. Bold strongly stands out from the regular text, and is often used to highlight keywords important to the text content.</strong></p>
+	<p><strong>For example, printed dictionaries often use boldface for their keywords, and the names of entries can conventionally be marked in bold.</strong></p>
+	<h3>Underlined type</h3>
+	<p><u>Professional Western typesetting usually does not employ lines under letters for emphasis within running text because it is considered too distracting.</u></p>
+
+	<p><u>Underlining is, however, often used with typewriters, in handwriting, and with some non-alphabetic scripts. It is also used for secondary emphasis, i.e. marks added by the reader and not the author.</u></p>
+</div>

+ 48 - 0
packages/ckeditor5-clipboard/docs/_snippets/features/paste-plain-text.js

@@ -0,0 +1,48 @@
+/**
+ * @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';
+
+ClassicEditor
+	.create( document.querySelector( '#snippet-paste-plain-text' ), {
+		toolbar: {
+			items: [
+				'heading',
+				'|',
+				'fontSize',
+				'fontFamily',
+				'fontColor',
+				'fontBackgroundColor',
+				'|',
+				'bold',
+				'italic',
+				'underline',
+				'strikethrough',
+				'|',
+				'undo',
+				'redo'
+			],
+			viewportTopOffset: window.getViewportTopOffsetConfig()
+		},
+		fontFamily: {
+			supportAllValues: true
+		},
+		fontSize: {
+			options: [ 10, 12, 14, 'default', 18, 20, 22 ],
+			supportAllValues: true
+		},
+		placeholder: 'Paste the content here to test the feature.',
+		cloudServices: CS_CONFIG
+	} )
+	.then( editor => {
+		window.editor = editor;
+		// Prevent showing a warning notification when user is pasting a content from MS Word or Google Docs.
+		window.preventPasteFromOfficeNotification = false;
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );

+ 66 - 0
packages/ckeditor5-clipboard/docs/features/paste-plain-text.md

@@ -0,0 +1,66 @@
+---
+menu-title: Paste plain text
+category: features-pasting
+order: 30
+---
+
+{@snippet features/build-paste-source}
+
+# Pasting plain text
+
+The plain text pasting feature is implemented by the {@link module:clipboard/clipboard~PastePlainText} plugin which is a part of the {@link module:clipboard/clipboard~Clipboard} plugin.
+
+It detects the <kbd>Ctrl</kbd>/<kbd>Cmd</kbd> + <kbd>Shift</kbd> + <kbd>V</kbd> keystroke during the paste and causes the pasted text to inherit the styles of the content it was pasted into. In this sense, the feature can also be described as "pasting without formatting" &mdash; the source formatting of the pasted text gets replaced with the target formatting of the text it was pasted into.
+
+Pasting plain text with a double line break will turn the break into a paragraph. A single line break will instead be turned into a soft break upon pasting.
+
+## Demo
+
+Paste plain text between the styled paragraphs below using the <kbd>Ctrl</kbd>/<kbd>Cmd</kbd> + <kbd>Shift</kbd> + <kbd>V</kbd> to test the style inheritance. Paste without the <kbd>Shift</kbd> modifier to retain unstyled text. You can copy this very paragraph or one from the demo for convenience.
+
+{@snippet features/paste-plain-text}
+
+## Related features
+
+CKEditor 5 supports a wider range of paste features, including:
+* {@link features/paste-from-word Paste from Word} &ndash; Paste content from Microsoft Word and maintain the original structure and formatting.
+* {@link features/paste-from-google-docs Paste from Google Docs} &ndash; Paste content from Google Docs, maintaining the original formatting and structure.
+
+## Installation
+
+<info-box info>
+	This feature is required by the clipboard plugin and is enabled by default in all official builds. The installation instructions are for developers interested in building their own custom rich-text editor.
+</info-box>
+
+To add this feature to your rich-text editor, install the [`@ckeditor/ckeditor5-clipboard`](https://www.npmjs.com/package/@ckeditor/ckeditor5-clipboard) package:
+
+```
+npm install --save @ckeditor/ckeditor5-clipboard
+```
+
+Then add the {@link module:clipboard/clipboard~Clipboard `Clipboard`} plugin to your plugin list:
+
+```js
+import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard';
+
+ClassicEditor
+	.create( document.querySelector( '#editor' ), {
+		plugins: [ Clipboard, Bold, ... ]
+	} )
+	.then( ... )
+	.catch( ... );
+```
+
+The {@link module:clipboard/clipboard~PastePlainText `PastePlainText`} plugin will activate along with the clipboard plugin.
+
+## Support for other applications
+
+You can find more information regarding compatibility with other applications in [this ticket](https://github.com/ckeditor/ckeditor5/issues/1184#issuecomment-409828069).
+
+If you think that support for any of the applications needs improvements, please add 👍  and comments in the following issues:
+
+* [Support pasting from Excel](https://github.com/ckeditor/ckeditor5/issues/2513).
+* [Support pasting from Libre Office](https://github.com/ckeditor/ckeditor5/issues/2520).
+* [Support pasting from Pages](https://github.com/ckeditor/ckeditor5/issues/2527).
+
+Feel free to open a [new feature request](https://github.com/ckeditor/ckeditor5/issues/new/choose) for other similar applications, too!

+ 35 - 35
packages/ckeditor5-clipboard/src/clipboardobserver.js

@@ -24,7 +24,7 @@ import DataTransfer from './datatransfer';
  * * {@link module:engine/view/document~Document#event:cut}
  *
  * Note that this observer is not available by default (it is not added by the engine).
- * To make it available it needs to be added to {@link module:engine/view/document~Document} by
+ * To make it available, it needs to be added to {@link module:engine/view/document~Document} by
  * the {@link module:engine/view/view~View#addObserver `View#addObserver()`} method. You can also load the
  * {@link module:clipboard/clipboard~Clipboard} plugin which adds this observer automatically (because it uses it).
  *
@@ -100,111 +100,111 @@ function getDropViewRange( view, domEvent ) {
 }
 
 /**
- * Fired as a continuation of {@link #event:paste} and {@link #event:drop} events.
+ * Fired as a continuation of the {@link #event:paste} and {@link #event:drop} events.
  *
  * It is a part of the {@glink framework/guides/deep-dive/clipboard#input-pipeline "clipboard input pipeline"}.
  *
- * Fired with a `dataTransfer` which comes from the clipboard and which content should be processed
+ * Fired with a `dataTransfer` which comes from the clipboard and whose content should be processed
  * and inserted into the editor.
  *
- * Note that this event is not available by default. To make it available {@link module:clipboard/clipboardobserver~ClipboardObserver}
- * needs to be added to {@link module:engine/view/document~Document} by the {@link module:engine/view/view~View#addObserver} method.
- * It's done by the {@link module:clipboard/clipboard~Clipboard} feature. If it's not loaded, it must be done manually.
+ * Note that this event is not available by default. To make it available, {@link module:clipboard/clipboardobserver~ClipboardObserver}
+ * needs to be added to the {@link module:engine/view/document~Document} by the {@link module:engine/view/view~View#addObserver} method.
+ * This is done by the {@link module:clipboard/clipboard~Clipboard} feature. If it is not loaded, it must be done manually.
  *
  * @see module:clipboard/clipboardobserver~ClipboardObserver
  * @see module:clipboard/clipboard~Clipboard
  * @event module:engine/view/document~Document#event:clipboardInput
  * @param {Object} data Event data.
- * @param {module:clipboard/datatransfer~DataTransfer} data.dataTransfer Data transfer instance.
+ * @param {module:clipboard/datatransfer~DataTransfer} data.dataTransfer The data transfer instance.
  * @param {Array.<module:engine/view/range~Range>} data.targetRanges Ranges which are the target of the operation
  * (usually – into which the content should be inserted).
  * If clipboard input was triggered by a paste operation, then these are the selection ranges. If by a drop operation,
- * then it's the drop position (which can be different than the selection at the moment of drop).
+ * then it is the drop position (which can be different than the selection at the moment of drop).
  */
 
 /**
- * Fired when user drags content over one of the editables.
+ * Fired when the user drags the content over one of the editables.
  *
  * Introduced by {@link module:clipboard/clipboardobserver~ClipboardObserver}.
  *
- * Note that this event is not available by default. To make it available {@link module:clipboard/clipboardobserver~ClipboardObserver}
- * needs to be added to {@link module:engine/view/document~Document} by the {@link module:engine/view/view~View#addObserver} method.
- * It's done by the {@link module:clipboard/clipboard~Clipboard} feature. If it's not loaded, it must be done manually.
+ * Note that this event is not available by default. To make it available, {@link module:clipboard/clipboardobserver~ClipboardObserver}
+ * needs to be added to the {@link module:engine/view/document~Document} by the {@link module:engine/view/view~View#addObserver} method.
+ * This is done by the {@link module:clipboard/clipboard~Clipboard} feature. If it is not loaded, it must be done manually.
  *
  * @see module:engine/view/document~Document#event:clipboardInput
  * @event module:engine/view/document~Document#event:dragover
- * @param {module:clipboard/clipboardobserver~ClipboardEventData} data Event data.
+ * @param {module:clipboard/clipboardobserver~ClipboardEventData} data The event data.
  */
 
 /**
- * Fired when user dropped content into one of the editables.
+ * Fired when the user dropped the content into one of the editables.
  *
  * Introduced by {@link module:clipboard/clipboardobserver~ClipboardObserver}.
  *
- * Note that this event is not available by default. To make it available {@link module:clipboard/clipboardobserver~ClipboardObserver}
- * needs to be added to {@link module:engine/view/document~Document} by the {@link module:engine/view/view~View#addObserver} method.
- * It's done by the {@link module:clipboard/clipboard~Clipboard} feature. If it's not loaded, it must be done manually.
+ * Note that this event is not available by default. To make it available, {@link module:clipboard/clipboardobserver~ClipboardObserver}
+ * needs to be added to the {@link module:engine/view/document~Document} by the {@link module:engine/view/view~View#addObserver} method.
+ * This is done by the {@link module:clipboard/clipboard~Clipboard} feature. If it is not loaded, it must be done manually.
  *
  * @see module:engine/view/document~Document#event:clipboardInput
  * @event module:engine/view/document~Document#event:drop
- * @param {module:clipboard/clipboardobserver~ClipboardEventData} data Event data.
+ * @param {module:clipboard/clipboardobserver~ClipboardEventData} data The event data.
  * @param {module:engine/view/range~Range} dropRange The position into which the content is dropped.
  */
 
 /**
- * Fired when user pasted content into one of the editables.
+ * Fired when the user pasted the content into one of the editables.
  *
  * Introduced by {@link module:clipboard/clipboardobserver~ClipboardObserver}.
  *
- * Note that this event is not available by default. To make it available {@link module:clipboard/clipboardobserver~ClipboardObserver}
- * needs to be added to {@link module:engine/view/document~Document} by the {@link module:engine/view/view~View#addObserver} method.
- * It's done by the {@link module:clipboard/clipboard~Clipboard} feature. If it's not loaded, it must be done manually.
+ * Note that this event is not available by default. To make it available, {@link module:clipboard/clipboardobserver~ClipboardObserver}
+ * needs to be added to the {@link module:engine/view/document~Document} by the {@link module:engine/view/view~View#addObserver} method.
+ * This is done by the {@link module:clipboard/clipboard~Clipboard} feature. If it is not loaded, it must be done manually.
  *
  * @see module:engine/view/document~Document#event:clipboardInput
  * @event module:engine/view/document~Document#event:paste
- * @param {module:clipboard/clipboardobserver~ClipboardEventData} data Event data.
+ * @param {module:clipboard/clipboardobserver~ClipboardEventData} data The event data.
  */
 
 /**
- * Fired when user copied content from one of the editables.
+ * Fired when the user copied the content from one of the editables.
  *
  * Introduced by {@link module:clipboard/clipboardobserver~ClipboardObserver}.
  *
- * Note that this event is not available by default. To make it available {@link module:clipboard/clipboardobserver~ClipboardObserver}
- * needs to be added to {@link module:engine/view/document~Document} by the {@link module:engine/view/view~View#addObserver} method.
- * It's done by the {@link module:clipboard/clipboard~Clipboard} feature. If it's not loaded, it must be done manually.
+ * Note that this event is not available by default. To make it available, {@link module:clipboard/clipboardobserver~ClipboardObserver}
+ * needs to be added to the {@link module:engine/view/document~Document} by the {@link module:engine/view/view~View#addObserver} method.
+ * This is done by the {@link module:clipboard/clipboard~Clipboard} feature. If it is not loaded, it must be done manually.
  *
  * @see module:clipboard/clipboardobserver~ClipboardObserver
  * @event module:engine/view/document~Document#event:copy
- * @param {module:clipboard/clipboardobserver~ClipboardEventData} data Event data.
+ * @param {module:clipboard/clipboardobserver~ClipboardEventData} data The event data.
  */
 
 /**
- * Fired when user cut content from one of the editables.
+ * Fired when the user cut the content from one of the editables.
  *
  * Introduced by {@link module:clipboard/clipboardobserver~ClipboardObserver}.
  *
- * Note that this event is not available by default. To make it available {@link module:clipboard/clipboardobserver~ClipboardObserver}
- * needs to be added to {@link module:engine/view/document~Document} by the {@link module:engine/view/view~View#addObserver} method.
- * It's done by the {@link module:clipboard/clipboard~Clipboard} feature. If it's not loaded, it must be done manually.
+ * Note that this event is not available by default. To make it available, {@link module:clipboard/clipboardobserver~ClipboardObserver}
+ * needs to be added to the {@link module:engine/view/document~Document} by the {@link module:engine/view/view~View#addObserver} method.
+ * This is done by the {@link module:clipboard/clipboard~Clipboard} feature. If it is not loaded, it must be done manually.
  *
  * @see module:clipboard/clipboardobserver~ClipboardObserver
  * @event module:engine/view/document~Document#event:cut
- * @param {module:clipboard/clipboardobserver~ClipboardEventData} data Event data.
+ * @param {module:clipboard/clipboardobserver~ClipboardEventData} data The event data.
  */
 
 /**
  * The value of the {@link module:engine/view/document~Document#event:paste},
  * {@link module:engine/view/document~Document#event:copy} and {@link module:engine/view/document~Document#event:cut} events.
  *
- * In order to access clipboard data use `dataTransfer` property.
+ * In order to access the clipboard data, use the `dataTransfer` property.
  *
  * @class module:clipboard/clipboardobserver~ClipboardEventData
  * @extends module:engine/view/observer/domeventdata~DomEventData
  */
 
 /**
- * Data transfer instance.
+ * The data transfer instance.
  *
  * @readonly
  * @member {module:clipboard/datatransfer~DataTransfer} module:clipboard/clipboardobserver~ClipboardEventData#dataTransfer

+ 1 - 1
packages/ckeditor5-clipboard/src/pasteplaintext.js

@@ -14,7 +14,7 @@ import ClipboardObserver from './clipboardobserver';
 /**
  * The plugin detects user intentions for pasting plain text.
  *
- * For example, it detects <kbd>ctrl/cmd</kbd> + <kbd>shift</kbd> + <kbd>ctrl/v</kbd> keystroke.
+ * For example, it detects <kbd>Ctrl/Cmd</kbd> + <kbd>Shift</kbd> + <kbd>V</kbd> keystroke.
  *
  * @extends module:core/plugin~Plugin
  */

+ 2 - 2
packages/ckeditor5-code-block/src/codeblockui.js

@@ -54,7 +54,7 @@ export default class CodeBlockUI extends Plugin {
 					language: defaultLanguageDefinition.language
 				} );
 
-				editor.editing.view.focus();
+				editor.focus();
 			} );
 
 			dropdownView.on( 'execute', evt => {
@@ -63,7 +63,7 @@ export default class CodeBlockUI extends Plugin {
 					forceValue: true
 				} );
 
-				editor.editing.view.focus();
+				editor.focus();
 			} );
 
 			dropdownView.class = 'ck-code-block-dropdown';

+ 7 - 0
packages/ckeditor5-core/src/editor/editor.js

@@ -296,6 +296,13 @@ export default class Editor {
 		}
 	}
 
+	/**
+	 * Puts the focus in the editor's editing view.
+	 */
+	focus() {
+		this.editing.view.focus();
+	}
+
 	/**
 	 * Creates and initializes a new editor instance.
 	 *

+ 12 - 0
packages/ckeditor5-core/tests/editor/editor.js

@@ -556,6 +556,18 @@ describe( 'Editor', () => {
 		} );
 	} );
 
+	describe( 'focus()', () => {
+		it( 'should call view\'s focus() method', () => {
+			const editor = new TestEditor();
+			const focusSpy = sinon.spy( editor.editing.view, 'focus' );
+
+			editor.editing.view.document.isFocused = true;
+			editor.focus();
+
+			expect( focusSpy.calledOnce ).to.be.true;
+		} );
+	} );
+
 	describe( 'create()', () => {
 		it( 'should return a promise that resolves properly', () => {
 			const promise = TestEditor.create();

+ 1 - 1
packages/ckeditor5-core/tests/manual/readonly.js

@@ -28,7 +28,7 @@ ClassicEditor
 			editor.isReadOnly = !editor.isReadOnly;
 			button.textContent = editor.isReadOnly ? 'Turn off read-only mode' : 'Turn on read-only mode';
 
-			editor.editing.view.focus();
+			editor.focus();
 		} );
 	} )
 	.catch( err => {

+ 1 - 1
packages/ckeditor5-font/src/fontfamily/fontfamilyui.js

@@ -55,7 +55,7 @@ export default class FontFamilyUI extends Plugin {
 			// Execute command when an item from the dropdown is selected.
 			this.listenTo( dropdownView, 'execute', evt => {
 				editor.execute( evt.source.commandName, { value: evt.source.commandParam } );
-				editor.editing.view.focus();
+				editor.focus();
 			} );
 
 			return dropdownView;

+ 1 - 1
packages/ckeditor5-font/src/fontsize/fontsizeui.js

@@ -60,7 +60,7 @@ export default class FontSizeUI extends Plugin {
 			// Execute command when an item from the dropdown is selected.
 			this.listenTo( dropdownView, 'execute', evt => {
 				editor.execute( evt.source.commandName, { value: evt.source.commandParam } );
-				editor.editing.view.focus();
+				editor.focus();
 			} );
 
 			return dropdownView;

+ 1 - 1
packages/ckeditor5-font/src/ui/colorui.js

@@ -126,7 +126,7 @@ export default class ColorUI extends Plugin {
 
 			dropdownView.on( 'execute', ( evt, data ) => {
 				editor.execute( this.commandName, data );
-				editor.editing.view.focus();
+				editor.focus();
 			} );
 
 			dropdownView.on( 'change:isOpen', ( evt, name, isVisible ) => {

+ 1 - 1
packages/ckeditor5-heading/src/headingbuttonsui.js

@@ -94,7 +94,7 @@ export default class HeadingButtonsUI extends Plugin {
 
 			view.on( 'execute', () => {
 				editor.execute( 'heading', { value: option.model } );
-				editor.editing.view.focus();
+				editor.focus();
 			} );
 
 			return view;

+ 1 - 1
packages/ckeditor5-heading/src/headingui.js

@@ -101,7 +101,7 @@ export default class HeadingUI extends Plugin {
 			// Execute command when an item from the dropdown is selected.
 			this.listenTo( dropdownView, 'execute', evt => {
 				editor.execute( evt.source.commandName, evt.source.commandValue ? { value: evt.source.commandValue } : undefined );
-				editor.editing.view.focus();
+				editor.focus();
 			} );
 
 			return dropdownView;

+ 2 - 2
packages/ckeditor5-highlight/src/highlightui.js

@@ -149,7 +149,7 @@ export default class HighlightUI extends Plugin {
 
 			buttonView.on( 'execute', () => {
 				editor.execute( 'highlight', { value } );
-				editor.editing.view.focus();
+				editor.focus();
 			} );
 
 			// Add additional behavior for buttonView.
@@ -228,7 +228,7 @@ export default class HighlightUI extends Plugin {
 			// Execute current action from dropdown's split button action button.
 			splitButtonView.on( 'execute', () => {
 				editor.execute( 'highlight', { value: splitButtonView.commandValue } );
-				editor.editing.view.focus();
+				editor.focus();
 			} );
 
 			// Returns active highlighter option depending on current command value.

+ 1 - 1
packages/ckeditor5-horizontal-line/src/horizontallineui.js

@@ -37,7 +37,7 @@ export default class HorizontalLineUI extends Plugin {
 			// Execute the command.
 			this.listenTo( view, 'execute', () => {
 				editor.execute( 'horizontalLine' );
-				editor.editing.view.focus();
+				editor.focus();
 			} );
 
 			return view;

+ 1 - 1
packages/ckeditor5-html-embed/src/htmlembedui.js

@@ -38,7 +38,7 @@ export default class HtmlEmbedUI extends Plugin {
 			// Execute the command.
 			this.listenTo( view, 'execute', () => {
 				editor.execute( 'insertHtmlEmbed' );
-				editor.editing.view.focus();
+				editor.focus();
 
 				const widgetWrapper = editor.editing.view.document.selection.getSelectedElement();
 

+ 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 - 1
packages/ckeditor5-image/src/imageinsert/imageinsertui.js

@@ -145,7 +145,7 @@ export default class ImageInsertUI extends Plugin {
 		}
 
 		function closePanel() {
-			editor.editing.view.focus();
+			editor.focus();
 			dropdownView.isOpen = false;
 		}
 

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

@@ -185,7 +185,7 @@ export default class ImageResizeButtons extends Plugin {
 			// Execute command when an item from the dropdown is selected.
 			this.listenTo( dropdownView, 'execute', evt => {
 				editor.execute( evt.source.commandName, { width: evt.source.commandValue } );
-				editor.editing.view.focus();
+				editor.focus();
 			} );
 
 			return dropdownView;

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

+ 1 - 1
packages/ckeditor5-image/src/imagestyle/imagestyleui.js

@@ -94,7 +94,7 @@ export default class ImageStyleUI extends Plugin {
 
 			this.listenTo( view, 'execute', () => {
 				editor.execute( 'imageStyle', { value: style.name } );
-				editor.editing.view.focus();
+				editor.focus();
 			} );
 
 			return view;

+ 1 - 1
packages/ckeditor5-image/src/imagetextalternative/imagetextalternativeui.js

@@ -206,7 +206,7 @@ export default class ImageTextAlternativeUI extends Plugin {
 		this._balloon.remove( this._form );
 
 		if ( focusEditable ) {
-			this.editor.editing.view.focus();
+			this.editor.focus();
 		}
 	}
 

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

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

+ 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-indent/src/indentui.js

@@ -70,7 +70,7 @@ export default class IndentUI extends Plugin {
 
 			this.listenTo( view, 'execute', () => {
 				editor.execute( commandName );
-				editor.editing.view.focus();
+				editor.focus();
 			} );
 
 			return view;

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

@@ -375,7 +375,7 @@ export default class LinkUI extends Plugin {
 
 			// Because the form has an input which has focus, the focus must be brought back
 			// to the editor. Otherwise, it would be lost.
-			this.editor.editing.view.focus();
+			this.editor.focus();
 
 			this._hideFakeVisualSelection();
 		}
@@ -443,7 +443,7 @@ export default class LinkUI extends Plugin {
 
 		// Make sure the focus always gets back to the editable _before_ removing the focused form view.
 		// Doing otherwise causes issues in some browsers. See https://github.com/ckeditor/ckeditor5-link/issues/193.
-		editor.editing.view.focus();
+		editor.focus();
 
 		// Remove form first because it's on top of the stack.
 		this._removeFormView();

+ 2 - 2
packages/ckeditor5-list/src/liststyleui.js

@@ -158,7 +158,7 @@ function getSplitButtonCreator( { editor, parentCommandName, buttonLabel, button
 
 		splitButtonView.on( 'execute', () => {
 			editor.execute( parentCommandName );
-			editor.editing.view.focus();
+			editor.focus();
 		} );
 
 		splitButtonView.set( {
@@ -222,7 +222,7 @@ function getStyleButtonCreator( { editor, listStyleCommand, parentCommandName }
 				} );
 			}
 
-			editor.editing.view.focus();
+			editor.focus();
 		} );
 
 		return button;

+ 1 - 1
packages/ckeditor5-list/src/utils.js

@@ -262,7 +262,7 @@ export function createUIComponent( editor, commandName, label, icon ) {
 		// Execute command.
 		buttonView.on( 'execute', () => {
 			editor.execute( commandName );
-			editor.editing.view.focus();
+			editor.focus();
 		} );
 
 		return buttonView;

+ 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.

+ 1 - 1
packages/ckeditor5-media-embed/src/mediaembedui.js

@@ -101,7 +101,7 @@ export default class MediaEmbedUI extends Plugin {
 		dropdown.on( 'cancel', () => closeUI() );
 
 		function closeUI() {
-			editor.editing.view.focus();
+			editor.focus();
 			dropdown.isOpen = false;
 		}
 	}

+ 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(

+ 1 - 1
packages/ckeditor5-mention/src/mentionui.js

@@ -253,7 +253,7 @@ export default class MentionUI extends Plugin {
 				range
 			} );
 
-			editor.editing.view.focus();
+			editor.focus();
 		} );
 
 		return mentionsView;

+ 1 - 1
packages/ckeditor5-page-break/src/pagebreakui.js

@@ -37,7 +37,7 @@ export default class PageBreakUI extends Plugin {
 			// Execute command.
 			this.listenTo( view, 'execute', () => {
 				editor.execute( 'pageBreak' );
-				editor.editing.view.focus();
+				editor.focus();
 			} );
 
 			return view;

+ 7 - 1
packages/ckeditor5-paste-from-office/docs/features/paste-from-google-docs.md

@@ -30,6 +30,12 @@ To test how Paste from Office works, open the [sample Google Docs document](http
 
 {@snippet features/paste-from-office}
 
+## Related features
+ 
+CKEditor 5 supports a wider range of paste features, including:
+* {@link features/paste-plain-text Paste plain text} &ndash; Paste text without formatting that will inherit the style of the content it was pasted into.
+* {@link features/paste-from-word Paste from Word} &ndash; Paste content from Microsoft Word and maintain the original structure and formatting.
+
 ## Automatic content filtering
 
 With CKEditor 5 you do not need to worry about pasting messy content from Google Docs (or any other possible sources). Thanks to the CKEditor 5 {@link framework/guides/overview custom data model}, only content which is specifically handled by the loaded rich-text editor features will be preserved.
@@ -44,7 +50,7 @@ This means that if you did not enable, for instance, {@link features/font font f
 
 To add this feature to your rich-text editor, install the [`@ckeditor/ckeditor5-paste-from-office`](https://www.npmjs.com/package/@ckeditor/ckeditor5-paste-from-office) package:
 
-```bash
+```
 npm install --save @ckeditor/ckeditor5-paste-from-office
 ```
 

+ 7 - 1
packages/ckeditor5-paste-from-office/docs/features/paste-from-word.md

@@ -30,6 +30,12 @@ To test how Paste from Office works, download the [sample Word document](../../a
 
 {@snippet features/paste-from-office}
 
+## Related features
+ 
+CKEditor 5 supports a wider range of paste features, including:
+* {@link features/paste-from-google-docs Paste from Google Docs} &ndash; Paste content from Google Docs, maintaining the original formatting and structure.
+* {@link features/paste-plain-text Paste plain text} &ndash; Paste text without formatting that will inherit the style of the content it was pasted into.
+
 ## Automatic content filtering
 
 With CKEditor 5 you do not need to worry about pasting messy content from Microsoft Word (or any other possible sources). Thanks to the CKEditor 5 {@link framework/guides/overview custom data model}, only content which is specifically handled by the loaded rich-text editor features will be preserved.
@@ -44,7 +50,7 @@ This means that if you did not enable, for instance, {@link features/font font f
 
 To add this feature to your rich-text editor, install the [`@ckeditor/ckeditor5-paste-from-office`](https://www.npmjs.com/package/@ckeditor/ckeditor5-paste-from-office) package:
 
-```bash
+```
 npm install --save @ckeditor/ckeditor5-paste-from-office
 ```
 

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

部分文件因文件數量過多而無法顯示