8
0
Просмотр исходного кода

Merge branch 'master' into t/ckeditor5/746

Maciej Gołaszewski 6 лет назад
Родитель
Сommit
f34744bb77
28 измененных файлов с 696 добавлено и 194 удалено
  1. 1 3
      packages/ckeditor5-core/.travis.yml
  2. 29 1
      packages/ckeditor5-core/CHANGELOG.md
  3. 0 1
      packages/ckeditor5-core/README.md
  4. 25 0
      packages/ckeditor5-core/lang/translations/af.po
  5. 17 17
      packages/ckeditor5-core/package.json
  6. 1 1
      packages/ckeditor5-core/src/commandcollection.js
  7. 16 0
      packages/ckeditor5-core/src/editor/editor.js
  8. 4 4
      packages/ckeditor5-core/src/editor/editorconfig.jsdoc
  9. 53 5
      packages/ckeditor5-core/src/editor/editorui.js
  10. 4 1
      packages/ckeditor5-core/src/editor/utils/attachtoform.js
  11. 4 1
      packages/ckeditor5-core/src/editor/utils/elementapimixin.js
  12. 99 0
      packages/ckeditor5-core/src/multicommand.js
  13. 1 1
      packages/ckeditor5-core/src/pendingactions.js
  14. 14 9
      packages/ckeditor5-core/src/plugincollection.js
  15. 57 4
      packages/ckeditor5-core/tests/_utils-tests/classictesteditor.js
  16. 36 0
      packages/ckeditor5-core/tests/_utils-tests/virtualtesteditor.js
  17. 45 16
      packages/ckeditor5-core/tests/_utils/classictesteditor.js
  18. 10 10
      packages/ckeditor5-core/tests/_utils/utils.js
  19. 3 4
      packages/ckeditor5-core/tests/_utils/virtualtesteditor.js
  20. 6 3
      packages/ckeditor5-core/tests/commandcollection.js
  21. 3 3
      packages/ckeditor5-core/tests/editor/editor.js
  22. 56 10
      packages/ckeditor5-core/tests/editor/editorui.js
  23. 6 4
      packages/ckeditor5-core/tests/editor/utils/attachtoform.js
  24. 6 2
      packages/ckeditor5-core/tests/editor/utils/elementapimixin.js
  25. 124 0
      packages/ckeditor5-core/tests/multicommand.js
  26. 3 3
      packages/ckeditor5-core/tests/pendingactions.js
  27. 20 18
      packages/ckeditor5-core/tests/plugin.js
  28. 53 73
      packages/ckeditor5-core/tests/plugincollection.js

+ 1 - 3
packages/ckeditor5-core/.travis.yml

@@ -28,6 +28,4 @@ after_script:
 - ckeditor5-dev-tests-notify-travis-status
 env:
   global:
-  - secure: uJiLhULzhBDdkuQrYW7k7H4l4HizyfCdnV9ORe2HXyNnWFnFfAhc2vD1FQ5182ldQXtUQMX51gM/YL5r1Sa2IO3EW/j0gyuznqYvM8KX4lU1hXUAap1ZHW7Zb/leVQfU76IH688rkGcpr2SwCG6/LglqunozwhyePuxBzH23whSrf5dGXjswnMUAjouavtm+YybPZY81uj181aFY7nbcWOCUkx7FGaIg5WqG27kkYuUCVDw977hlwSevL6K1AF1OMtfHTAt4NR2oRKtpcRsrBlVNFFCraVa+3E1DN/kcEMaNDFJ4737Zta6jK9irGdfOwvtt1t4Og2fWKtUNCix2Almjiu0t58miuHhZenB4xRm4kpnc5nzfu0x/Ur4R95OHbKMf/6Zvvd/2v4bHe0iFihZXB8qiXvqZwqVyXCH2pWzj+tFztwpvyfj+Z1k4LIuo3shbG9GlK8mSl0OfL9WdSR/g09NBRg1aJtEKsJreensTEO9esJ5dD7JksKo6VsESSAztN4v5xuavtPM+yT2g2vFWpwyY0/6/yxR8hlIo0//24t8be9Lm+G3k1f++Jc6ea8aFYUPF+O6kdZgqD0g2LoAp5e28CjXkMDoq/0l+Ab0obpaMjZlyEZ+S8X+8+kyICpyADmLTWP/Mwmik8ixzABgBz0903w4aExOl8mEi9ws=
-  - secure: tVEuv/7HAhriDudbEP0+30DvPV04pI3XoW3ibHDVdI7qhjtNH4AnYcqJOtI25wZTYr8hUquqkFNM7nl4sSoPPHEgdLL/c9U1bLkmTfQD4l+e6OPL8V2D9qStnSvswDMevtoCAVep+yKU7tDwL+ppNZeC9SX4xigo8MMC0qKhgx/q2CWIOlDVURFbFeEss+RI6nn1nxO+1kyyiZ1O0hyh5Xkn2u9h5ihV3SAeAHfZyvKvhyii74jSjHBHiapKkwUr4WSUkmKwuaqA2OppZXiDf5ah5nbjh1kX8u90jN+1yFH0Aor+M1Qdejs8A5sg9spOU2L0OXlw1wiLj0gzrGEQH6wbTR2T8nyIYYpivnuPUh19WK6FCABCbH3baxJwpZdYrd87Oi2EAPDS8obpNc/fZEj5XrZBVIf8Tk9wvaDrB4JSsyjp8U8G5eKVrY++sRdCTGxpP1W9LJlkVSTW8weX1FWWo6OWnZGo7+Q6xd63wVZ7/dc0TU/h9Ck9ekTeOAU2/to71EnPZzU4Ydv5R/BK1VK0PUIIThosFIvd7UwrnVzmgndovIAfas+XhxgpJCWN+9pXPA46OfFNyYl8naiLdJqCOpl40tWNCE6T8rC60seTUSrDJwGEForCfr3eu2fkl0x5syc7eOimHHECBIGk4dE3TwwIg2kf6PUHMIT433M=
-  - secure: luFopu3v12G0Rn1nCriEa9TeKsghtiDezeFBCQ7pWxlpSDZKjt07x6Sd10h6xR6ixotHFSkiZX+Zsbsu6MAI2PjP1v49PIQkJFm0gyAxC2adVCn9KVf5Q/ag1/gF8Rn9L0MdjG/netiu5vOqTfHnASF953fibc/q53HQpIaKL/8cizipz0L0V8sT6lgCKna+K2OFLQ8wb/RZ6FwmrM+35KzuyxUdcXKUcPmGen6i3cJGeyB5TT2wPe7JYs4mYNuhREI0XzuPR8TsU37KYsl0sR8UTYClUoGVp/abbyHwTxuxlHnW/E1sYArmaFi84Y7bOhLD3P9VR+Ts9imYp38oLgF35FnynkOfL2BZnXzx25eUQXbDb9y6lq1J73ECTt3Iw+G13Y3YmLVGfNRdEq12CgD/V/BqgAkltYCo6AgWWyTPnToI636NqL1c+a4ruVfp6qt082BS+SpduiNIu+x/0OlMPqe+TbEBCxAd7jDN6sfhYAiCUMfUcTAFO4O/drodcyGsqAz6rPtmzDHMfX1sAUsBY9Q0jIhP/8fA+zf4br47Q37Gg8ilCw7sZ4QF+83wi4Z7poIl2U0EG3qhCyj0/V/I/mtJ/2A1ZHzO997NNrT61mqCO2tBFWBgZKrm62NWFItTIk1L7ZnUiUj+tZEyaD8HX0YMam2LJYlLo4BVWOU=
+  - secure: TEv7Uuc8Xcd+AaR8W+s7qt/synWT60kR8/8QNiqBmdFDYC3LNdoI48V8vh8rIbdtYk1u6Ur+6pBLDC1+gMyA6+oAY5SVKdiep7JgJUkYEygCtr4chyiatbZezuKmWs9GxJ8awhZGeFuKNt/jwyVxMqYHDQ1t0Mb4+AyYB60h6o0Mx8IQ4rnmVmcCe45NZNe24a2a2ao5IX3Y3INrdXKBZ780mfDQrCWRa404MZ3ZoVdVeKoEkX3OiuKnsiCDIcKu89IVLRW7mq1kw84G8npDNbddU8sGtS6RdfSZ61M1jp1yHNXi/2/tLQyE0N8UF3bYHy9GzqKC6D4VGLLykGgv+yd95y0arwMMdi2gfrg3nE2Zehu/kuTRwMSBMwkhScZAhixYOjX/AFGd7EAmt4/wfBrNNCs29lCiOUpR7wC1ZSNCMbVxkNlAoI1J/2MoBKALqLdBfYGcH1XwfIwx/w62dqiE/zlnDYgdBursnpa0sGXMau1xAA+5ud+oHwmKTkMjKeszQC5YaWLS+qtTX/y2TnePvbzO8yTJrLDt+8tVAWsbUrWe9Uk+zuLNFxhNVmBbaQD3hhkRIm/ElCQROIA7tGRRyksuG63m1GBWZU6tIeiK6fitdXpgb+UpIru+Qc0p48XAz68zdslJg/4N3wcFuacNNdVqA3ZWd2Rsp8LO7t4=

+ 29 - 1
packages/ckeditor5-core/CHANGELOG.md

@@ -1,6 +1,34 @@
 Changelog
 =========
 
+## [12.2.1](https://github.com/ckeditor/ckeditor5-core/compare/v12.2.0...v12.2.1) (2019-07-10)
+
+Internal changes only (updated dependencies, documentation, etc.).
+
+
+## [12.2.0](https://github.com/ckeditor/ckeditor5-core/compare/v12.1.1...v12.2.0) (2019-07-04)
+
+### Features
+
+* Added an editor instance reference to the native editable DOM element under the `ckeditorInstance` property. Closes [ckeditor/ckeditor5#1838](https://github.com/ckeditor/ckeditor5/issues/1838). ([fa94600](https://github.com/ckeditor/ckeditor5-core/commit/fa94600))
+
+  Implemented the `EditorUI#setEditableElement()` method.
+  Deprecated the `EditorUI#_editableElements` property.
+* Introduced `MultiCommand` which acts as a composite command – it can group multiple commands under the hood. ([ebcbd01](https://github.com/ckeditor/ckeditor5-core/commit/ebcbd01))
+
+### Other changes
+
+* Introduce the `editor-wrong-element` error thrown when the editor is created over a wrong element. Closes [ckeditor/ckeditor5#1591](https://github.com/ckeditor/ckeditor5/issues/1591). ([9945fc6](https://github.com/ckeditor/ckeditor5-core/commit/9945fc6))
+* Updated translations. ([43f5b6e](https://github.com/ckeditor/ckeditor5-core/commit/43f5b6e))
+
+
+## [12.1.1](https://github.com/ckeditor/ckeditor5-core/compare/v12.1.0...v12.1.1) (2019-06-05)
+
+### Other changes
+
+* Updated translations. ([ed6bd3c](https://github.com/ckeditor/ckeditor5-core/commit/ed6bd3c))
+
+
 ## [12.1.0](https://github.com/ckeditor/ckeditor5-core/compare/v12.0.0...v12.1.0) (2019-04-10)
 
 ### Features
@@ -10,7 +38,7 @@ Changelog
 ### Other changes
 
 * Optimized icons. ([a5f8d34](https://github.com/ckeditor/ckeditor5-core/commit/a5f8d34))
-* Updated translations. ([2dedc43](https://github.com/ckeditor/ckeditor5-core/commit/2dedc43)) 
+* Updated translations. ([2dedc43](https://github.com/ckeditor/ckeditor5-core/commit/2dedc43))
 
 
 ## [12.0.0](https://github.com/ckeditor/ckeditor5-core/compare/v11.1.0...v12.0.0) (2019-02-28)

+ 0 - 1
packages/ckeditor5-core/README.md

@@ -4,7 +4,6 @@ CKEditor 5 core editor architecture
 [![Join the chat at https://gitter.im/ckeditor/ckeditor5](https://badges.gitter.im/ckeditor/ckeditor5.svg)](https://gitter.im/ckeditor/ckeditor5?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
 [![npm version](https://badge.fury.io/js/%40ckeditor%2Fckeditor5-core.svg)](https://www.npmjs.com/package/@ckeditor/ckeditor5-core)
 [![Build Status](https://travis-ci.org/ckeditor/ckeditor5-core.svg?branch=master)](https://travis-ci.org/ckeditor/ckeditor5-core)
-[![BrowserStack Status](https://automate.browserstack.com/automate/badge.svg?badge_key=d3hvenZqQVZERFQ5d09FWXdyT0ozVXhLaVltRFRjTTUyZGpvQWNmWVhUUT0tLUZqNlJ1YWRUd0RvdEVOaEptM1B2Q0E9PQ==--c9d3dee40b9b4471ff3fb516d9ecf8d09292c7e0)](https://automate.browserstack.com/public-build/d3hvenZqQVZERFQ5d09FWXdyT0ozVXhLaVltRFRjTTUyZGpvQWNmWVhUUT0tLUZqNlJ1YWRUd0RvdEVOaEptM1B2Q0E9PQ==--c9d3dee40b9b4471ff3fb516d9ecf8d09292c7e0)
 [![Coverage Status](https://coveralls.io/repos/github/ckeditor/ckeditor5-core/badge.svg?branch=master)](https://coveralls.io/github/ckeditor/ckeditor5-core?branch=master)
 <br>
 [![Dependency Status](https://david-dm.org/ckeditor/ckeditor5-core/status.svg)](https://david-dm.org/ckeditor/ckeditor5-core)

+ 25 - 0
packages/ckeditor5-core/lang/translations/af.po

@@ -0,0 +1,25 @@
+# Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+#
+#                                     !!! IMPORTANT !!!
+#
+#         Before you edit this file, please keep in mind that contributing to the project
+#                translations is possible ONLY via the Transifex online service.
+#
+#         To submit your translations, visit https://www.transifex.com/ckeditor/ckeditor5.
+#
+#                   To learn more, check out the official contributor's guide:
+#     https://ckeditor.com/docs/ckeditor5/latest/framework/guides/contributing/contributing.html
+#
+msgid ""
+msgstr ""
+"Language-Team: Afrikaans (https://www.transifex.com/ckeditor/teams/11143/af/)\n"
+"Language: af\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+msgctxt "Label for the Save button."
+msgid "Save"
+msgstr "Berg"
+
+msgctxt "Label for the Cancel button."
+msgid "Cancel"
+msgstr "Kanselleer"

+ 17 - 17
packages/ckeditor5-core/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@ckeditor/ckeditor5-core",
-  "version": "12.1.0",
+  "version": "12.2.1",
   "description": "The core architecture of CKEditor 5 – the best browser-based rich text editor.",
   "keywords": [
     "ckeditor5-lib",
@@ -21,26 +21,26 @@
     "framework"
   ],
   "dependencies": {
-    "@ckeditor/ckeditor5-engine": "^13.1.0",
-    "@ckeditor/ckeditor5-utils": "^12.1.0",
+    "@ckeditor/ckeditor5-engine": "^13.2.1",
+    "@ckeditor/ckeditor5-utils": "^13.0.1",
     "lodash-es": "^4.17.10"
   },
   "devDependencies": {
-    "@ckeditor/ckeditor5-autoformat": "^11.0.1",
-    "@ckeditor/ckeditor5-basic-styles": "^11.1.0",
-    "@ckeditor/ckeditor5-block-quote": "^11.0.1",
-    "@ckeditor/ckeditor5-editor-classic": "^12.1.0",
-    "@ckeditor/ckeditor5-essentials": "^11.0.1",
-    "@ckeditor/ckeditor5-heading": "^11.0.1",
-    "@ckeditor/ckeditor5-image": "^13.0.1",
-    "@ckeditor/ckeditor5-link": "^11.0.1",
-    "@ckeditor/ckeditor5-list": "^12.0.1",
-    "@ckeditor/ckeditor5-media-embed": "^11.1.0",
-    "@ckeditor/ckeditor5-paragraph": "^11.0.1",
-    "@ckeditor/ckeditor5-table": "^12.0.1",
-    "@ckeditor/ckeditor5-ui": "^12.1.0",
+    "@ckeditor/ckeditor5-autoformat": "^11.0.4",
+    "@ckeditor/ckeditor5-basic-styles": "^11.1.3",
+    "@ckeditor/ckeditor5-block-quote": "^11.1.2",
+    "@ckeditor/ckeditor5-editor-classic": "^12.1.3",
+    "@ckeditor/ckeditor5-essentials": "^11.0.4",
+    "@ckeditor/ckeditor5-heading": "^11.0.4",
+    "@ckeditor/ckeditor5-image": "^13.1.2",
+    "@ckeditor/ckeditor5-link": "^11.1.1",
+    "@ckeditor/ckeditor5-list": "^12.0.4",
+    "@ckeditor/ckeditor5-media-embed": "^11.1.3",
+    "@ckeditor/ckeditor5-paragraph": "^11.0.4",
+    "@ckeditor/ckeditor5-table": "^13.0.2",
+    "@ckeditor/ckeditor5-ui": "^13.0.2",
     "eslint": "^5.5.0",
-    "eslint-config-ckeditor5": "^1.0.11",
+    "eslint-config-ckeditor5": "^2.0.0",
     "husky": "^1.3.1",
     "lint-staged": "^7.0.0"
   },

+ 1 - 1
packages/ckeditor5-core/src/commandcollection.js

@@ -62,7 +62,7 @@ export default class CommandCollection {
 			 * @error commandcollection-command-not-found
 			 * @param {String} commandName Name of the command.
 			 */
-			throw new CKEditorError( 'commandcollection-command-not-found: Command does not exist.', { commandName } );
+			throw new CKEditorError( 'commandcollection-command-not-found: Command does not exist.', this, { commandName } );
 		}
 
 		command.execute( ...args );

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

@@ -312,6 +312,22 @@ mix( Editor, ObservableMixin );
  * @event destroy
  */
 
+/**
+ * This error is thrown when a user tries to use a `<textarea>` element to create a non-classic editor in it.
+ *
+ * Textarea element represents a plain-text and cannot be used as a editable root element with included CKEditor5.
+ * Content of an editor should be nicely present to the user and show him how it's going to looks like. Textarea element
+ * doesn't support such behavior.
+ *
+ * Only {@glink builds/guides/overview#classic-editor Classic Editor} has implemented a special system, which
+ * **replace** DOM element and load data from it
+ * ({@link module:editor-classic/classiceditor~ClassicEditor.create more information}). All other editors
+ * use an existing element, load data from it and make this element editable. Details about behaviour of each editor
+ * might be found in an associated description of a `create` method of each editor.
+ *
+ * @error editor-wrong-element
+ */
+
 /**
  * An array of plugins built into this editor class.
  * It is used in CKEditor 5 builds to provide a list of plugins which are later automatically initialized

+ 4 - 4
packages/ckeditor5-core/src/editor/editorconfig.jsdoc

@@ -33,10 +33,10 @@
 /**
  * The initial editor data to be used instead of the provided element's HTML content.
  *
- *      ClassicEditor
- *          .create( document.querySelector( '#editor' ), {
- *              initialData: '<h2>Initial data</h2><p>Foo bar.</p>'
- *          } )
+ *		ClassicEditor
+ *			.create( document.querySelector( '#editor' ), {
+ *				initialData: '<h2>Initial data</h2><p>Foo bar.</p>'
+ *			} )
  *			.then( ... )
  *			.catch( ... );
  *

+ 53 - 5
packages/ckeditor5-core/src/editor/editorui.js

@@ -7,6 +7,8 @@
  * @module core/editor/editorui
  */
 
+/* globals console */
+
 import ComponentFactory from '@ckeditor/ckeditor5-ui/src/componentfactory';
 import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker';
 
@@ -54,10 +56,10 @@ export default class EditorUI {
 		/**
 		 * Stores all editable elements used by the editor instance.
 		 *
-		 * @protected
+		 * @private
 		 * @member {Map.<String,HTMLElement>}
 		 */
-		this._editableElements = new Map();
+		this._editableElementsMap = new Map();
 
 		// Informs UI components that should be refreshed after layout change.
 		this.listenTo( editor.editing.view.document, 'layoutChanged', () => this.update() );
@@ -100,7 +102,29 @@ export default class EditorUI {
 
 		this.focusTracker.destroy();
 
-		this._editableElements = new Map();
+		// Clean–up the references to the CKEditor instance stored in the native editable DOM elements.
+		for ( const domElement of this._editableElementsMap.values() ) {
+			domElement.ckeditorInstance = null;
+		}
+
+		this._editableElementsMap = new Map();
+	}
+
+	/**
+	 * Store the native DOM editable element used by the editor under
+	 * a unique name.
+	 *
+	 * @param {String} rootName The unique name of the editable element.
+	 * @param {HTMLElement} domElement The native DOM editable element.
+	 */
+	setEditableElement( rootName, domElement ) {
+		this._editableElementsMap.set( rootName, domElement );
+
+		// Put a reference to the CKEditor instance in the editable native DOM element.
+		// It helps 3rd–party software (browser extensions, other libraries) access and recognize
+		// CKEditor 5 instances (editing roots) and use their API (there is no global editor
+		// instance registry).
+		domElement.ckeditorInstance = this.editor;
 	}
 
 	/**
@@ -110,7 +134,7 @@ export default class EditorUI {
 	 * @returns {HTMLElement|undefined}
 	 */
 	getEditableElement( rootName = 'main' ) {
-		return this._editableElements.get( rootName );
+		return this._editableElementsMap.get( rootName );
 	}
 
 	/**
@@ -119,7 +143,31 @@ export default class EditorUI {
 	 * @returns {Iterable.<String>}
 	 */
 	getEditableElementsNames() {
-		return this._editableElements.keys();
+		return this._editableElementsMap.keys();
+	}
+
+	/**
+	 * Stores all editable elements used by the editor instance.
+	 *
+	 * @protected
+	 * @deprecated
+	 * @member {Map.<String,HTMLElement>}
+	 */
+	get _editableElements() {
+		/**
+		 * The {@link module:core/editor/editorui~EditorUI#_editableElements `EditorUI#_editableElements`} property has been
+		 * deprecated and will be removed in the near future. Please use {@link #setEditableElement `setEditableElement()`} and
+		 * {@link #getEditableElement `getEditableElement()`} methods instead.
+		 *
+		 * @error editor-ui-deprecated-editable-elements
+		 * @param {module:core/editor/editorui~EditorUI} editorUI Editor UI instance the deprecated property belongs to.
+		 */
+		console.warn(
+			'editor-ui-deprecated-editable-elements: ' +
+			'The EditorUI#_editableElements property has been deprecated and will be removed in the near future.',
+			{ editorUI: this } );
+
+		return this._editableElementsMap;
 	}
 
 	/**

+ 4 - 1
packages/ckeditor5-core/src/editor/utils/attachtoform.js

@@ -26,7 +26,10 @@ export default function attachToForm( editor ) {
 		 *
 		 * @error attachtoform-missing-elementapi-interface
 		 */
-		throw new CKEditorError( 'attachtoform-missing-elementapi-interface: Editor passed to attachToForm() must implement ElementApi.' );
+		throw new CKEditorError(
+			'attachtoform-missing-elementapi-interface: Editor passed to attachToForm() must implement ElementApi.',
+			editor
+		);
 	}
 
 	const sourceElement = editor.sourceElement;

+ 4 - 1
packages/ckeditor5-core/src/editor/utils/elementapimixin.js

@@ -30,7 +30,10 @@ const ElementApiMixin = {
 			 *
 			 * @error editor-missing-sourceelement
 			 */
-			throw new CKEditorError( 'editor-missing-sourceelement: Cannot update the source element of a detached editor.' );
+			throw new CKEditorError(
+				'editor-missing-sourceelement: Cannot update the source element of a detached editor.',
+				this
+			);
 		}
 
 		setDataInElement( this.sourceElement, this.data.get() );

+ 99 - 0
packages/ckeditor5-core/src/multicommand.js

@@ -0,0 +1,99 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+import Command from './command';
+
+/**
+ * @module core/multicommand
+ */
+
+/**
+ * A CKEditor command that aggregates other commands.
+ *
+ * This command is used to proxy multiple commands. The multi-command is enabled when
+ * at least one of its registered child commands is enabled.
+ * When executing a multi-command the first command that is enabled will be executed.
+ *
+ *		const multiCommand = new MultiCommand( editor );
+ *
+ *		const commandFoo = new Command( editor );
+ *		const commandBar = new Command( editor );
+ *
+ *		// Register child commands.
+ *		multiCommand.registerChildCommand( commandFoo );
+ *		multiCommand.registerChildCommand( commandBar );
+ *
+ *		// Enable one of the commands.
+ *		commandBar.isEnabled = true;
+ *
+ *		multiCommand.execute(); // Will execute commandBar.
+ *
+ * @extends module:core/command~Command
+ */
+export default class MultiCommand extends Command {
+	/**
+	 * @inheritDoc
+	 */
+	constructor( editor ) {
+		super( editor );
+
+		/**
+		 * Registered child commands.
+		 *
+		 * @type {Array.<module:core/command~Command>}
+		 * @private
+		 */
+		this._childCommands = [];
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	refresh() {
+		// Override base command refresh(): the command's state is changed when one of child commands changes states.
+	}
+
+	/**
+	 * Executes the first of it registered child commands.
+	 */
+	execute( ...args ) {
+		const command = this._getFirstEnabledCommand();
+
+		command.execute( args );
+	}
+
+	/**
+	 * Registers a child command.
+	 *
+	 * @param {module:core/command~Command} command
+	 */
+	registerChildCommand( command ) {
+		this._childCommands.push( command );
+
+		// Change multi command enabled state when one of registered commands changes state.
+		command.on( 'change:isEnabled', () => this._checkEnabled() );
+
+		this._checkEnabled();
+	}
+
+	/**
+	 * Checks if any of child commands is enabled.
+	 *
+	 * @private
+	 */
+	_checkEnabled() {
+		this.isEnabled = !!this._getFirstEnabledCommand();
+	}
+
+	/**
+	 * Returns a first enabled command or undefined if none of them is enabled.
+	 *
+	 * @returns {module:core/command~Command|undefined}
+	 * @private
+	 */
+	_getFirstEnabledCommand() {
+		return this._childCommands.find( command => command.isEnabled );
+	}
+}

+ 1 - 1
packages/ckeditor5-core/src/pendingactions.js

@@ -99,7 +99,7 @@ export default class PendingActions extends Plugin {
 			 *
 			 * @error pendingactions-add-invalid-message
 			 */
-			throw new CKEditorError( 'pendingactions-add-invalid-message: The message must be a string.' );
+			throw new CKEditorError( 'pendingactions-add-invalid-message: The message must be a string.', this );
 		}
 
 		const action = Object.create( ObservableMixin );

+ 14 - 9
packages/ckeditor5-core/src/plugincollection.js

@@ -7,8 +7,9 @@
  * @module core/plugincollection
  */
 
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
-import log from '@ckeditor/ckeditor5-utils/src/log';
+/* globals console */
+
+import CKEditorError, { attachLinkToDocumentation } from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
 import mix from '@ckeditor/ckeditor5-utils/src/mix';
@@ -119,7 +120,7 @@ export default class PluginCollection {
 				pluginName = key.pluginName || key.name;
 			}
 
-			throw new CKEditorError( errorMsg, { plugin: pluginName } );
+			throw new CKEditorError( errorMsg, this._editor, { plugin: pluginName } );
 		}
 
 		return plugin;
@@ -189,9 +190,9 @@ export default class PluginCollection {
 			const errorMsg = 'plugincollection-plugin-not-found: Some plugins are not available and could not be loaded.';
 
 			// Log the error so it's more visible on the console. Hopefully, for better DX.
-			log.error( errorMsg, { plugins: missingPlugins } );
+			console.error( attachLinkToDocumentation( errorMsg ), { plugins: missingPlugins } );
 
-			return Promise.reject( new CKEditorError( errorMsg, { plugins: missingPlugins } ) );
+			return Promise.reject( new CKEditorError( errorMsg, this._editor, { plugins: missingPlugins } ) );
 		}
 
 		return Promise.all( pluginConstructors.map( loadPlugin ) )
@@ -231,7 +232,9 @@ export default class PluginCollection {
 					 * @error plugincollection-load
 					 * @param {String} plugin The name of the plugin that could not be loaded.
 					 */
-					log.error( 'plugincollection-load: It was not possible to load the plugin.', { plugin: PluginConstructor } );
+					console.error( attachLinkToDocumentation(
+						'plugincollection-load: It was not possible to load the plugin.'
+					), { plugin: PluginConstructor } );
 
 					throw err;
 				} );
@@ -266,6 +269,7 @@ export default class PluginCollection {
 							throw new CKEditorError(
 								'plugincollection-required: Cannot load a plugin because one of its dependencies is listed in' +
 								'the `removePlugins` option.',
+								editor,
 								{ plugin: RequiredPluginConstructor, requiredBy: PluginConstructor }
 							);
 						}
@@ -370,13 +374,14 @@ export default class PluginCollection {
 			 * @param {Function} plugin1 The first plugin constructor.
 			 * @param {Function} plugin2 The second plugin constructor.
 			 */
-			log.warn(
+			throw new CKEditorError(
 				'plugincollection-plugin-name-conflict: Two plugins with the same name were loaded.',
+				null,
 				{ pluginName, plugin1: this._plugins.get( pluginName ).constructor, plugin2: PluginConstructor }
 			);
-		} else {
-			this._plugins.set( pluginName, plugin );
 		}
+
+		this._plugins.set( pluginName, plugin );
 	}
 }
 

+ 57 - 4
packages/ckeditor5-core/tests/_utils-tests/classictesteditor.js

@@ -10,6 +10,7 @@ import ClassicTestEditor from '../../tests/_utils/classictesteditor';
 
 import Plugin from '../../src/plugin';
 import HtmlDataProcessor from '@ckeditor/ckeditor5-engine/src/dataprocessor/htmldataprocessor';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 
 import EditorUI from '../../src/editor/editorui';
 import BoxedEditorUIView from '@ckeditor/ckeditor5-ui/src/editorui/boxed/boxededitoruiview';
@@ -21,6 +22,7 @@ import RootElement from '@ckeditor/ckeditor5-engine/src/model/rootelement';
 
 import { getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import testUtils from '../../tests/_utils/utils';
+import { assertCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 describe( 'ClassicTestEditor', () => {
 	let editorElement;
@@ -32,13 +34,17 @@ describe( 'ClassicTestEditor', () => {
 		document.body.appendChild( editorElement );
 	} );
 
+	afterEach( () => {
+		editorElement.remove();
+	} );
+
 	describe( 'constructor()', () => {
 		it( 'creates an instance of editor', () => {
 			const editor = new ClassicTestEditor( editorElement, { foo: 1 } );
 
 			expect( editor ).to.be.instanceof( Editor );
 			expect( editor.config.get( 'foo' ) ).to.equal( 1 );
-			expect( editor.element ).to.equal( editorElement );
+			expect( editor.sourceElement ).to.equal( editorElement );
 			expect( editor.ui ).to.be.instanceOf( EditorUI );
 			expect( editor.ui.view ).to.be.instanceOf( BoxedEditorUIView );
 			expect( editor.data.processor ).to.be.instanceof( HtmlDataProcessor );
@@ -82,7 +88,7 @@ describe( 'ClassicTestEditor', () => {
 					expect( editor ).to.be.instanceof( ClassicTestEditor );
 
 					expect( editor.config.get( 'foo' ) ).to.equal( 1 );
-					expect( editor ).to.have.property( 'element', editorElement );
+					expect( editor.sourceElement ).to.equal( editorElement );
 				} );
 		} );
 
@@ -175,6 +181,53 @@ describe( 'ClassicTestEditor', () => {
 					return editor.destroy();
 				} );
 		} );
+
+		it( 'initializes the data controller with `config.initialData` if this option is provided', () => {
+			return ClassicTestEditor.create( editorElement, { initialData: '<p>foo</p>', plugins: [ Paragraph ] } )
+				.then( editor => {
+					expect( editor.getData() ).to.equal( '<p>foo</p>' );
+
+					return editor.destroy();
+				} );
+		} );
+
+		it( 'initializes the data controller with an empty string if the `config.initialData` is not provided', () => {
+			return ClassicTestEditor.create( editorElement )
+				.then( editor => {
+					expect( editor.getData() ).to.equal( '' );
+
+					return editor.destroy();
+				} );
+		} );
+
+		it( 'initializes the data controller with the data from the source element', () => {
+			editorElement.innerHTML = '<p>foo</p>';
+
+			return ClassicTestEditor.create( editorElement, { plugins: [ Paragraph ] } )
+				.then( editor => {
+					expect( editor.getData() ).to.equal( '<p>foo</p>' );
+
+					return editor.destroy();
+				} );
+		} );
+
+		it( 'initializes the data controller with the data from the first argument if it is a string', () => {
+			return ClassicTestEditor.create( '<p>foo</p>', { plugins: [ Paragraph ] } )
+				.then( editor => {
+					expect( editor.getData() ).to.equal( '<p>foo</p>' );
+
+					return editor.destroy();
+				} );
+		} );
+
+		it( 'throws when the data is passed from as the first argument and as a `config.initialData` at the same time', () => {
+			return ClassicTestEditor.create( '<p>foo</p>', { initialData: '<p>bar</p>' } )
+				.then( () => {
+					throw new Error( 'It should throw an error' );
+				}, err => {
+					assertCKEditorError( err, /^editor-create-initial-data:/, null );
+				} );
+		} );
 	} );
 
 	describe( 'destroy()', () => {
@@ -195,11 +248,11 @@ describe( 'ClassicTestEditor', () => {
 		it( 'restores the editor element', () => {
 			return ClassicTestEditor.create( editorElement, { foo: 1 } )
 				.then( editor => {
-					expect( editor.element.style.display ).to.equal( 'none' );
+					expect( editor.sourceElement.style.display ).to.equal( 'none' );
 
 					return editor.destroy()
 						.then( () => {
-							expect( editor.element.style.display ).to.equal( '' );
+							expect( editor.sourceElement.style.display ).to.equal( '' );
 						} );
 				} );
 		} );

+ 36 - 0
packages/ckeditor5-core/tests/_utils-tests/virtualtesteditor.js

@@ -9,6 +9,7 @@ import VirtualTestEditor from '../../tests/_utils/virtualtesteditor';
 import HtmlDataProcessor from '@ckeditor/ckeditor5-engine/src/dataprocessor/htmldataprocessor';
 import DataApiMixin from '../../src/editor/utils/dataapimixin';
 import RootElement from '@ckeditor/ckeditor5-engine/src/model/rootelement';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 
 import testUtils from '../../tests/_utils/utils';
 
@@ -34,4 +35,39 @@ describe( 'VirtualTestEditor', () => {
 			expect( testUtils.isMixed( VirtualTestEditor, DataApiMixin ) ).to.true;
 		} );
 	} );
+
+	describe( 'static create()', () => {
+		it( 'initializes the data controller with the `config.initialData`', () => {
+			return VirtualTestEditor.create( { initialData: '<p>foo</p>', plugins: [ Paragraph ] } )
+				.then( editor => {
+					expect( editor.getData() ).to.equal( '<p>foo</p>' );
+
+					return editor.destroy();
+				} );
+		} );
+
+		it( 'initializes the data controller with an empty string if the `config.initialData` is not provided', () => {
+			return VirtualTestEditor.create()
+				.then( editor => {
+					expect( editor.getData() ).to.equal( '' );
+
+					return editor.destroy();
+				} );
+		} );
+
+		it( 'fires the `data#ready` event once', () => {
+			const dataReadySpy = sinon.spy();
+
+			const Plugin = function( editor ) {
+				editor.data.on( 'ready', dataReadySpy );
+			};
+
+			return VirtualTestEditor.create( { plugins: [ Plugin ] } )
+				.then( editor => {
+					sinon.assert.calledOnce( dataReadySpy );
+
+					return editor.destroy();
+				} );
+		} );
+	} );
 } );

+ 45 - 16
packages/ckeditor5-core/tests/_utils/classictesteditor.js

@@ -13,6 +13,8 @@ import ElementReplacer from '@ckeditor/ckeditor5-utils/src/elementreplacer';
 import InlineEditableUIView from '@ckeditor/ckeditor5-ui/src/editableui/inline/inlineeditableuiview';
 import getDataFromElement from '@ckeditor/ckeditor5-utils/src/dom/getdatafromelement';
 import mix from '@ckeditor/ckeditor5-utils/src/mix';
+import { isElement } from 'lodash-es';
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 /**
  * A simplified classic editor. Useful for testing features.
@@ -24,51 +26,67 @@ export default class ClassicTestEditor extends Editor {
 	/**
 	 * @inheritDoc
 	 */
-	constructor( element, config ) {
+	constructor( sourceElementOrData, config ) {
 		super( config );
 
-		// The element on which the editor has been initialized.
-		this.element = element;
+		if ( isElement( sourceElementOrData ) ) {
+			this.sourceElement = sourceElementOrData;
+		}
 
 		// Use the HTML data processor in this editor.
 		this.data.processor = new HtmlDataProcessor();
 
+		// Create the ("main") root element of the model tree.
+		this.model.document.createRoot();
+
 		this.ui = new ClassicTestEditorUI( this, new BoxedEditorUIView( this.locale ) );
 
 		// Expose properties normally exposed by the ClassicEditorUI.
 		this.ui.view.editable = new InlineEditableUIView( this.ui.view.locale, this.editing.view );
-
-		// Create the ("main") root element of the model tree.
-		this.model.document.createRoot();
 	}
 
 	/**
 	 * @inheritDoc
 	 */
 	destroy() {
+		if ( this.sourceElement ) {
+			this.updateSourceElement();
+		}
+
 		this.ui.destroy();
 
 		return super.destroy();
 	}
 
 	/**
-	 * @inheritDoc
+	 * @param {HTMLElement|String} sourceElementOrData The DOM element that will be the source for the created editor
+	 * or the editor's initial data.
+	 * @param {module:core/editor/editorconfig~EditorConfig} [config] The editor configuration.
+	 * @returns {Promise} A promise resolved once the editor is ready. The promise resolves with the created editor instance.
 	 */
-	static create( element, config ) {
+	static create( sourceElementOrData, config = {} ) {
 		return new Promise( resolve => {
-			const editor = new this( element, config );
+			const editor = new this( sourceElementOrData, config );
 
 			resolve(
 				editor.initPlugins()
 					// Simulate EditorUI.init() (e.g. like in ClassicEditorUI). The ui#view
 					// should be rendered after plugins are initialized.
-					.then( () => editor.ui.init( element ) )
+					.then( () => editor.ui.init( isElement( sourceElementOrData ) ? sourceElementOrData : null ) )
 					.then( () => editor.editing.view.attachDomRoot( editor.ui.getEditableElement() ) )
-					.then( () => editor.data.init( getDataFromElement( element ) ) )
 					.then( () => {
-						editor.state = 'ready';
-						editor.fire( 'ready' );
+						if ( !isElement( sourceElementOrData ) && config.initialData ) {
+							// Documented in core/editor/editorconfig.jsdoc.
+							throw new CKEditorError(
+								'editor-create-initial-data: ' +
+								'The config.initialData option cannot be used together with initial data passed in Editor.create().',
+								null
+							);
+						}
+
+						editor.data.init( config.initialData || getInitialData( sourceElementOrData ) );
 					} )
+					.then( () => editor.fire( 'ready' ) )
 					.then( () => editor )
 			);
 		} );
@@ -104,7 +122,12 @@ class ClassicTestEditorUI extends EditorUI {
 		return this._view;
 	}
 
-	init( element ) {
+	/**
+	 * Initializes the UI.
+	 *
+	 * @param {HTMLElement|null} replacementElement The DOM element that will be the source for the created editor.
+	 */
+	init( replacementElement ) {
 		const view = this.view;
 		const editable = view.editable;
 		const editingView = this.editor.editing.view;
@@ -116,9 +139,11 @@ class ClassicTestEditorUI extends EditorUI {
 
 		view.main.add( view.editable );
 
-		this._editableElements.set( 'main', view.editable.element );
+		this.setEditableElement( 'main', view.editable.element );
 
-		this._elementReplacer.replace( element, view.element );
+		if ( replacementElement ) {
+			this._elementReplacer.replace( replacementElement, view.element );
+		}
 
 		this.fire( 'ready' );
 	}
@@ -137,3 +162,7 @@ class ClassicTestEditorUI extends EditorUI {
 
 mix( ClassicTestEditor, DataApiMixin );
 mix( ClassicTestEditor, ElementApiMixin );
+
+function getInitialData( sourceElementOrData ) {
+	return isElement( sourceElementOrData ) ? getDataFromElement( sourceElementOrData ) : sourceElementOrData;
+}

+ 10 - 10
packages/ckeditor5-core/tests/_utils/utils.js

@@ -49,19 +49,19 @@ const utils = {
 	 *
 	 * Usage:
 	 *
-	 *      it( 'test', () => {
-	 *          // Test bootstrapping...
+	 *		it( 'test', () => {
+	 *			// Test bootstrapping...
 	 *
-	 *          const assertEdge = () => {
-	 *              // expect();
-	 *          };
+	 *			const assertEdge = () => {
+	 *				// expect();
+	 *			};
 	 *
-	 *          const assertAll = () => {
-	 *              // expect();
-	 *          };
+	 *			const assertAll = () => {
+	 *				// expect();
+	 *			};
 	 *
-	 *          testUtils.checkAssertions( assertEdge, assertAll );
-	 *      } );
+	 *			testUtils.checkAssertions( assertEdge, assertAll );
+	 *		} );
 	 *
 	 * @param {...Function} assertions Functions that will be executed.
 	 */

+ 3 - 4
packages/ckeditor5-core/tests/_utils/virtualtesteditor.js

@@ -27,18 +27,17 @@ export default class VirtualTestEditor extends Editor {
 		this.model.document.createRoot();
 	}
 
-	static create( config ) {
+	static create( config = {} ) {
 		return new Promise( resolve => {
 			const editor = new this( config );
 
 			resolve(
 				editor.initPlugins()
+					.then( () => editor.data.init( config.initialData || '' ) )
 					.then( () => {
-						// Fire `data#ready` event manually as `data#init()` method is not used.
-						editor.data.fire( 'ready' );
 						editor.fire( 'ready' );
+						return editor;
 					} )
-					.then( () => editor )
 			);
 		} );
 	}

+ 6 - 3
packages/ckeditor5-core/tests/commandcollection.js

@@ -4,9 +4,9 @@
  */
 
 import CommandCollection from '../src/commandcollection';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import Command from '../src/command';
 import ModelTestEditor from './_utils/modeltesteditor';
+import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 class SomeCommand extends Command {
 	execute() {}
@@ -56,9 +56,12 @@ describe( 'CommandCollection', () => {
 		} );
 
 		it( 'throws an error if command does not exist', () => {
-			expect( () => {
+			const command = new SomeCommand( editor );
+			collection.add( 'bar', command );
+
+			expectToThrowCKEditorError( () => {
 				collection.execute( 'foo' );
-			} ).to.throw( CKEditorError, /^commandcollection-command-not-found:/ );
+			}, /^commandcollection-command-not-found:/, editor );
 		} );
 	} );
 

+ 3 - 3
packages/ckeditor5-core/tests/editor/editor.js

@@ -11,10 +11,10 @@ import Config from '@ckeditor/ckeditor5-utils/src/config';
 import EditingController from '@ckeditor/ckeditor5-engine/src/controller/editingcontroller';
 import PluginCollection from '../../src/plugincollection';
 import CommandCollection from '../../src/commandcollection';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import Locale from '@ckeditor/ckeditor5-utils/src/locale';
 import Command from '../../src/command';
 import EditingKeystrokeHandler from '../../src/editingkeystrokehandler';
+import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 class TestEditor extends Editor {
 	static create( config ) {
@@ -368,9 +368,9 @@ describe( 'Editor', () => {
 		it( 'should throw an error if specified command has not been added', () => {
 			const editor = new TestEditor();
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				editor.execute( 'command' );
-			} ).to.throw( CKEditorError, /^commandcollection-command-not-found:/ );
+			}, /^commandcollection-command-not-found:/, editor );
 		} );
 	} );
 

+ 56 - 10
packages/ckeditor5-core/tests/editor/editorui.js

@@ -11,7 +11,7 @@ import ComponentFactory from '@ckeditor/ckeditor5-ui/src/componentfactory';
 
 import testUtils from '../_utils/utils';
 
-/* global document */
+/* global document, console */
 
 describe( 'EditorUI', () => {
 	let editor, ui;
@@ -85,14 +85,50 @@ describe( 'EditorUI', () => {
 		} );
 
 		it( 'should reset editables array', () => {
-			ui._editableElements.set( 'foo', {} );
-			ui._editableElements.set( 'bar', {} );
+			ui.setEditableElement( 'foo', {} );
+			ui.setEditableElement( 'bar', {} );
 
-			expect( ui._editableElements.size ).to.equal( 2 );
+			expect( [ ...ui.getEditableElementsNames() ] ).to.deep.equal( [ 'foo', 'bar' ] );
 
 			ui.destroy();
 
-			expect( ui._editableElements.size ).to.equal( 0 );
+			expect( [ ...ui.getEditableElementsNames() ] ).to.have.length( 0 );
+		} );
+
+		it( 'removes domElement#ckeditorInstance references from registered root elements', () => {
+			const fooElement = document.createElement( 'foo' );
+			const barElement = document.createElement( 'bar' );
+
+			ui.setEditableElement( 'foo', fooElement );
+			ui.setEditableElement( 'bar', barElement );
+
+			expect( fooElement.ckeditorInstance ).to.equal( editor );
+			expect( barElement.ckeditorInstance ).to.equal( editor );
+
+			ui.destroy();
+
+			expect( fooElement.ckeditorInstance ).to.be.null;
+			expect( barElement.ckeditorInstance ).to.be.null;
+		} );
+	} );
+
+	describe( 'setEditableElement()', () => {
+		it( 'should register the editable element under a name', () => {
+			const ui = new EditorUI( editor );
+			const element = document.createElement( 'div' );
+
+			ui.setEditableElement( 'main', element );
+
+			expect( ui.getEditableElement( 'main' ) ).to.equal( element );
+		} );
+
+		it( 'puts a reference to the editor instance in domElement#ckeditorInstance', () => {
+			const ui = new EditorUI( editor );
+			const element = document.createElement( 'div' );
+
+			ui.setEditableElement( 'main', element );
+
+			expect( element.ckeditorInstance ).to.equal( editor );
 		} );
 	} );
 
@@ -101,7 +137,7 @@ describe( 'EditorUI', () => {
 			const ui = new EditorUI( editor );
 			const editableMock = { name: 'main', element: document.createElement( 'div' ) };
 
-			ui._editableElements.set( editableMock.name, editableMock.element );
+			ui.setEditableElement( editableMock.name, editableMock.element );
 
 			expect( ui.getEditableElement() ).to.equal( editableMock.element );
 		} );
@@ -111,8 +147,8 @@ describe( 'EditorUI', () => {
 			const editableMock1 = { name: 'root1', element: document.createElement( 'div' ) };
 			const editableMock2 = { name: 'root2', element: document.createElement( 'p' ) };
 
-			ui._editableElements.set( editableMock1.name, editableMock1.element );
-			ui._editableElements.set( editableMock2.name, editableMock2.element );
+			ui.setEditableElement( editableMock1.name, editableMock1.element );
+			ui.setEditableElement( editableMock2.name, editableMock2.element );
 
 			expect( ui.getEditableElement( 'root1' ) ).to.equal( editableMock1.element );
 			expect( ui.getEditableElement( 'root2' ) ).to.equal( editableMock2.element );
@@ -131,8 +167,8 @@ describe( 'EditorUI', () => {
 			const editableMock1 = { name: 'main', element: document.createElement( 'div' ) };
 			const editableMock2 = { name: 'root2', element: document.createElement( 'p' ) };
 
-			ui._editableElements.set( editableMock1.name, editableMock1.element );
-			ui._editableElements.set( editableMock2.name, editableMock2.element );
+			ui.setEditableElement( editableMock1.name, editableMock1.element );
+			ui.setEditableElement( editableMock2.name, editableMock2.element );
 
 			const names = ui.getEditableElementsNames();
 			expect( names[ Symbol.iterator ] ).to.instanceof( Function );
@@ -145,4 +181,14 @@ describe( 'EditorUI', () => {
 			expect( ui.getEditableElementsNames() ).to.be.empty;
 		} );
 	} );
+
+	describe( '_editableElements()', () => {
+		it( 'should warn about deprecation', () => {
+			const ui = new EditorUI( editor );
+			const stub = testUtils.sinon.stub( console, 'warn' );
+
+			expect( ui._editableElements ).to.be.instanceOf( Map );
+			sinon.assert.calledWithMatch( stub, 'editor-ui-deprecated-editable-elements' );
+		} );
+	} );
 } );

+ 6 - 4
packages/ckeditor5-core/tests/editor/utils/attachtoform.js

@@ -7,8 +7,8 @@ import attachToForm from '../../../src/editor/utils/attachtoform';
 import ElementApiMixin from '../../../src/editor/utils/elementapimixin';
 import Editor from '../../../src/editor/editor';
 import HtmlDataProcessor from '@ckeditor/ckeditor5-engine/src/dataprocessor/htmldataprocessor';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import mix from '@ckeditor/ckeditor5-utils/src/mix';
+import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 /* global document, Event */
 
@@ -47,9 +47,11 @@ describe( 'attachToForm()', () => {
 	} );
 
 	it( 'should throw an error when is used with editor without `ElementApiMixin`', () => {
-		expect( () => {
-			attachToForm( new Editor() );
-		} ).to.throw( CKEditorError, /^attachtoform-missing-elementapi-interface/ );
+		const editor = new Editor();
+
+		expectToThrowCKEditorError( () => {
+			attachToForm( editor );
+		}, /^attachtoform-missing-elementapi-interface/, editor );
 	} );
 
 	it( 'should update editor#element after the "submit" event', () => {

+ 6 - 2
packages/ckeditor5-core/tests/editor/utils/elementapimixin.js

@@ -9,7 +9,7 @@ import ElementApiMixin from '../../../src/editor/utils/elementapimixin';
 import Editor from '../../../src/editor/editor';
 import HtmlDataProcessor from '@ckeditor/ckeditor5-engine/src/dataprocessor/htmldataprocessor';
 import mix from '@ckeditor/ckeditor5-utils/src/mix';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 describe( 'ElementApiMixin', () => {
 	let editor;
@@ -46,7 +46,11 @@ describe( 'ElementApiMixin', () => {
 		} );
 
 		it( 'throws an error if "sourceElement" has not been set', () => {
-			expect( () => editor.updateSourceElement() ).to.throw( CKEditorError, /editor-missing-sourceelement/ );
+			expectToThrowCKEditorError(
+				() => editor.updateSourceElement(),
+				/editor-missing-sourceelement/,
+				editor
+			);
 		} );
 	} );
 } );

+ 124 - 0
packages/ckeditor5-core/tests/multicommand.js

@@ -0,0 +1,124 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+import MultiCommand from '../src/multicommand';
+import ModelTestEditor from './_utils/modeltesteditor';
+import Command from '../src/command';
+
+describe( 'MultiCommand', () => {
+	let editor, multiCommand;
+
+	beforeEach( () => {
+		return ModelTestEditor
+			.create()
+			.then( newEditor => {
+				editor = newEditor;
+				multiCommand = new MultiCommand( editor );
+			} );
+	} );
+
+	afterEach( () => {
+		multiCommand.destroy();
+
+		return editor.destroy();
+	} );
+
+	describe( 'isEnabled', () => {
+		it( 'is always falsy when no child commands are registered', () => {
+			expect( multiCommand.isEnabled ).to.false;
+
+			multiCommand.refresh();
+
+			expect( multiCommand.isEnabled ).to.false;
+		} );
+
+		it( 'is set to true if one of registered commands is true', () => {
+			expect( multiCommand.isEnabled ).to.false;
+
+			const commandA = new Command( editor );
+			const commandB = new Command( editor );
+
+			multiCommand.registerChildCommand( commandA );
+			multiCommand.registerChildCommand( commandB );
+
+			expect( multiCommand.isEnabled ).to.false;
+
+			commandA.isEnabled = true;
+
+			expect( multiCommand.isEnabled ).to.be.true;
+
+			commandA.isEnabled = false;
+
+			expect( multiCommand.isEnabled ).to.be.false;
+
+			commandB.isEnabled = true;
+
+			expect( multiCommand.isEnabled ).to.be.true;
+		} );
+	} );
+
+	describe( 'execute()', () => {
+		it( 'does not call any command if all are disabled', () => {
+			const commandA = new Command( editor );
+			const commandB = new Command( editor );
+
+			multiCommand.registerChildCommand( commandA );
+			multiCommand.registerChildCommand( commandB );
+
+			const spyA = sinon.spy( commandA, 'execute' );
+			const spyB = sinon.spy( commandB, 'execute' );
+
+			multiCommand.execute();
+
+			sinon.assert.notCalled( spyA );
+			sinon.assert.notCalled( spyB );
+		} );
+
+		it( 'executes enabled command', () => {
+			const commandA = new Command( editor );
+			const commandB = new Command( editor );
+			const commandC = new Command( editor );
+
+			multiCommand.registerChildCommand( commandA );
+			multiCommand.registerChildCommand( commandB );
+			multiCommand.registerChildCommand( commandC );
+
+			const spyA = sinon.spy( commandA, 'execute' );
+			const spyB = sinon.spy( commandB, 'execute' );
+			const spyC = sinon.spy( commandC, 'execute' );
+
+			commandC.isEnabled = true;
+
+			multiCommand.execute();
+
+			sinon.assert.notCalled( spyA );
+			sinon.assert.notCalled( spyB );
+			sinon.assert.calledOnce( spyC );
+		} );
+
+		it( 'executes first registered command if many are enabled', () => {
+			const commandA = new Command( editor );
+			const commandB = new Command( editor );
+			const commandC = new Command( editor );
+
+			multiCommand.registerChildCommand( commandA );
+			multiCommand.registerChildCommand( commandB );
+			multiCommand.registerChildCommand( commandC );
+
+			const spyA = sinon.spy( commandA, 'execute' );
+			const spyB = sinon.spy( commandB, 'execute' );
+			const spyC = sinon.spy( commandC, 'execute' );
+
+			commandC.isEnabled = true;
+			commandB.isEnabled = true;
+
+			multiCommand.execute();
+
+			sinon.assert.notCalled( spyA );
+			sinon.assert.calledOnce( spyB );
+			sinon.assert.notCalled( spyC );
+		} );
+	} );
+} );

+ 3 - 3
packages/ckeditor5-core/tests/pendingactions.js

@@ -5,7 +5,7 @@
 
 import VirtualTestEditor from './_utils/virtualtesteditor';
 import PendingActions from '../src/pendingactions';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 let editor, pendingActions;
 
@@ -69,9 +69,9 @@ describe( 'PendingActions', () => {
 		} );
 
 		it( 'should throw an error when invalid message is given', () => {
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				pendingActions.add( {} );
-			} ).to.throw( CKEditorError, /^pendingactions-add-invalid-message/ );
+			}, /^pendingactions-add-invalid-message/, editor );
 		} );
 
 		it( 'should fire add event with added item', () => {

+ 20 - 18
packages/ckeditor5-core/tests/plugin.js

@@ -6,33 +6,35 @@
 import Plugin from '../src/plugin';
 import Editor from '../src/editor/editor';
 
-let editor;
+describe( 'Plugin', () => {
+	let editor;
 
-before( () => {
-	editor = new Editor();
-} );
-
-describe( 'constructor()', () => {
-	it( 'should set the `editor` property', () => {
-		const plugin = new Plugin( editor );
-
-		expect( plugin ).to.have.property( 'editor' ).to.equal( editor );
+	beforeEach( () => {
+		editor = new Editor();
 	} );
 
-	describe( 'destroy()', () => {
-		it( 'should be defined', () => {
+	describe( 'constructor()', () => {
+		it( 'should set the `editor` property', () => {
 			const plugin = new Plugin( editor );
 
-			expect( plugin.destroy ).to.be.a( 'function' );
+			expect( plugin ).to.have.property( 'editor' ).to.equal( editor );
 		} );
 
-		it( 'should stop listening', () => {
-			const plugin = new Plugin( editor );
-			const stopListeningSpy = sinon.spy( plugin, 'stopListening' );
+		describe( 'destroy()', () => {
+			it( 'should be defined', () => {
+				const plugin = new Plugin( editor );
+
+				expect( plugin.destroy ).to.be.a( 'function' );
+			} );
+
+			it( 'should stop listening', () => {
+				const plugin = new Plugin( editor );
+				const stopListeningSpy = sinon.spy( plugin, 'stopListening' );
 
-			plugin.destroy();
+				plugin.destroy();
 
-			expect( stopListeningSpy.calledOnce ).to.equal( true );
+				expect( stopListeningSpy.calledOnce ).to.equal( true );
+			} );
 		} );
 	} );
 } );

+ 53 - 73
packages/ckeditor5-core/tests/plugincollection.js

@@ -3,14 +3,12 @@
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
-/* globals setTimeout */
+/* globals setTimeout, console */
 
-import testUtils from '../tests/_utils/utils';
 import Editor from '../src/editor/editor';
 import PluginCollection from '../src/plugincollection';
 import Plugin from '../src/plugin';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
-import log from '@ckeditor/ckeditor5-utils/src/log';
+import { expectToThrowCKEditorError, assertCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 let editor, availablePlugins;
 let PluginA, PluginB, PluginC, PluginD, PluginE, PluginF, PluginG, PluginH, PluginI, PluginJ, PluginK, PluginX, PluginFoo, AnotherPluginFoo;
@@ -19,8 +17,6 @@ class ChildPlugin extends Plugin {}
 class GrandPlugin extends ChildPlugin {}
 
 describe( 'PluginCollection', () => {
-	testUtils.createSinonSandbox();
-
 	before( () => {
 		PluginA = createPlugin( 'A' );
 		PluginB = createPlugin( 'B' );
@@ -73,6 +69,10 @@ describe( 'PluginCollection', () => {
 		PluginFoo.requires = [];
 	} );
 
+	afterEach( () => {
+		sinon.restore();
+	} );
+
 	describe( 'load()', () => {
 		it( 'should not fail when trying to load 0 plugins (empty array)', () => {
 			const plugins = new PluginCollection( editor, availablePlugins );
@@ -194,7 +194,7 @@ describe( 'PluginCollection', () => {
 		} );
 
 		it( 'should load plugin which does not extend the base Plugin class', () => {
-			class Y { }
+			class Y {}
 
 			const plugins = new PluginCollection( editor, availablePlugins );
 
@@ -240,7 +240,7 @@ describe( 'PluginCollection', () => {
 		} );
 
 		it( 'should reject on broken plugins (forward the error thrown in a plugin)', () => {
-			const logSpy = testUtils.sinon.stub( log, 'error' );
+			const consoleErrorStub = sinon.stub( console, 'error' );
 
 			const plugins = new PluginCollection( editor, availablePlugins );
 
@@ -253,13 +253,13 @@ describe( 'PluginCollection', () => {
 					expect( err ).to.be.an.instanceof( TestError );
 					expect( err ).to.have.property( 'message', 'Some error inside a plugin' );
 
-					sinon.assert.calledOnce( logSpy );
-					expect( logSpy.args[ 0 ][ 0 ] ).to.match( /^plugincollection-load:/ );
+					sinon.assert.calledOnce( consoleErrorStub );
+					expect( consoleErrorStub.args[ 0 ][ 0 ] ).to.match( /^plugincollection-load:/ );
 				} );
 		} );
 
 		it( 'should reject when loading non-existent plugin', () => {
-			const logSpy = testUtils.sinon.stub( log, 'error' );
+			const consoleErrorStub = sinon.stub( console, 'error' );
 
 			const plugins = new PluginCollection( editor, availablePlugins );
 
@@ -269,11 +269,10 @@ describe( 'PluginCollection', () => {
 					throw new Error( 'Test error: this promise should not be resolved successfully' );
 				} )
 				.catch( err => {
-					expect( err ).to.be.an.instanceof( CKEditorError );
-					expect( err.message ).to.match( /^plugincollection-plugin-not-found/ );
+					assertCKEditorError( err, /^plugincollection-plugin-not-found/, editor );
 
-					sinon.assert.calledOnce( logSpy );
-					expect( logSpy.args[ 0 ][ 0 ] ).to.match( /^plugincollection-plugin-not-found:/ );
+					sinon.assert.calledOnce( consoleErrorStub );
+					expect( consoleErrorStub.args[ 0 ][ 0 ] ).to.match( /^plugincollection-plugin-not-found:/ );
 				} );
 		} );
 
@@ -340,84 +339,65 @@ describe( 'PluginCollection', () => {
 		} );
 
 		it( 'should reject when loaded plugin requires not allowed plugins', () => {
-			const logSpy = testUtils.sinon.stub( log, 'error' );
+			const consoleErrorStub = sinon.stub( console, 'error' );
 			const plugins = new PluginCollection( editor, availablePlugins );
 
 			return plugins.init( [ PluginA, PluginB, PluginC, PluginD ], [ PluginA, PluginB ] )
-				// Throw here, so if by any chance plugins.init() was resolved correctly catch() will be stil executed.
+				// Throw here, so if by any chance plugins.init() was resolved correctly catch() will be still executed.
 				.then( () => {
 					throw new Error( 'Test error: this promise should not be resolved successfully' );
 				} )
 				.catch( err => {
-					expect( err ).to.be.an.instanceof( CKEditorError );
-					expect( err.message ).to.match( /^plugincollection-required/ );
+					assertCKEditorError( err, /^plugincollection-required/, editor );
 
-					expect( logSpy.calledTwice ).to.equal( true );
+					sinon.assert.calledTwice( consoleErrorStub );
 				} );
 		} );
 
-		it( 'logs if tries to load more than one plugin with the same name', () => {
-			const logSpy = testUtils.sinon.stub( log, 'warn' );
+		it( 'should reject when loading more than one plugin with the same name', () => {
 			const plugins = new PluginCollection( editor );
 
 			return plugins.init( [ PluginFoo, AnotherPluginFoo ] )
 				.then( () => {
-					expect( getPlugins( plugins ).length ).to.equal( 2 );
-
-					expect( plugins.get( 'Foo' ) ).to.be.an.instanceof( PluginFoo );
-					expect( plugins.get( PluginFoo ) ).to.be.an.instanceof( PluginFoo );
-					expect( plugins.get( AnotherPluginFoo ) ).to.be.an.instanceof( AnotherPluginFoo );
-
-					expect( logSpy.calledOnce ).to.equal( true );
-					expect( logSpy.firstCall.args[ 0 ] ).to.match( /^plugincollection-plugin-name-conflict:/ );
-
-					const warnData = logSpy.firstCall.args[ 1 ];
-					expect( warnData.pluginName ).to.equal( 'Foo' );
-					expect( warnData.plugin1 ).to.equal( PluginFoo );
-					expect( warnData.plugin2 ).to.equal( AnotherPluginFoo );
+					throw new Error( 'The `init()` method should fail.' );
+				} )
+				.catch( err => {
+					assertCKEditorError( err, /^plugincollection-plugin-name-conflict:/, null, {
+						pluginName: 'Foo',
+						plugin1: PluginFoo,
+						plugin2: AnotherPluginFoo
+					} );
 				} );
 		} );
 
-		it( 'logs if tries to load more than one plugin with the same name (plugin requires plugin with the same name)', () => {
+		it( 'should reject when loading more than one plugin with the same name (plugin requires plugin with the same name)', () => {
 			PluginFoo.requires = [ AnotherPluginFoo ];
 
-			const logSpy = testUtils.sinon.stub( log, 'warn' );
 			const plugins = new PluginCollection( editor );
 
 			return plugins.init( [ PluginFoo ] )
 				.then( () => {
-					expect( getPlugins( plugins ).length ).to.equal( 2 );
-
-					expect( plugins.get( 'Foo' ) ).to.be.an.instanceof( AnotherPluginFoo );
-					expect( plugins.get( AnotherPluginFoo ) ).to.be.an.instanceof( AnotherPluginFoo );
-					expect( plugins.get( PluginFoo ) ).to.be.an.instanceof( PluginFoo );
-
-					expect( logSpy.calledOnce ).to.equal( true );
-					expect( logSpy.firstCall.args[ 0 ] ).to.match( /^plugincollection-plugin-name-conflict:/ );
+					throw new Error( 'The `init()` method should fail.' );
+				} )
+				.catch( err => {
+					assertCKEditorError( err, /^plugincollection-plugin-name-conflict:/, null );
 				} );
 		} );
 
-		it(
-			'logs if tries to load more than one plugin with the same name (plugin with the same name is built-in the PluginCollection)',
-			() => {
-				availablePlugins = [ PluginFoo ];
-
-				const logSpy = testUtils.sinon.stub( log, 'warn' );
-				const plugins = new PluginCollection( editor, availablePlugins );
+		it( 'should reject when loading more than one plugin with the same name' +
+			'(plugin with the same name is built-in the PluginCollection)', () => {
+			availablePlugins = [ PluginFoo ];
 
-				return plugins.init( [ 'Foo', AnotherPluginFoo ] )
-					.then( () => {
-						expect( getPlugins( plugins ).length ).to.equal( 2 );
-
-						expect( plugins.get( 'Foo' ) ).to.be.an.instanceof( PluginFoo );
-						expect( plugins.get( PluginFoo ) ).to.be.an.instanceof( PluginFoo );
-						expect( plugins.get( AnotherPluginFoo ) ).to.be.an.instanceof( AnotherPluginFoo );
+			const plugins = new PluginCollection( editor, availablePlugins );
 
-						expect( logSpy.calledOnce ).to.equal( true );
-						expect( logSpy.firstCall.args[ 0 ] ).to.match( /^plugincollection-plugin-name-conflict:/ );
-					} );
-			}
-		);
+			return plugins.init( [ 'Foo', AnotherPluginFoo ] )
+				.then( () => {
+					throw new Error( 'The `init()` method should fail.' );
+				} )
+				.catch( err => {
+					assertCKEditorError( err, /^plugincollection-plugin-name-conflict:/, null );
+				} );
+		} );
 	} );
 
 	describe( 'get()', () => {
@@ -453,9 +433,9 @@ describe( 'PluginCollection', () => {
 			const plugins = new PluginCollection( editor, availablePlugins );
 
 			return plugins.init( [] ).then( () => {
-				expect( () => plugins.get( 'foo' ) )
-					.to.throw( CKEditorError, /^plugincollection-plugin-not-loaded:/ )
-					.with.deep.property( 'data', { plugin: 'foo' } );
+				expectToThrowCKEditorError( () => plugins.get( 'foo' ),
+					/^plugincollection-plugin-not-loaded:/, editor, { plugin: 'foo' }
+				);
 			} );
 		} );
 
@@ -466,9 +446,8 @@ describe( 'PluginCollection', () => {
 			const plugins = new PluginCollection( editor, availablePlugins );
 
 			return plugins.init( [] ).then( () => {
-				expect( () => plugins.get( SomePlugin ) )
-					.to.throw( CKEditorError, /^plugincollection-plugin-not-loaded:/ )
-					.with.deep.property( 'data', { plugin: 'foo' } );
+				expectToThrowCKEditorError( () => plugins.get( SomePlugin ),
+					/^plugincollection-plugin-not-loaded:/, editor, { plugin: 'foo' } );
 			} );
 		} );
 
@@ -478,9 +457,10 @@ describe( 'PluginCollection', () => {
 			const plugins = new PluginCollection( editor, availablePlugins );
 
 			return plugins.init( [] ).then( () => {
-				expect( () => plugins.get( SomePlugin ) )
-					.to.throw( CKEditorError, /^plugincollection-plugin-not-loaded:/ )
-					.with.deep.property( 'data', { plugin: 'SomePlugin' } );
+				expectToThrowCKEditorError( () => plugins.get( SomePlugin ),
+					/^plugincollection-plugin-not-loaded:/,
+					editor, { plugin: 'SomePlugin' }
+				);
 			} );
 		} );
 	} );