Explorar el Código

Merge pull request #125 from ckeditor/t/119

Completed basic editor infrastructure.
Piotrek Koszuliński hace 9 años
padre
commit
3b3972da83
Se han modificado 38 ficheros con 1927 adiciones y 811 borrados
  1. 68 23
      ckeditor.js
  2. 1 0
      src/command/command.js
  3. 0 141
      src/creator.js
  4. 26 0
      src/creator/creator.js
  5. 169 0
      src/creator/standardcreator.js
  6. 112 0
      src/editable.js
  7. 60 0
      src/editablecollection.js
  8. 145 22
      src/editor.js
  9. 1 1
      src/plugin.js
  10. 1 1
      tests/_utils-tests/utils.js
  11. 0 11
      tests/_utils/ui/editable/inline/inlineeditable.js
  12. 4 4
      tests/_utils/ui/editableui/framed/framededitableui.js
  13. 2 2
      tests/_utils/ui/editableui/framed/framededitableuiview.js
  14. 14 3
      tests/_utils/ui/editableui/inline/inlineeditableuiview.js
  15. 1 1
      tests/_utils/ui/floatingtoolbar/floatingtoolbar.js
  16. 1 1
      tests/_utils/utils.js
  17. 136 20
      tests/ckeditor.js
  18. 4 5
      tests/command/attributecommand.js
  19. 9 181
      tests/creator/creator.js
  20. 48 34
      tests/creator/manual/_utils/creator/classiccreator.js
  21. 53 30
      tests/creator/manual/_utils/creator/inlinecreator.js
  22. 105 0
      tests/creator/manual/_utils/creator/multicreator.js
  23. 5 4
      tests/creator/manual/creator-classic.html
  24. 6 5
      tests/creator/manual/creator-classic.js
  25. 1 1
      tests/creator/manual/creator-classic.md
  26. 5 4
      tests/creator/manual/creator-inline.html
  27. 6 5
      tests/creator/manual/creator-inline.js
  28. 1 1
      tests/creator/manual/creator-inline.md
  29. 20 0
      tests/creator/manual/creator-multi.html
  30. 60 0
      tests/creator/manual/creator-multi.js
  31. 22 0
      tests/creator/manual/creator-multi.md
  32. 0 0
      tests/creator/standardcreator.html
  33. 228 0
      tests/creator/standardcreator.js
  34. 126 0
      tests/editable.js
  35. 104 0
      tests/editablecollection.js
  36. 131 163
      tests/editor/creator.js
  37. 251 147
      tests/editor/editor.js
  38. 1 1
      tests/plugincollection.js

+ 68 - 23
ckeditor.js

@@ -8,6 +8,10 @@
 import Editor from './ckeditor5/editor.js';
 import Collection from './ckeditor5/utils/collection.js';
 import Config from './ckeditor5/utils/config.js';
+import CKEditorError from './ckeditor5/utils/ckeditorerror.js';
+import isArrayLike from './ckeditor5/utils/lib/lodash/isArrayLike.js';
+import clone from './ckeditor5/utils/lib/lodash/clone.js';
+import utils from './ckeditor5/utils/utils.js';
 
 /**
  * This is the API entry point. The entire CKEditor code runs under this object.
@@ -23,6 +27,15 @@ const CKEDITOR = {
 	 */
 	instances: new Collection(),
 
+	/**
+	 * Holds global configuration defaults, which will be used by editor instances when such configurations are not
+	 * available on them directly.
+	 *
+	 * @readonly
+	 * @member {utils.Config} CKEDITOR.config
+	 */
+	config: new Config(),
+
 	/**
 	 * Creates an editor instance for the provided DOM element.
 	 *
@@ -36,22 +49,15 @@ const CKEDITOR = {
 	 *		} );
 	 *
 	 * @method CKEDITOR.create
-	 * @param {String|HTMLElement} element An element selector or a DOM element, which will be the source for the
-	 * created instance.
+	 * @param {String|HTMLElement|HTMLCollection|NodeList|Array.<HTMLElement>|Object.<String, HTMLElement>} elements
+	 * One or more elements on which the editor will be initialized. Different creators can handle these
+	 * elements differently, but usually a creator loads the data from the element and either makes
+	 * it editable or hides it and inserts the editor UI next to it.
 	 * @returns {Promise} A promise, which will be fulfilled with the created editor.
 	 */
-	create( element, config ) {
-		return new Promise( ( resolve, reject ) => {
-			// If a query selector has been passed, transform it into a real element.
-			if ( typeof element == 'string' ) {
-				element = document.querySelector( element );
-
-				if ( !element ) {
-					return reject( new Error( 'Element not found' ) );
-				}
-			}
-
-			const editor = new Editor( element, config );
+	create( elements, config ) {
+		return new Promise( ( resolve ) => {
+			const editor = new Editor( normalizeElements( elements ), config );
 
 			this.instances.add( editor );
 
@@ -69,15 +75,54 @@ const CKEDITOR = {
 					} )
 			);
 		} );
-	},
-
-	/**
-	 * Holds global configuration defaults, which will be used by editor instances when such configurations are not
-	 * available on them directly.
-	 *
-	 * @member {utils.Config} CKEDITOR.config
-	 */
-	config: new Config()
+	}
 };
 
 export default CKEDITOR;
+
+function normalizeElements( elements ) {
+	let elementsObject;
+
+	// If a query selector has been passed, transform it into a real element.
+	if ( typeof elements == 'string' ) {
+		elementsObject = toElementsObject( document.querySelectorAll( elements ) );
+	}
+	// Arrays and array-like objects.
+	else if ( isArrayLike( elements ) ) {
+		elementsObject = toElementsObject( elements );
+	}
+	// Single HTML element.
+	else if ( elements instanceof HTMLElement ) {
+		elementsObject = toElementsObject( [ elements ] );
+	}
+	// Object.
+	else {
+		elementsObject = clone( elements );
+	}
+
+	if ( !Object.keys( elementsObject ).length ) {
+		throw new CKEditorError( 'ckeditor5-create-no-elements: No elements have been passed to CKEDITOR.create()' );
+	}
+
+	return elementsObject;
+}
+
+function toElementsObject( elements ) {
+	return Array.from( elements ).reduce( ( ret, el ) => {
+		ret[ getEditorElementName( el ) ] = el;
+
+		return ret;
+	}, {} );
+}
+
+function getEditorElementName( element ) {
+	if ( element.id ) {
+		return element.id;
+	}
+
+	if ( element.dataset.editable ) {
+		return element.dataset.editable;
+	}
+
+	return 'editable' + utils.uid();
+}

+ 1 - 0
src/command/command.js

@@ -31,6 +31,7 @@ export default class Command {
 		/**
 		 * Editor on which this command will be used.
 		 *
+		 * @readonly
 		 * @member {ckeditor5.Editor} ckeditor5.command.Command#editor
 		 */
 		this.editor = editor;

+ 0 - 141
src/creator.js

@@ -1,141 +0,0 @@
-/**
- * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-'use strict';
-
-import Plugin from './plugin.js';
-
-/**
- * Basic creator class.
- *
- * @memberOf ckeditor5
- * @extends ckeditor5.Plugin
- */
-export default class Creator extends Plugin {
-	/**
-	 * The element used to {@link ckeditor5.Creator#_replaceElement _replaceElement} the editor element.
-	 *
-	 * @private
-	 * @member {HTMLElement} ckeditor5.Creator#_elementReplacement
-	 */
-
-	/**
-	 * The creator's trigger. This method is called by the editor to finalize
-	 * the editor creation.
-	 *
-	 * @returns {Promise}
-	 */
-	create() {
-		if ( this.editor.ui ) {
-			return this.editor.ui.init();
-		} else {
-			return Promise.resolve();
-		}
-	}
-
-	/**
-	 * Method called by the editor on its destruction. It should destroy what the creator created.
-	 *
-	 * @returns {Promise}
-	 */
-	destroy() {
-		super.destroy();
-
-		if ( this._elementReplacement ) {
-			this._restoreElement();
-		}
-
-		const ui = this.editor.ui;
-		let promise = Promise.resolve();
-
-		if ( ui ) {
-			promise = promise
-				.then( ui.destroy.bind( ui ) )
-				.then( () => {
-					this.editor.ui = null;
-				} );
-		}
-
-		return promise;
-	}
-
-	/**
-	 * Updates the {@link ckeditor5.Editor#element editor element}'s content with the data.
-	 *
-	 */
-	updateEditorElement() {
-		Creator.setDataInElement( this.editor.element, this.editor.getData() );
-	}
-
-	/**
-	 * Loads the data from the {@link ckeditor5.Editor#element editor element} to the editable.
-	 *
-	 */
-	loadDataFromEditorElement() {
-		this.editor.setData( Creator.getDataFromElement( this.editor.element ) );
-	}
-
-	/**
-	 * Gets data from a given source element.
-	 *
-	 * @param {HTMLElement} el The element from which the data will be retrieved.
-	 * @returns {String} The data string.
-	 */
-	static getDataFromElement( el ) {
-		if ( el instanceof HTMLTextAreaElement ) {
-			return el.value;
-		}
-
-		return el.innerHTML;
-	}
-
-	/**
-	 * Sets data in a given element.
-	 *
-	 * @param {HTMLElement} el The element in which the data will be set.
-	 * @param {String} data The data string.
-	 */
-	static setDataInElement( el, data ) {
-		if ( el instanceof HTMLTextAreaElement ) {
-			el.value = data;
-		}
-
-		el.innerHTML = data;
-	}
-
-	/**
-	 * Hides the {@link ckeditor5.Editor#element editor element} and inserts the the given element
-	 * (usually, editor's UI main element) next to it.
-	 *
-	 * The effect of this method will be automatically reverted by {@link ckeditor5.Creator#destroy destroy}.
-	 *
-	 * @protected
-	 * @param {HTMLElement} [newElement] The replacement element. If not passed, then the main editor's UI view element
-	 * will be used.
-	 */
-	_replaceElement( newElement ) {
-		if ( !newElement ) {
-			newElement = this.editor.ui.view.element;
-		}
-
-		this._elementReplacement = newElement;
-
-		const editorEl = this.editor.element;
-
-		editorEl.style.display = 'none';
-		editorEl.parentNode.insertBefore( newElement, editorEl.nextSibling );
-	}
-
-	/**
-	 * Restores what the {@link ckeditor5.Creator#_replaceElement _replaceElement} did.
-	 *
-	 * @protected
-	 */
-	_restoreElement() {
-		this.editor.element.style.display = '';
-		this._elementReplacement.remove();
-		this._elementReplacement = null;
-	}
-}

+ 26 - 0
src/creator/creator.js

@@ -0,0 +1,26 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import Plugin from '../plugin.js';
+
+/**
+ * Base creator class.
+ *
+ * @memberOf ckeditor5.creator
+ * @extends ckeditor5.Plugin
+ */
+export default class Creator extends Plugin {
+	/**
+	 * The creator's trigger. This method is called by the editor to finalize
+	 * the editor creation.
+	 *
+	 * @returns {Promise}
+	 */
+	create() {
+		return Promise.resolve();
+	}
+}

+ 169 - 0
src/creator/standardcreator.js

@@ -0,0 +1,169 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import Creator from './creator.js';
+
+import Document from '../engine/treemodel/document.js';
+import DataController from '../engine/treecontroller/datacontroller.js';
+import EditingController from '../engine/treecontroller/editingcontroller.js';
+
+/**
+ * Standard creator for browser environment.
+ *
+ * @memberOf ckeditor5.creator
+ * @extends ckeditor5.creator.Creator
+ */
+export default class StandardCreator extends Creator {
+	/**
+	 * Creates an instance of the standard creator. Initializes the engine ({@link engine.treeModel.Document document},
+	 * {@link engine.treeController.EditingController editing controller} and
+	 * {@link engine.treeController.DataController data controller}).
+	 *
+	 * @param {ckeditor5.Editor} The editor instance.
+	 * @param {engine.dataProcessor.DataProcessor} The data processor to use.
+	 */
+	constructor( editor, dataProcessor ) {
+		super( editor );
+
+		editor.document = new Document();
+		editor.editing = new EditingController( editor.document );
+		editor.data = new DataController( editor.document, dataProcessor );
+
+		/**
+		 * The elements replaced by {@link ckeditor5.creator.StandardCreator#_replaceElement} and their replacements.
+		 *
+		 * @private
+		 * @member {Array.<Object>} ckeditor5.creator.StandardCreator#_replacedElements
+		 */
+		this._replacedElements = [];
+	}
+
+	destroy() {
+		const editor = this.editor;
+
+		super.destroy();
+
+		editor.document.destroy();
+		editor.editing.destroy();
+		editor.data.destroy();
+
+		this._restoreElements();
+	}
+
+	/**
+	 * Updates the {@link ckeditor5.Editor#element editor element}'s content with the data.
+	 *
+	 * @param [elementName] If not specified, the first element will be used.
+	 */
+	updateEditorElement( elementName ) {
+		if ( !elementName ) {
+			elementName = this.editor.firstElementName;
+		}
+
+		StandardCreator.setDataInElement( this.editor.elements.get( elementName ), this.editor.getData( elementName ) );
+	}
+
+	/**
+	 * Updates all {@link ckeditor5.Editor#element editor elements} content with the data taken from
+	 * their corresponding editables.
+	 */
+	updateEditorElements() {
+		this.editor.elements.forEach( ( editorElement, elementName ) => {
+			this.updateEditorElement( elementName );
+		} );
+	}
+
+	/**
+	 * Loads the data from the given {@link ckeditor5.Editor#element editor element} to the editable.
+	 *
+	 * @param [elementName] If not specified, the first element will be used.
+	 */
+	loadDataFromEditorElement( elementName ) {
+		if ( !elementName ) {
+			elementName = this.editor.firstElementName;
+		}
+
+		this.editor.setData( StandardCreator.getDataFromElement( this.editor.elements.get( elementName ) ), elementName );
+	}
+
+	/**
+	 * Loads the data from all {@link ckeditor5.Editor#element editor elements} to their corresponding editables.
+	 */
+	loadDataFromEditorElements() {
+		this.editor.elements.forEach( ( editorElement, elementName ) => {
+			this.loadDataFromEditorElement( elementName );
+		} );
+	}
+
+	/**
+	 * Gets data from a given source element.
+	 *
+	 * @param {HTMLElement} el The element from which the data will be retrieved.
+	 * @returns {String} The data string.
+	 */
+	static getDataFromElement( el ) {
+		if ( el instanceof HTMLTextAreaElement ) {
+			return el.value;
+		}
+
+		return el.innerHTML;
+	}
+
+	/**
+	 * Sets data in a given element.
+	 *
+	 * @param {HTMLElement} el The element in which the data will be set.
+	 * @param {String} data The data string.
+	 */
+	static setDataInElement( el, data ) {
+		if ( el instanceof HTMLTextAreaElement ) {
+			el.value = data;
+		}
+
+		el.innerHTML = data;
+	}
+
+	/**
+	 * Hides one of the {@link ckeditor5.Editor#elements editor elements} and, if specified, inserts the the given element
+	 * (e.g. the editor's UI main element) next to it.
+	 *
+	 * The effect of this method will be automatically reverted by {@link ckeditor5.creator.StandardCreator#destroy destroy}.
+	 *
+	 * The second argument may not be passed and then the element will be replaced by nothing, so in other words it will
+	 * be hidden.
+	 *
+	 * @protected
+	 * @param {HTMLElement} element The element to replace.
+	 * @param {HTMLElement} [newElement] The replacement element. If not passed, then the `element` will just be hidden.
+	 */
+	_replaceElement( element, newElement ) {
+		this._replacedElements.push( { element, newElement } );
+
+		element.style.display = 'none';
+
+		if ( newElement ) {
+			element.parentNode.insertBefore( newElement, element.nextSibling );
+		}
+	}
+
+	/**
+	 * Restores what the {@link ckeditor5.creator.StandardCreator#_replaceElement _replaceElement} did.
+	 *
+	 * @protected
+	 */
+	_restoreElements() {
+		this._replacedElements.forEach( ( { element, newElement } ) => {
+			element.style.display = '';
+
+			if ( newElement ) {
+				newElement.remove();
+			}
+		} );
+
+		this._replacedElements = [];
+	}
+}

+ 112 - 0
src/editable.js

@@ -0,0 +1,112 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import utils from './utils/utils.js';
+import ObservableMixin from './utils/observablemixin.js';
+import FocusObserver from './engine/treeview/observer/focusobserver.js';
+
+/**
+ * Class representing a single editable element. It combines the {@link ckeditor5.Editable#viewElement editable view}
+ * with the {@link ckeditor5.Editable#domElement editable DOM element} to which the view is rendering.
+ *
+ * @memberOf ckeditor5
+ * @mixes utils.ObservaleMixin
+ */
+export default class Editable {
+	/**
+	 * Creates a new instance of the Editable class.
+	 *
+	 * @param {ckeditor5.Editor} editor The editor instance.
+	 * @param {String} name The name of the editable.
+	 */
+	constructor( editor, name ) {
+		/**
+		 * The editor instance.
+		 *
+		 * @readonly
+		 * @member {ckeditor5.Editor} ckeditor5.Editable#editor
+		 */
+		this.editor = editor;
+
+		/**
+		 * The name of the editable.
+		 *
+		 * @readonly
+		 * @member {String} ckeditor5.Editable#name
+		 */
+		this.name = name;
+
+		/**
+		 * Whether the editable is in read-write or read-only mode.
+		 *
+		 * @observable
+		 * @member {Boolean} ckeditor5.Editable#isEditable
+		 */
+		this.set( 'isEditable', true );
+
+		/**
+		 * Whether the editable is focused.
+		 *
+		 * @readonly
+		 * @observable
+		 * @member {Boolean} ckeditor5.Editable#isFocused
+		 */
+		this.set( 'isFocused', false );
+
+		/**
+		 * The editable DOM element.
+		 *
+		 * @readonly
+		 * @member {HTMLElement} ckeditor5.Editable#domElement
+		 */
+
+		/**
+		 * The view element which holds this editable.
+		 *
+		 * @readonly
+		 * @member {engine.treeView.Element} ckeditor5.Editable#viewElement
+		 */
+	}
+
+	/**
+	 * Binds the {@link ckeditor5.Editable#viewElement editable's view} to a concrete DOM element.
+	 *
+	 * @param {HTMLElement} domElement The DOM element which holds the editable.
+	 */
+	bindTo( domElement ) {
+		const editingView = this.editor.editing.view;
+		const viewElement = editingView.createRoot( domElement, this.name );
+
+		this.domElement = domElement;
+		this.viewElement = viewElement;
+
+		// Move to EditingController constructor.
+		editingView.addObserver( FocusObserver );
+
+		this.listenTo( editingView, 'focus', ( evt, data ) => {
+			if ( data.target === this.viewElement ) {
+				this.isFocused = true;
+			}
+		} );
+
+		this.listenTo( editingView, 'blur', ( evt, data ) => {
+			if ( data.target === this.viewElement ) {
+				this.isFocused = false;
+			}
+		} );
+	}
+
+	/**
+	 * Destroys the editable.
+	 */
+	destroy() {
+		this.stopListening();
+		this.domElement = this.viewElement = null;
+	}
+}
+
+utils.mix( Editable, ObservableMixin );

+ 60 - 0
src/editablecollection.js

@@ -0,0 +1,60 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import utils from './utils/utils.js';
+import Collection from './utils/collection.js';
+import ObservableMixin from './utils/observablemixin.js';
+
+/**
+ * A collection of {@link ckeditor5.Editable editables}.
+ *
+ * @memberOf ckeditor5
+ * @mixes utils.ObservaleMixin
+ * @extends utils.Collection
+ */
+export default class EditableCollection extends Collection {
+	/**
+	 * Creates a new instance of EditableCollection.
+	 */
+	constructor() {
+		super( { idProperty: 'name' } );
+
+		/**
+		 * The currently focused editable.
+		 *
+		 * @readonly
+		 * @observable
+		 * @member {ckeditor5.Editable} ckeditor5.EditableCollection#current
+		 */
+		this.set( 'current', null );
+
+		this.on( 'add', ( evt, editable ) => {
+			this.listenTo( editable, 'change:isFocused', ( evt, value ) => {
+				this.current = value ? editable : null;
+			} );
+		} );
+
+		this.on( 'remove', ( evt, editable ) => {
+			this.stopListening( editable );
+		} );
+	}
+
+	/**
+	 * Destroys the collection.
+	 */
+	destroy() {
+		this.stopListening();
+
+		for ( let editable of this ) {
+			editable.destroy();
+		}
+
+		this.clear();
+	}
+}
+
+utils.mix( EditableCollection, ObservableMixin );

+ 145 - 22
src/editor.js

@@ -8,7 +8,7 @@
 import ObservableMixin from './utils/observablemixin.js';
 import EditorConfig from './editorconfig.js';
 import PluginCollection from './plugincollection.js';
-import Document from './engine/treemodel/document.js';
+import EditableCollection from './editablecollection.js';
 import CKEditorError from './utils/ckeditorerror.js';
 import Locale from './utils/locale.js';
 import isArray from './utils/lib/lodash/isArray.js';
@@ -27,18 +27,26 @@ export default class Editor {
 	 * This constructor should be rarely used. When creating new editor instance use instead the
 	 * {@link CKEDITOR#create `CKEDITOR.create()` method}.
 	 *
-	 * @param {HTMLElement} element The DOM element that will be the source for the created editor.
+	 * @param {Object.<String, HTMLElement>|null} [elements] The DOM elements that will be the source
+	 * for the created editor.
 	 * @param {Object} config The editor config.
 	 */
-	constructor( element, config ) {
+	constructor( elements, config ) {
 		/**
-		 * The original host page element upon which the editor is created. It is only supposed to be provided on
-		 * editor creation and is not subject to be modified.
+		 * The original host page elements upon which the editor is created.
 		 *
 		 * @readonly
-		 * @member {HTMLElement} ckeditor5.Editor#element
+		 * @member {Map.<String, HTMLElement>|null} ckeditor5.Editor#elements
 		 */
-		this.element = element;
+		if ( elements ) {
+			this.elements = new Map();
+
+			for ( let name in elements ) {
+				this.elements.set( name, elements[ name ] );
+			}
+		} else {
+			this.elements = null;
+		}
 
 		/**
 		 * Holds all configurations specific to this editor instance.
@@ -61,18 +69,18 @@ export default class Editor {
 		this.plugins = new PluginCollection( this );
 
 		/**
-		 * Tree Model document managed by this editor.
+		 * The editables of the editor.
 		 *
 		 * @readonly
-		 * @member {engine.treeModel.Document} ckeditor5.Editor#document
+		 * @member {ckeditor5.EditableCollection} ckeditor5.Editor#editables
 		 */
-		this.document = new Document();
+		this.editables = new EditableCollection();
 
 		/**
 		 * Commands registered to the editor.
 		 *
 		 * @readonly
-		 * @member {Map} ckeditor5.Editor#commands
+		 * @member {Map.<ckeditor5.command.Command>} ckeditor5.Editor#commands
 		 */
 		this.commands = new Map();
 
@@ -90,14 +98,69 @@ export default class Editor {
 		 */
 		this.t = this.locale.t;
 
+		/**
+		 * Tree Model document managed by this editor.
+		 *
+		 * This property is set by the {@link ckeditor5.creator.Creator}.
+		 *
+		 * @readonly
+		 * @member {engine.treeModel.Document} ckeditor5.Editor#document
+		 */
+
+		/**
+		 * Instance of the {@link engine.treecontroller.EditingController editing controller}.
+		 *
+		 * This property is set by the {@link ckeditor5.creator.Creator}.
+		 *
+		 * @readonly
+		 * @member {engine.treecontroller.EditingController} ckeditor5.Editor#editing
+		 */
+
+		/**
+		 * Instance of the {@link engine.treecontroller.DataController data controller}.
+		 *
+		 * This property is set by the {@link ckeditor5.creator.Creator}.
+		 *
+		 * @readonly
+		 * @member {engine.treecontroller.DataController} ckeditor5.Editor#data
+		 */
+
 		/**
 		 * The chosen creator.
 		 *
 		 * @protected
-		 * @member {ckeditor5.Creator} ckeditor5.Editor#_creator
+		 * @member {ckeditor5.creator.Creator} ckeditor5.Editor#_creator
 		 */
 	}
 
+	/**
+	 * First element from {@link ckeditor5.Editor#elements}.
+	 *
+	 * @readonly
+	 * @type {HTMLElement|null}
+	 */
+	get firstElement() {
+		if ( !this.elements ) {
+			return null;
+		}
+
+		return utils.nth( 0, this.elements )[ 1 ];
+	}
+
+	/**
+	 * Name of the first element from {@link ckeditor5.Editor#elements}.
+	 *
+	 * @readonly
+	 * @type {String|null}
+	 */
+	get firstElementName() {
+		if ( !this.elements ) {
+			return null;
+		}
+
+		return utils.nth( 0, this.elements )[ 0 ];
+	}
+
 	/**
 	 * Initializes the editor instance object after its creation.
 	 *
@@ -168,26 +231,52 @@ export default class Editor {
 	 * @returns {Promise} A promise that resolves once the editor instance is fully destroyed.
 	 */
 	destroy() {
-		const that = this;
-
 		this.fire( 'destroy' );
 		this.stopListening();
 
+		// Note: The destruction order should be the reverse of the initialization order.
 		return Promise.resolve()
 			.then( () => {
-				return that._creator && that._creator.destroy();
+				return this._creator && this._creator.destroy();
 			} )
-			.then( () => {
-				delete this.element;
-			} );
+			.then( () => this.editables.destroy() );
 	}
 
-	setData( data ) {
-		this.editable.setData( data );
+	/**
+	 * Sets the data in the specified editor's editable root.
+	 *
+	 * @param {*} data The data to load.
+	 * @param {String} [editableRootName] The name of the editable root to which the data should be loaded.
+	 * If not specified and if there's only one editable root added to the editor, then the data will be loaded
+	 * to that editable.
+	 */
+	setData( data, editableRootName ) {
+		if ( !this.data ) {
+			/**
+			 * Data controller has not been defined yet, so methds like {@link ckeditor5.Editor#setData} and
+			 * {@link ckeditor5.Editor#getData} cannot be used.
+			 *
+			 * @error editor-no-datacontroller
+			 */
+			throw new CKEditorError( 'editor-no-datacontroller: Data controller has not been defined yet.' );
+		}
+
+		this.data.set( editableRootName || this._getDefaultRootName(), data );
 	}
 
-	getData() {
-		return this.editable.getData();
+	/**
+	 * Gets the data from the specified editor's editable root.
+	 *
+	 * @param {String} [editableRootName] The name of the editable root to get the data from.
+	 * If not specified and if there's only one editable root added to the editor, then the data will be retrieved
+	 * from it.
+	 */
+	getData( editableRootName ) {
+		if ( !this.data ) {
+			throw new CKEditorError( 'editor-no-datacontroller: Data controller has not been defined yet.' );
+		}
+
+		return this.data.get( editableRootName || this._getDefaultRootName() );
 	}
 
 	/**
@@ -210,6 +299,40 @@ export default class Editor {
 
 		command._execute( commandParam );
 	}
+
+	/**
+	 * Returns name of the editable root if there is only one. If there are multiple or no editable roots, throws an error.
+	 *
+	 * Note: The error message makes sense only for methods like {@link ckeditor5.Editor#setData} and
+	 * {@link ckeditor5.Editor#getData}.
+	 *
+	 * @private
+	 * @returns {String}
+	 */
+	_getDefaultRootName() {
+		const rootNames = Array.from( this.document.rootNames );
+
+		if ( rootNames.length > 1 ) {
+			/**
+			 * The name of the editable root must be specified. There are multiple editable roots added to the editor,
+			 * so the name of the editable must be specified.
+			 *
+			 * @error editor-editable-root-name-missing
+			 */
+			throw new CKEditorError( 'editor-editable-root-name-missing: The name of the editable root must be specified.' );
+		}
+
+		if ( rootNames.length === 0 ) {
+			/**
+			 * The editor does not contain any editable roots, so the data cannot be set or read from it.
+			 *
+			 * @error editor-no-editable-roots
+			 */
+			throw new CKEditorError( 'editor-no-editable-roots: There are no editable roots defined.' );
+		}
+
+		return rootNames[ 0 ];
+	}
 }
 
 utils.mix( Editor, ObservableMixin );

+ 1 - 1
src/plugin.js

@@ -53,7 +53,7 @@ export default class Plugin {
 	/**
 	 * Destroys the plugin.
 	 *
-	 * TODO waits to be implemented (#186).
+	 * @returns {null|Promise}
 	 */
 	destroy() {}
 }

+ 1 - 1
tests/_utils-tests/utils.js

@@ -7,7 +7,7 @@
 
 import testUtils from '/tests/ckeditor5/_utils/utils.js';
 import moduleTestUtils from '/tests/ckeditor5/_utils/module.js';
-import Creator from '/ckeditor5/creator.js';
+import Creator from '/ckeditor5/creator/creator.js';
 
 let createFn3 = () => {};
 let destroyFn3 = () => {};

+ 0 - 11
tests/_utils/ui/editable/inline/inlineeditable.js

@@ -1,11 +0,0 @@
-/**
- * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-'use strict';
-
-import Editable from '/ckeditor5/ui/editable/editable.js';
-
-export default class InlineEditable extends Editable {
-}

+ 4 - 4
tests/_utils/ui/editable/framed/framededitable.js → tests/_utils/ui/editableui/framed/framededitableui.js

@@ -5,11 +5,11 @@
 
 'use strict';
 
-import Editable from '/ckeditor5/ui/editable/editable.js';
+import EditableUI from '/ckeditor5/ui/editableui/editableui.js';
 
-export default class FramedEditable extends Editable {
-	constructor( editor ) {
-		super( editor );
+export default class FramedEditableUI extends EditableUI {
+	constructor( editor, editableModel ) {
+		super( editor, editableModel );
 
 		this.viewModel.bind( 'width', 'height' ).to( editor.ui );
 	}

+ 2 - 2
tests/_utils/ui/editable/framed/framededitableview.js → tests/_utils/ui/editableui/framed/framededitableuiview.js

@@ -5,9 +5,9 @@
 
 'use strict';
 
-import EditableView from '/ckeditor5/ui/editable/editableview.js';
+import EditableUIView from '/ckeditor5/ui/editableui/editableuiview.js';
 
-export default class FramedEditableView extends EditableView {
+export default class FramedEditableUIView extends EditableUIView {
 	constructor( model, locale ) {
 		super( model, locale );
 

+ 14 - 3
tests/_utils/ui/editable/inline/inlineeditableview.js → tests/_utils/ui/editableui/inline/inlineeditableuiview.js

@@ -5,13 +5,24 @@
 
 'use strict';
 
-import EditableView from '/ckeditor5/ui/editable/editableview.js';
+import EditableUIView from '/ckeditor5/ui/editableui/editableuiview.js';
 
-export default class InlineEditableView extends EditableView {
+export default class InlineEditableView extends EditableUIView {
 	constructor( model, locale, editableElement ) {
 		super( model, locale );
 
-		this.element = editableElement;
+		if ( editableElement ) {
+			this.element = editableElement;
+		} else {
+			const bind = this.attributeBinder;
+
+			this.template = {
+				tag: 'div',
+				attributes: {
+					contentEditable: bind.to( 'isEditable' )
+				}
+			};
+		}
 	}
 
 	init() {

+ 1 - 1
tests/_utils/ui/floatingtoolbar/floatingtoolbar.js

@@ -11,6 +11,6 @@ export default class FloatingToolbar extends Toolbar {
 	constructor( model, view, editor ) {
 		super( model, view, editor );
 
-		model.bind( 'isVisible' ).to( editor.editable, 'isFocused' );
+		model.bind( 'isVisible' ).to( editor.editables.get( model.editableName ), 'isFocused' );
 	}
 }

+ 1 - 1
tests/_utils/utils.js

@@ -49,7 +49,7 @@ const utils = {
 	 * be copied to the prototype of the creator.
 	 */
 	defineEditorCreatorMock( creatorName, proto ) {
-		moduleUtils.define( `creator-${ creatorName }/creator-${ creatorName }`, [ 'creator' ], ( Creator ) => {
+		moduleUtils.define( `creator-${ creatorName }/creator-${ creatorName }`, [ 'creator/creator' ], ( Creator ) => {
 			class TestCreator extends Creator {}
 
 			if ( proto ) {

+ 136 - 20
tests/ckeditor.js

@@ -10,9 +10,10 @@ import testUtils from '/tests/ckeditor5/_utils/utils.js';
 import CKEDITOR from '/ckeditor.js';
 import Editor from '/ckeditor5/editor.js';
 import Config from '/ckeditor5/utils/config.js';
+import CKEditorError from '/ckeditor5/utils/ckeditorerror.js';
 
-let content = document.getElementById( 'content' );
-let editorConfig = { creator: 'creator-test' };
+const content = document.getElementById( 'content' );
+const editorConfig = { creator: 'creator-test' };
 
 testUtils.createSinonSandbox();
 testUtils.defineEditorCreatorMock( 'test' );
@@ -32,14 +33,6 @@ describe( 'create', () => {
 	it( 'should create a new editor instance', () => {
 		return CKEDITOR.create( content, editorConfig ).then( ( editor ) => {
 			expect( editor ).to.be.instanceof( Editor );
-			expect( editor.element ).to.equal( content );
-		} );
-	} );
-
-	it( 'should create a new editor instance (using a selector)', () => {
-		return CKEDITOR.create( '.editor', editorConfig ).then( ( editor ) => {
-			expect( editor ).to.be.instanceof( Editor );
-			expect( editor.element ).to.equal( document.querySelector( '.editor' ) );
 		} );
 	} );
 
@@ -82,18 +75,141 @@ describe( 'create', () => {
 		} );
 	} );
 
-	it( 'should be rejected on element not found', () => {
-		let addSpy = testUtils.sinon.spy( CKEDITOR.instances, 'add' );
+	describe( 'elements param', () => {
+		const container = document.createElement( 'div' );
+		let el1, el2;
+
+		document.body.appendChild( container );
+
+		beforeEach( () => {
+			container.innerHTML = '';
+
+			el1 = document.createElement( 'div' );
+			el2 = document.createElement( 'div' );
+
+			container.appendChild( el1 );
+			container.appendChild( el2 );
+		} );
+
+		it( 'should work with a string', () => {
+			return CKEDITOR.create( 'div', editorConfig ).then( ( editor ) => {
+				assertElements( editor, document.querySelectorAll( 'div' ).length );
+			} );
+		} );
+
+		it( 'should work with an HTMLElement', () => {
+			return CKEDITOR.create( el1, editorConfig ).then( ( editor ) => {
+				assertElements( editor, 1 );
+			} );
+		} );
+
+		it( 'should work with a NodeList', () => {
+			const elements = container.querySelectorAll( 'div' );
+
+			return CKEDITOR.create( elements, editorConfig ).then( ( editor ) => {
+				assertElements( editor, 2 );
+			} );
+		} );
+
+		it( 'should work with an HTMLCollection', () => {
+			const elements = container.getElementsByTagName( 'div' );
+
+			return CKEDITOR.create( elements, editorConfig ).then( ( editor ) => {
+				assertElements( editor, 2 );
+			} );
+		} );
+
+		it( 'should work with an array', () => {
+			const elements = Array.from( container.getElementsByTagName( 'div' ) );
+
+			return CKEDITOR.create( elements, editorConfig ).then( ( editor ) => {
+				assertElements( editor, 2 );
+			} );
+		} );
+
+		it( 'should work with an object', () => {
+			const elements = {
+				editableA: el1,
+				editableB: el2
+			};
+
+			return CKEDITOR.create( elements, editorConfig ).then( ( editor ) => {
+				assertElements( editor, 2 );
+			} );
+		} );
+
+		it( 'should be rejected on element not found (when string passed)', () => {
+			let addSpy = testUtils.sinon.spy( CKEDITOR.instances, 'add' );
+
+			return CKEDITOR.create( '.undefined' )
+				.then( () => {
+					throw new Error( 'It should not enter this function.' );
+				} )
+				.catch( ( error ) => {
+					expect( error ).to.be.instanceof( CKEditorError );
+					expect( error.message ).to.match( /^ckeditor5-create-no-elements:/ );
+
+					// We need to make sure that create()'s execution is stopped.
+					// Assertion based on a real mistake we made that reject() wasn't followed by a return.
+					sinon.assert.notCalled( addSpy );
+				} );
+		} );
+
+		it( 'should be rejected on an empty elements array-like obj', () => {
+			return CKEDITOR.create( [] )
+				.then( () => {
+					throw new Error( 'It should not enter this function.' );
+				} )
+				.catch( ( error ) => {
+					expect( error ).to.be.instanceof( CKEditorError );
+					expect( error.message ).to.match( /^ckeditor5-create-no-elements:/ );
+				} );
+		} );
+
+		it( 'should be rejected on an empty object', () => {
+			return CKEDITOR.create( {} )
+				.then( () => {
+					throw new Error( 'It should not enter this function.' );
+				} )
+				.catch( ( error ) => {
+					expect( error ).to.be.instanceof( CKEditorError );
+					expect( error.message ).to.match( /^ckeditor5-create-no-elements:/ );
+				} );
+		} );
+
+		it( 'should take names from the ids or data-editable attributes', () => {
+			el1.id = 'foo';
+			el2.dataset.editable = 'bar';
 
-		return CKEDITOR.create( '.undefined' ).then( () => {
-			throw new Error( 'It should not enter this function' );
-		} ).catch( ( error ) => {
-			expect( error ).to.be.instanceof( Error );
-			expect( error.message ).to.equal( 'Element not found' );
-			// We need to make sure that create()'s execution is stopped.
-			// Assertion based on a real mistake we made that reject() wasn't followed by a return.
-			sinon.assert.notCalled( addSpy );
+			return CKEDITOR.create( [ el1, el2 ], editorConfig )
+				.then( ( editor ) => {
+					expect( editor.elements.get( 'foo' ) ).to.equal( el1 );
+					expect( editor.elements.get( 'bar' ) ).to.equal( el2 );
+				} );
 		} );
+
+		it( 'should take names from the object keys', () => {
+			el1.id = 'foo';
+			el2.dataset.editable = 'bar';
+
+			return CKEDITOR.create( { a: el1, b: el2 }, editorConfig )
+				.then( ( editor ) => {
+					expect( editor.elements.get( 'a' ) ).to.equal( el1 );
+					expect( editor.elements.get( 'b' ) ).to.equal( el2 );
+				} );
+		} );
+
+		it( 'should generate editableN names', () => {
+			return CKEDITOR.create( [ el1, el2 ], editorConfig )
+				.then( ( editor ) => {
+					expect( Array.from( editor.elements.keys() ).join( ',' ) ).to.match( /^editable\d+,editable\d+$/ );
+				} );
+		} );
+
+		function assertElements( editor, expectedSize ) {
+			expect( editor.elements ).to.be.instanceof( Map );
+			expect( editor.elements ).to.have.property( 'size', expectedSize );
+		}
 	} );
 } );
 

+ 4 - 5
tests/command/attributecommand.js

@@ -6,21 +6,20 @@
 'use strict';
 
 import Editor from '/ckeditor5/editor.js';
+import Document from '/ckeditor5/engine/treemodel/document.js';
 import AttributeCommand from '/ckeditor5/command/attributecommand.js';
 import Text from '/ckeditor5/engine/treemodel/text.js';
 import Range from '/ckeditor5/engine/treemodel/range.js';
 import Position from '/ckeditor5/engine/treemodel/position.js';
 import Element from '/ckeditor5/engine/treemodel/element.js';
 
-let element, editor, command, modelDoc, root;
+let editor, command, modelDoc, root;
 
 const attrKey = 'bold';
 
 beforeEach( () => {
-	element = document.createElement( 'div' );
-	document.body.appendChild( element );
-
-	editor = new Editor( element );
+	editor = new Editor();
+	editor.document = new Document();
 	modelDoc = editor.document;
 	root = modelDoc.createRoot( 'root', 'div' );
 

+ 9 - 181
tests/creator/creator.js

@@ -8,9 +8,9 @@
 'use strict';
 
 import testUtils from '/tests/ckeditor5/_utils/utils.js';
-import Creator from '/ckeditor5/creator.js';
-import Editor from '/ckeditor5/editor.js';
+import Creator from '/ckeditor5/creator/creator.js';
 import Plugin from '/ckeditor5/plugin.js';
+import Editor from '/ckeditor5/editor.js';
 
 testUtils.createSinonSandbox();
 
@@ -18,191 +18,19 @@ describe( 'Creator', () => {
 	let creator, editor;
 
 	beforeEach( () => {
-		const editorElement = document.createElement( 'div' );
-		document.body.appendChild( editorElement );
-
-		editor = new Editor( editorElement );
+		editor = new Editor();
 		creator = new Creator( editor );
 	} );
 
-	describe( 'create', () => {
-		it( 'should init the UI', () => {
-			const promise = new Promise( () => {} );
-
-			editor.ui = {
-				init: sinon.stub().returns( promise )
-			};
-
-			const ret = creator.create();
-
-			expect( ret ).to.equal( promise );
-			expect( editor.ui.init.called ).to.be.true;
-		} );
-
-		it( 'should not fail when there is no UI', () => {
-			expect( editor.ui ).to.not.exist;
-
-			return creator.create()
-				.then(); // Just checking whether a promise was returned.
-		} );
-	} );
-
-	describe( 'destroy', () => {
-		it( 'calls super.destroy', () => {
-			const pluginSpy  = testUtils.sinon.spy( Plugin.prototype, 'destroy' );
-
-			editor.ui = {
-				destroy() {}
-			};
-
-			creator.destroy();
-
-			expect( pluginSpy.called ).to.be.true;
-		} );
-
-		it( 'should destroy the UI (sync)', () => {
-			const uiSpy = sinon.spy();
-
-			editor.ui = {
-				destroy: uiSpy
-			};
-
-			return creator.destroy()
-				.then( () => {
-					expect( uiSpy.called ).to.be.true;
-					expect( editor.ui ).to.be.null;
-				} );
-		} );
-
-		it( 'should destroy the UI (async)', () => {
-			const uiSpy = sinon.stub().returns( Promise.resolve() );
-
-			editor.ui = {
-				destroy: uiSpy
-			};
-
-			return creator.destroy()
-				.then( () => {
-					expect( uiSpy.called ).to.be.true;
-					expect( editor.ui ).to.be.null;
-				} );
-		} );
-
-		it( 'should wait until UI is destroyed (async)', () => {
-			let resolved = false;
-			let resolve;
-			const uiSpy = sinon.stub().returns(
-				new Promise( ( r ) => {
-					resolve = r;
-				} )
-			);
-
-			editor.ui = {
-				destroy: uiSpy
-			};
-
-			// Is there an easier method to verify whether the promise chain isn't broken? ;/
-			setTimeout( () => {
-				resolved = true;
-				resolve( 'foo' );
-			} );
-
-			return creator.destroy()
-				.then( () => {
-					expect( resolved ).to.be.true;
-				} );
-		} );
-
-		it( 'should restore the replaced element', () => {
-			const spy = testUtils.sinon.stub( creator, '_restoreElement' );
-			const element = document.createElement( 'div' );
-
-			editor.ui = {
-				destroy() {}
-			};
-
-			creator._replaceElement( element );
-			creator.destroy();
-
-			expect( spy.calledOnce ).to.be.true;
-		} );
-	} );
-
-	describe( 'updateEditorElement', () => {
-		it( 'should pass data to the element', () => {
-			editor.editable = {
-				getData() {
-					return 'foo';
-				}
-			};
-
-			creator.updateEditorElement();
-
-			expect( editor.element.innerHTML ).to.equal( 'foo' );
-		} );
-	} );
-
-	describe( 'loadDataFromEditorElement', () => {
-		it( 'should pass data to the element', () => {
-			editor.editable = {
-				setData: sinon.spy()
-			};
-
-			editor.element.innerHTML = 'foo';
-			creator.loadDataFromEditorElement();
-
-			expect( editor.editable.setData.args[ 0 ][ 0 ] ).to.equal( 'foo' );
-		} );
-	} );
-
-	describe( 'getDataFromElement', () => {
-		[ 'textarea', 'template', 'div' ].forEach( ( elementName ) => {
-			it( 'should return the content of a ' + elementName, function() {
-				const data = Creator.getDataFromElement( document.getElementById( 'getData-' + elementName ) );
-				expect( data ).to.equal( '<b>foo</b>' );
-			} );
+	describe( 'constructor', () => {
+		it( 'inherits from the Plugin', () => {
+			expect( creator ).to.be.instanceof( Plugin );
 		} );
 	} );
 
-	describe( 'setDataInElement', () => {
-		[ 'textarea', 'template', 'div' ].forEach( ( elementName ) => {
-			it( 'should set the content of a ' + elementName, () => {
-				const el = document.createElement( elementName );
-				const expectedData = '<b>foo</b>';
-
-				Creator.setDataInElement( el, expectedData );
-
-				const actualData = Creator.getDataFromElement( el );
-				expect( actualData ).to.equal( actualData );
-			} );
-		} );
-	} );
-
-	describe( '_replaceElement', () => {
-		it( 'should use editor ui element when arg not provided', () => {
-			editor.ui = {
-				view: {
-					element: document.createElement( 'div' )
-				}
-			};
-
-			creator._replaceElement();
-
-			expect( editor.element.nextSibling ).to.equal( editor.ui.view.element );
-		} );
-	} );
-
-	describe( '_restoreElement', () => {
-		it( 'should remove the replacement element', () => {
-			const element = document.createElement( 'div' );
-
-			creator._replaceElement( element );
-
-			expect( editor.element.nextSibling ).to.equal( element );
-
-			creator._restoreElement();
-
-			expect( element.parentNode ).to.be.null;
+	describe( 'create', () => {
+		it( 'returns a promise', () => {
+			expect( creator.create() ).to.be.instanceof( Promise );
 		} );
 	} );
 } );

+ 48 - 34
tests/creator/manual/_utils/creator/classiccreator.js

@@ -5,31 +5,55 @@
 
 'use strict';
 
-import Creator from '/ckeditor5/creator.js';
+import StandardCreator from '/ckeditor5/creator/standardcreator.js';
+
+import HtmlDataProcessor from '/ckeditor5/engine/dataprocessor/htmldataprocessor.js';
+import Editable from '/ckeditor5/editable.js';
+
+import { createEditableUI, createEditorUI } from '/ckeditor5/ui/creator-utils.js';
+
 import BoxedEditorUI from '/tests/ckeditor5/_utils/ui/boxededitorui/boxededitorui.js';
 import BoxedEditorUIView from '/tests/ckeditor5/_utils/ui/boxededitorui/boxededitoruiview.js';
-import FramedEditable from '/tests/ckeditor5/_utils/ui/editable/framed/framededitable.js';
-import FramedEditableView from '/tests/ckeditor5/_utils/ui/editable/framed/framededitableview.js';
+import FramedEditableUI from '/tests/ckeditor5/_utils/ui/editableui/framed/framededitableui.js';
+import FramedEditableUIView from '/tests/ckeditor5/_utils/ui/editableui/framed/framededitableuiview.js';
 import Model from '/ckeditor5/ui/model.js';
 import Toolbar from '/ckeditor5/ui/bindings/toolbar.js';
 import ToolbarView from '/ckeditor5/ui/toolbar/toolbarview.js';
+
 import { imitateFeatures, imitateDestroyFeatures } from '../imitatefeatures.js';
 
-export default class ClassicCreator extends Creator {
+export default class ClassicCreator extends StandardCreator {
 	constructor( editor ) {
-		super( editor );
+		super( editor, new HtmlDataProcessor() );
 
-		editor.ui = this._createEditorUI();
+		const editableName = editor.firstElementName;
+		editor.editables.add( new Editable( editor, editableName ) );
+		editor.document.createRoot( editableName, '$root' );
+
+		// UI.
+		createEditorUI( editor, BoxedEditorUI, BoxedEditorUIView );
+
+		// Data controller mock.
+		this._mockDataController();
 	}
 
 	create() {
-		imitateFeatures( this.editor );
+		const editor = this.editor;
+		const editable = editor.editables.get( 0 );
+
+		// Features mock.
+		imitateFeatures( editor );
 
-		this._replaceElement();
-		this._setupEditable();
-		this._setupToolbar();
+		// UI.
+		this._replaceElement( editor.firstElement, editor.ui.view.element );
+		this._createToolbar();
+		editor.ui.add( 'main', createEditableUI( editor, editable, FramedEditableUI, FramedEditableUIView ) );
 
+		// Init.
 		return super.create()
+			.then( () => editor.ui.init() )
+			// We'll be able to do that much earlier once the loading will be done to the document model,
+			// rather than straight to the editable.
 			.then( () => this.loadDataFromEditorElement() );
 	}
 
@@ -38,40 +62,30 @@ export default class ClassicCreator extends Creator {
 
 		this.updateEditorElement();
 
-		return super.destroy();
-	}
-
-	_setupEditable() {
-		const editable = this._createEditable();
+		super.destroy();
 
-		this.editor.editable = editable;
-		this.editor.ui.add( 'main', editable );
+		return this.editor.ui.destroy();
 	}
 
-	_setupToolbar() {
+	_createToolbar() {
+		const editor = this.editor;
 		const toolbarModel = new Model();
-		const toolbar = new Toolbar( toolbarModel, new ToolbarView( toolbarModel, this.editor.locale ), this.editor );
+		const toolbar = new Toolbar( toolbarModel, new ToolbarView( toolbarModel, editor.locale ), editor );
 
-		toolbar.addButtons( this.editor.config.toolbar );
+		toolbar.addButtons( editor.config.toolbar );
 
 		this.editor.ui.add( 'top', toolbar );
 	}
 
-	_createEditable() {
-		const editable = new FramedEditable( this.editor );
-		const editableView = new FramedEditableView( editable.viewModel, this.editor.locale );
-
-		editable.view = editableView;
-
-		return editable;
-	}
-
-	_createEditorUI() {
-		const editorUI = new BoxedEditorUI( this.editor );
-		const editorUIView = new BoxedEditorUIView( editorUI.viewModel, this.editor.locale );
+	_mockDataController() {
+		const editor = this.editor;
 
-		editorUI.view = editorUIView;
+		editor.data.get = ( rootName ) => {
+			return editor.editables.get( rootName ).domElement.innerHTML + `<p>getData( '${ rootName }' )</p>`;
+		};
 
-		return editorUI;
+		this.editor.data.set = ( rootName, data ) => {
+			editor.editables.get( rootName ).domElement.innerHTML = data + `<p>setData( '${ rootName }' )</p>`;
+		};
 	}
 }

+ 53 - 30
tests/creator/manual/_utils/creator/inlinecreator.js

@@ -5,30 +5,50 @@
 
 'use strict';
 
-import Creator from '/ckeditor5/creator.js';
+import StandardCreator from '/ckeditor5/creator/standardcreator.js';
+
+import HtmlDataProcessor from '/ckeditor5/engine/dataprocessor/htmldataprocessor.js';
+import Editable from '/ckeditor5/editable.js';
+
+import { createEditableUI, createEditorUI } from '/ckeditor5/ui/creator-utils.js';
+
 import EditorUIView from '/ckeditor5/ui/editorui/editoruiview.js';
 import BoxlessEditorUI from '/tests/ckeditor5/_utils/ui/boxlesseditorui/boxlesseditorui.js';
-import InlineEditable from '/tests/ckeditor5/_utils/ui/editable/inline/inlineeditable.js';
-import InlineEditableView from '/tests/ckeditor5/_utils/ui/editable/inline/inlineeditableview.js';
+import EditableUI from '/ckeditor5/ui/editableui/editableui.js';
+import InlineEditableUIView from '/tests/ckeditor5/_utils/ui/editableui/inline/inlineeditableuiview.js';
 import Model from '/ckeditor5/ui/model.js';
 import FloatingToolbar from '/tests/ckeditor5/_utils/ui/floatingtoolbar/floatingtoolbar.js';
 import FloatingToolbarView from '/tests/ckeditor5/_utils/ui/floatingtoolbar/floatingtoolbarview.js';
 import { imitateFeatures, imitateDestroyFeatures } from '../imitatefeatures.js';
 
-export default class InlineCreator extends Creator {
+export default class InlineCreator extends StandardCreator {
 	constructor( editor ) {
-		super( editor );
+		super( editor, new HtmlDataProcessor() );
+
+		this._createEditable();
 
-		editor.ui = this._createEditorUI();
+		createEditorUI( editor, BoxlessEditorUI, EditorUIView );
+
+		// Data controller mock.
+		this._mockDataController();
 	}
 
 	create() {
-		imitateFeatures( this.editor );
+		const editor = this.editor;
+		const editable = editor.editables.get( 0 );
+
+		// Features mock.
+		imitateFeatures( editor );
 
-		this._setupEditable();
-		this._setupToolbar();
+		// UI.
+		this._createToolbars();
+		editor.ui.add( 'editable', createEditableUI( editor, editable, EditableUI, InlineEditableUIView ) );
 
+		// Init.
 		return super.create()
+			.then( () => editor.ui.init() )
+			// We'll be able to do that much earlier once the loading will be done to the document model,
+			// rather than straight to the editable.
 			.then( () => this.loadDataFromEditorElement() );
 	}
 
@@ -37,20 +57,28 @@ export default class InlineCreator extends Creator {
 
 		this.updateEditorElement();
 
-		return super.destroy();
-	}
+		super.destroy();
 
-	_setupEditable() {
-		this.editor.editable = this._createEditable();
+		return this.editor.ui.destroy();
+	}
 
-		this.editor.ui.add( 'editable', this.editor.editable );
+	_createEditable() {
+		const editor = this.editor;
+		const editorElement = editor.firstElement;
+		const editableName = editor.firstElementName;
+		const editable = new Editable( editor, editableName );
+
+		editor.editables.add( editable );
+		editable.bindTo( editorElement );
+		editor.document.createRoot( editableName, '$root' );
 	}
 
-	_setupToolbar() {
+	_createToolbars() {
+		const editableName = this.editor.firstElementName;
 		const locale = this.editor.locale;
 
-		const toolbar1Model = new Model();
-		const toolbar2Model = new Model();
+		const toolbar1Model = new Model( null, { editableName } );
+		const toolbar2Model = new Model( null, { editableName } );
 
 		const toolbar1 = new FloatingToolbar( toolbar1Model, new FloatingToolbarView( toolbar1Model, locale ), this.editor );
 		const toolbar2 = new FloatingToolbar( toolbar2Model, new FloatingToolbarView( toolbar2Model, locale ), this.editor );
@@ -62,20 +90,15 @@ export default class InlineCreator extends Creator {
 		this.editor.ui.add( 'body', toolbar2 );
 	}
 
-	_createEditable() {
-		const editable = new InlineEditable( this.editor );
-		const editableView = new InlineEditableView( editable.viewModel, this.editor.locale, this.editor.element );
-
-		editable.view = editableView;
-
-		return editable;
-	}
-
-	_createEditorUI() {
-		const editorUI = new BoxlessEditorUI( this.editor );
+	_mockDataController() {
+		const editor = this.editor;
 
-		editorUI.view = new EditorUIView( editorUI.viewModel, this.editor.locale );
+		editor.data.get = ( rootName ) => {
+			return editor.editables.get( rootName ).domElement.innerHTML + `<p>getData( '${ rootName }' )</p>`;
+		};
 
-		return editorUI;
+		this.editor.data.set = ( rootName, data ) => {
+			editor.editables.get( rootName ).domElement.innerHTML = data + `<p>setData( '${ rootName }' )</p>`;
+		};
 	}
 }

+ 105 - 0
tests/creator/manual/_utils/creator/multicreator.js

@@ -0,0 +1,105 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import StandardCreator from '/ckeditor5/creator/standardcreator.js';
+
+import HtmlDataProcessor from '/ckeditor5/engine/dataprocessor/htmldataprocessor.js';
+import Editable from '/ckeditor5/editable.js';
+
+import { createEditableUI, createEditorUI } from '/ckeditor5/ui/creator-utils.js';
+
+import BoxedEditorUI from '/tests/ckeditor5/_utils/ui/boxededitorui/boxededitorui.js';
+import BoxedEditorUIView from '/tests/ckeditor5/_utils/ui/boxededitorui/boxededitoruiview.js';
+import EditableUI from '/ckeditor5/ui/editableui/editableui.js';
+import InlineEditableUIView from '/tests/ckeditor5/_utils/ui/editableui/inline/inlineeditableuiview.js';
+import Model from '/ckeditor5/ui/model.js';
+import Toolbar from '/ckeditor5/ui/bindings/toolbar.js';
+import ToolbarView from '/ckeditor5/ui/toolbar/toolbarview.js';
+import { imitateFeatures, imitateDestroyFeatures } from '../imitatefeatures.js';
+
+export default class MultiCreator extends StandardCreator {
+	constructor( editor ) {
+		super( editor, new HtmlDataProcessor() );
+
+		// Engine.
+		this._createEditables();
+
+		// UI.
+		createEditorUI( editor, BoxedEditorUI, BoxedEditorUIView );
+
+		// Data controller mock.
+		this._mockDataController();
+	}
+
+	create() {
+		const editor = this.editor;
+
+		// Features mock.
+		imitateFeatures( editor );
+
+		// UI.
+		this._createToolbar();
+
+		for ( let editable of editor.editables ) {
+			editor.ui.add( 'main', createEditableUI( editor, editable, EditableUI, InlineEditableUIView ) );
+		}
+
+		editor.elements.forEach( ( element ) => {
+			this._replaceElement( element, null );
+		} );
+
+		// Init.
+		return super.create()
+			.then( () => editor.ui.init() )
+			// We'll be able to do that much earlier once the loading will be done to the document model,
+			// rather than straight to the editable.
+			.then( () => this.loadDataFromEditorElements() );
+	}
+
+	destroy() {
+		imitateDestroyFeatures();
+
+		this.updateEditorElements();
+
+		super.destroy();
+
+		this.editor.ui.destroy();
+	}
+
+	_createEditables() {
+		const editor = this.editor;
+
+		editor.elements.forEach( ( editorElement, editableName ) => {
+			const editable = new Editable( editor, editableName );
+
+			editor.editables.add( editable );
+			editor.document.createRoot( editableName, '$root' );
+		} );
+	}
+
+	_createToolbar() {
+		const editor = this.editor;
+		const toolbarModel = new Model();
+		const toolbar = new Toolbar( toolbarModel, new ToolbarView( toolbarModel, editor.locale ), editor );
+
+		toolbar.addButtons( editor.config.toolbar );
+
+		this.editor.ui.add( 'top', toolbar );
+	}
+
+	_mockDataController() {
+		const editor = this.editor;
+
+		editor.data.get = ( rootName ) => {
+			return editor.editables.get( rootName ).domElement.innerHTML + `<p>getData( '${ rootName }' )</p>`;
+		};
+
+		this.editor.data.set = ( rootName, data ) => {
+			editor.editables.get( rootName ).domElement.innerHTML = data + `<p>setData( '${ rootName }' )</p>`;
+		};
+	}
+}

+ 5 - 4
tests/creator/manual/creator-classic.html

@@ -2,11 +2,12 @@
 	<link rel="stylesheet" href="%TEST_DIR%_assets/styles.css">
 </head>
 
+<p>
+	<button id="destroyEditor">Destroy editor</button>
+	<button id="initEditor">Init editor</button>
+</p>
+
 <div id="editor">
 	<h1>Hello world!</h1>
 	<p>This is an editor instance.</p>
 </div>
-
-<button id="destroyEditor">Destroy editor</button>
-<button id="initEditor">Init editor</button>
-

+ 6 - 5
tests/creator/manual/creator-classic.js

@@ -11,7 +11,7 @@ import CKEDITOR from '/ckeditor.js';
 import ClassicCreator from '/tests/ckeditor5/creator/manual/_utils/creator/classiccreator.js';
 import testUtils from '/tests/utils/_utils/utils.js';
 
-let editor, observer;
+let editor, editable, observer;
 
 function initEditor() {
 	CKEDITOR.create( '#editor', {
@@ -24,20 +24,21 @@ function initEditor() {
 	} )
 	.then( ( newEditor ) => {
 		console.log( 'Editor was initialized', newEditor );
-		console.log( 'You can now play with it using global `editor` variable.' );
+		console.log( 'You can now play with it using global `editor` and `editable` variables.' );
 
 		window.editor = editor = newEditor;
+		window.editable = editable = editor.editables.get( 0 );
 
 		observer = testUtils.createObserver();
-		observer.observe( 'Editable', editor.editable );
+		observer.observe( 'Editable', editable );
 	} );
 }
 
 function destroyEditor() {
 	editor.destroy()
 		.then( () => {
-			window.editor = null;
-			editor = null;
+			window.editor = editor = null;
+			window.editable = editable = null;
 
 			observer.stopListening();
 			observer = null;

+ 1 - 1
tests/creator/manual/creator-classic.md

@@ -16,7 +16,7 @@
 ## Notes:
 
 * You can play with:
-  * `editor.editable.isEditable`,
+  * `editable.isEditable`,
   * `editor.ui.width/height`.
   * `boldModel.isEnabled` and `italicModel.isEnabled`.
 * Changes to `editable.isFocused/isEditable` should be logged to the console.

+ 5 - 4
tests/creator/manual/creator-inline.html

@@ -2,11 +2,12 @@
 	<link rel="stylesheet" href="%TEST_DIR%_assets/styles.css">
 </head>
 
+<p>
+	<button id="destroyEditor">Destroy editor</button>
+	<button id="initEditor">Init editor</button>
+</p>
+
 <div id="editor">
 	<h1>Hello world!</h1>
 	<p>This is an editor instance.</p>
 </div>
-
-<button id="destroyEditor">Destroy editor</button>
-<button id="initEditor">Init editor</button>
-

+ 6 - 5
tests/creator/manual/creator-inline.js

@@ -11,7 +11,7 @@ import CKEDITOR from '/ckeditor.js';
 import InlineCreator from '/tests/ckeditor5/creator/manual/_utils/creator/inlinecreator.js';
 import testUtils from '/tests/utils/_utils/utils.js';
 
-let editor, observer;
+let editor, editable, observer;
 
 function initEditor() {
 	CKEDITOR.create( '#editor', {
@@ -20,20 +20,21 @@ function initEditor() {
 	} )
 	.then( ( newEditor ) => {
 		console.log( 'Editor was initialized', newEditor );
-		console.log( 'You can now play with it using global `editor` variable.' );
+		console.log( 'You can now play with it using global `editor` and `editable` variable.' );
 
 		window.editor = editor = newEditor;
+		window.editable = editable = editor.editables.get( 0 );
 
 		observer = testUtils.createObserver();
-		observer.observe( 'Editable', editor.editable );
+		observer.observe( 'Editable', editable );
 	} );
 }
 
 function destroyEditor() {
 	editor.destroy()
 		.then( () => {
-			window.editor = null;
-			editor = null;
+			window.editor = editor = null;
+			window.editable = editable = null;
 
 			observer.stopListening();
 			observer = null;

+ 1 - 1
tests/creator/manual/creator-inline.md

@@ -15,7 +15,7 @@
 ## Notes:
 
 * You can play with:
-  * `editor.editable.isEditable`.
+  * `editable.isEditable`,
   * `boldModel.isEnabled` and `italicModel.isEnabled`.
 * Changes to `editable.isFocused/isEditable` should be logged to the console.
 * Buttons' states should be synchronised between toolbars (they share models).

+ 20 - 0
tests/creator/manual/creator-multi.html

@@ -0,0 +1,20 @@
+<head>
+	<link rel="stylesheet" href="%TEST_DIR%_assets/styles.css">
+</head>
+
+<p>
+	<button id="destroyEditor">Destroy editor</button>
+	<button id="initEditor">Init editor</button>
+</p>
+
+<div id="editorContainer"></div>
+
+<div id="editable1" class="editor">
+	<h1>Hello world!</h1>
+	<p>This is an editor instance.</p>
+</div>
+
+<div id="editable2" class="editor">
+	<h1>Hello again!</h1>
+	<p>This is the same editor instance.</p>
+</div>

+ 60 - 0
tests/creator/manual/creator-multi.js

@@ -0,0 +1,60 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* global console:false */
+
+'use strict';
+
+import CKEDITOR from '/ckeditor.js';
+import MultiCreator from '/tests/ckeditor5/creator/manual/_utils/creator/multicreator.js';
+import testUtils from '/tests/utils/_utils/utils.js';
+
+let editor, editables, observer;
+
+function initEditor() {
+	CKEDITOR.create( '.editor', {
+		creator: MultiCreator,
+		toolbar: [ 'bold', 'italic' ]
+	} )
+	.then( ( newEditor ) => {
+		console.log( 'Editor was initialized', newEditor );
+		console.log( 'You can now play with it using global `editor` and `editables` variables.' );
+
+		window.editor = editor = newEditor;
+		window.editables = editables = editor.editables;
+
+		const editable1 = editables.get( 'editable1' );
+		const editable2 = editables.get( 'editable2' );
+
+		editable1.toString = editable2.toString = function() {
+			return `Editable(${ this.name })`;
+		};
+
+		observer = testUtils.createObserver();
+		observer.observe( 'Editable 1', editable1 );
+		observer.observe( 'Editable 2', editable2 );
+		observer.observe( 'EditableCollection', editables );
+
+		document.getElementById( 'editorContainer' ).appendChild( editor.ui.view.element );
+	} );
+}
+
+function destroyEditor() {
+	editor.destroy()
+		.then( () => {
+			window.editor = editor = null;
+			window.editables = editables = null;
+
+			observer.stopListening();
+			observer = null;
+
+			document.getElementById( 'editorContainer' ).innerHTML = '';
+
+			console.log( 'Editor was destroyed' );
+		} );
+}
+
+document.getElementById( 'initEditor' ).addEventListener( 'click', initEditor );
+document.getElementById( 'destroyEditor' ).addEventListener( 'click', destroyEditor );

+ 22 - 0
tests/creator/manual/creator-multi.md

@@ -0,0 +1,22 @@
+@bender-ui: collapsed
+
+1. Click "Init editor".
+2. Expected:
+  * Boxed editor with two editables should be created.
+  * Original elements should disappear.
+  * There should be a toolbar with "Bold" and "Italic" buttons.
+3. Click "Destroy editor".
+4. Expected:
+  * Editor should be destroyed.
+  * Original elements should be visible.
+  * The elements should contain their data (updated).
+  * The 'ck-body region' should be removed.
+
+## Notes:
+
+* You can play with:
+  * `editables.get( 'editable1/2' ).isEditable`,
+  * `boldModel.isEnabled` and `italicModel.isEnabled`.
+* Changes to `editable.isFocused/isEditable` should be logged to the console.
+* Changes to `editables.current` should be logged to the console.
+* Clicks on the buttons should be logged to the console.

+ 0 - 0
tests/creator/creator.html → tests/creator/standardcreator.html


+ 228 - 0
tests/creator/standardcreator.js

@@ -0,0 +1,228 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: creator */
+
+'use strict';
+
+import testUtils from '/tests/ckeditor5/_utils/utils.js';
+import Creator from '/ckeditor5/creator/creator.js';
+import StandardCreator from '/ckeditor5/creator/standardcreator.js';
+import Editor from '/ckeditor5/editor.js';
+import HtmlDataProcessor from '/ckeditor5/engine/dataprocessor/htmldataprocessor.js';
+import Document from '/ckeditor5/engine/treemodel/document.js';
+import EditingController from '/ckeditor5/engine/treecontroller/editingcontroller.js';
+import DataController from '/ckeditor5/engine/treecontroller/datacontroller.js';
+
+testUtils.createSinonSandbox();
+
+describe( 'Creator', () => {
+	let creator, editor;
+
+	beforeEach( () => {
+		const firstElement = document.createElement( 'div' );
+		document.body.appendChild( firstElement );
+
+		const secondElement = document.createElement( 'div' );
+		document.body.appendChild( secondElement );
+
+		editor = new Editor( { first: firstElement, second: secondElement } );
+		creator = new StandardCreator( editor, new HtmlDataProcessor() );
+	} );
+
+	describe( 'constructor', () => {
+		it( 'inherits from the Creator', () => {
+			expect( creator ).to.be.instanceof( Creator );
+		} );
+
+		it( 'creates the engine', () => {
+			expect( editor.document ).to.be.instanceof( Document );
+			expect( editor.editing ).to.be.instanceof( EditingController );
+			expect( editor.data ).to.be.instanceof( DataController );
+			expect( editor.data.processor ).to.be.instanceof( HtmlDataProcessor );
+		} );
+	} );
+
+	describe( 'create', () => {
+		it( 'returns a promise', () => {
+			expect( creator.create() ).to.be.instanceof( Promise );
+		} );
+	} );
+
+	describe( 'destroy', () => {
+		it( 'calls super.destroy', () => {
+			const creatorSpy = testUtils.sinon.spy( Creator.prototype, 'destroy' );
+
+			creator.destroy();
+
+			expect( creatorSpy.called ).to.be.true;
+		} );
+
+		it( 'should destroy the engine', () => {
+			const spy = editor.document.destroy = editor.data.destroy = editor.editing.destroy = sinon.spy();
+
+			creator.destroy();
+
+			expect( spy.callCount ).to.equal( 3 );
+		} );
+
+		it( 'should restore the replaced element', () => {
+			const spy = testUtils.sinon.stub( creator, '_restoreElements' );
+
+			creator.destroy();
+
+			expect( spy.calledOnce ).to.be.true;
+		} );
+	} );
+
+	describe( 'updateEditorElement', () => {
+		it( 'should pass data to the first element when element name not specified', () => {
+			editor.getData = ( rootName ) => {
+				expect( rootName ).to.equal( 'first' );
+
+				return 'foo';
+			};
+
+			creator.updateEditorElement();
+
+			expect( editor.firstElement.innerHTML ).to.equal( 'foo' );
+		} );
+
+		it( 'should pass data to the given element', () => {
+			editor.elements.set( 'second', document.createElement( 'div' ) );
+
+			editor.getData = ( rootName ) => {
+				expect( rootName ).to.equal( 'second' );
+
+				return 'foo';
+			};
+
+			creator.updateEditorElement( 'second' );
+
+			expect( editor.elements.get( 'second' ).innerHTML ).to.equal( 'foo' );
+		} );
+	} );
+
+	describe( 'updateEditorElements', () => {
+		it( 'updates all editor elements', () => {
+			const spy = sinon.stub( creator, 'updateEditorElement' );
+
+			creator.updateEditorElements();
+
+			expect( spy.calledTwice ).to.be.true;
+			expect( spy.calledWith( 'first' ) ).to.be.true;
+			expect( spy.calledWith( 'second' ) ).to.be.true;
+		} );
+	} );
+
+	describe( 'loadDataFromEditorElement', () => {
+		it( 'should pass data to the first element', () => {
+			editor.setData = sinon.spy();
+
+			editor.elements.get( 'first' ).innerHTML = 'foo';
+			creator.loadDataFromEditorElement();
+
+			expect( editor.setData.calledWithExactly( 'foo', 'first' ) ).to.be.true;
+		} );
+
+		it( 'should pass data to the given element', () => {
+			const element = document.createElement( 'div' );
+			element.innerHTML = 'foo';
+
+			editor.elements.set( 'second', element );
+
+			editor.setData = sinon.spy();
+
+			creator.loadDataFromEditorElement( 'second' );
+
+			expect( editor.setData.calledWithExactly( 'foo', 'second' ) ).to.be.true;
+		} );
+	} );
+
+	describe( 'loadDataFromEditorElements', () => {
+		it( 'updates all editor elements', () => {
+			const spy = sinon.stub( creator, 'loadDataFromEditorElement' );
+
+			creator.loadDataFromEditorElements();
+
+			expect( spy.calledTwice ).to.be.true;
+			expect( spy.calledWith( 'first' ) ).to.be.true;
+			expect( spy.calledWith( 'second' ) ).to.be.true;
+		} );
+	} );
+
+	describe( 'getDataFromElement', () => {
+		[ 'textarea', 'template', 'div' ].forEach( ( elementName ) => {
+			it( 'should return the content of a ' + elementName, function() {
+				const data = StandardCreator.getDataFromElement( document.getElementById( 'getData-' + elementName ) );
+				expect( data ).to.equal( '<b>foo</b>' );
+			} );
+		} );
+	} );
+
+	describe( 'setDataInElement', () => {
+		[ 'textarea', 'template', 'div' ].forEach( ( elementName ) => {
+			it( 'should set the content of a ' + elementName, () => {
+				const el = document.createElement( elementName );
+				const expectedData = '<b>foo</b>';
+
+				StandardCreator.setDataInElement( el, expectedData );
+
+				const actualData = StandardCreator.getDataFromElement( el );
+				expect( actualData ).to.equal( actualData );
+			} );
+		} );
+	} );
+
+	describe( '_replaceElement', () => {
+		it( 'should hide the element', () => {
+			const el = editor.elements.get( 'first' );
+
+			creator._replaceElement( el );
+
+			expect( el.style.display ).to.equal( 'none' );
+		} );
+
+		it( 'should inserts the replacement next to the element being hidden', () => {
+			const el = editor.elements.get( 'first' );
+			const replacement = document.createElement( 'div' );
+
+			creator._replaceElement( el, replacement );
+
+			expect( el.nextSibling ).to.equal( replacement );
+		} );
+	} );
+
+	describe( '_restoreElements', () => {
+		it( 'should restore all elements', () => {
+			const el1 = editor.elements.get( 'first' );
+			const replacement1 = document.createElement( 'div' );
+			const el2 = editor.elements.get( 'second' );
+			const replacement2 = document.createElement( 'div' );
+
+			creator._replaceElement( el1, replacement1 );
+			creator._replaceElement( el2, replacement2 );
+
+			creator._restoreElements();
+
+			expect( replacement1.parentNode ).to.be.null;
+			expect( replacement2.parentNode ).to.be.null;
+			expect( el2.style.display ).to.not.equal( 'none' );
+		} );
+
+		it( 'should not try to remove replacement elements', () => {
+			const el1 = editor.elements.get( 'first' );
+			const el2 = editor.elements.get( 'second' );
+
+			creator._replaceElement( el1 );
+			creator._replaceElement( el2 );
+
+			creator._restoreElements();
+
+			expect( el1.style.display ).to.not.equal( 'none' );
+			expect( el2.style.display ).to.not.equal( 'none' );
+		} );
+	} );
+} );

+ 126 - 0
tests/editable.js

@@ -0,0 +1,126 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import Editor from '/ckeditor5/editor.js';
+import Editable from '/ckeditor5/editable.js';
+import EditingController from '/ckeditor5/engine/treecontroller/editingcontroller.js';
+import ViewElement from '/ckeditor5/engine/treeview/element.js';
+
+describe( 'Editable', () => {
+	const ELEMENT_NAME = 'h1';
+	const EDITABLE_NAME = 'editableNaNaNaNa';
+
+	let editable, editor;
+
+	beforeEach( () => {
+		editor = new Editor();
+		editable = new Editable( editor, EDITABLE_NAME );
+	} );
+
+	describe( 'constructor', () => {
+		it( 'sets the properties', () => {
+			expect( editable ).to.have.property( 'editor', editor );
+			expect( editable ).to.have.property( 'name', EDITABLE_NAME );
+			expect( editable ).to.have.property( 'isEditable', true );
+			expect( editable ).to.have.property( 'isFocused', false );
+		} );
+	} );
+
+	describe( 'isEditable', () => {
+		it( 'is observable', () => {
+			const spy = sinon.spy();
+
+			editable.on( 'change:isEditable', spy );
+
+			editable.isEditable = false;
+
+			expect( spy.calledOnce ).to.be.true;
+		} );
+	} );
+
+	describe( 'isFocused', () => {
+		it( 'is observable', () => {
+			const spy = sinon.spy();
+
+			editable.on( 'change:isFocused', spy );
+
+			editable.isFocused = true;
+
+			expect( spy.calledOnce ).to.be.true;
+		} );
+	} );
+
+	describe( 'bindTo', () => {
+		let domElement, editingView;
+
+		beforeEach( () => {
+			domElement = document.createElement( ELEMENT_NAME );
+
+			editor.editing = new EditingController();
+			editingView = editor.editing.view;
+
+			editable.bindTo( domElement );
+		} );
+
+		it( 'creates view root element', () => {
+			expect( editable.viewElement ).to.be.instanceof( ViewElement );
+			expect( editable.viewElement ).to.have.property( 'name', ELEMENT_NAME );
+
+			expect( editingView.viewRoots.get( EDITABLE_NAME ) ).to.equal( editable.viewElement );
+		} );
+
+		describe( 'isFocused binding', () => {
+			it( 'reacts on editingView#focus', () => {
+				editingView.fire( 'focus', {
+					target: editable.viewElement
+				} );
+
+				expect( editable ).to.have.property( 'isFocused', true );
+			} );
+
+			it( 'reacts on editingView#blur', () => {
+				editable.isFocused = true;
+
+				editingView.fire( 'blur', {
+					target: editable.viewElement
+				} );
+
+				expect( editable ).to.have.property( 'isFocused', false );
+			} );
+
+			it( 'reacts on editingView#focus only if target matches', () => {
+				editingView.fire( 'focus', {
+					target: new ViewElement( 'foo' )
+				} );
+
+				expect( editable ).to.have.property( 'isFocused', false );
+			} );
+
+			it( 'reacts on editingView#blur only if target matches', () => {
+				editable.isFocused = true;
+
+				editingView.fire( 'blur', {
+					target: new ViewElement( 'foo' )
+				} );
+
+				expect( editable ).to.have.property( 'isFocused', true );
+			} );
+		} );
+	} );
+
+	describe( 'destroy', () => {
+		it( 'offs everything', () => {
+			const spy = sinon.spy( editable, 'stopListening' );
+
+			editable.destroy();
+
+			expect( spy.calledOnce ).to.be.true;
+			expect( editable.viewElement ).to.be.null;
+			expect( editable.domElement ).to.be.null;
+		} );
+	} );
+} );

+ 104 - 0
tests/editablecollection.js

@@ -0,0 +1,104 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import Editor from '/ckeditor5/editor.js';
+import Editable from '/ckeditor5/editable.js';
+import EditableCollection from '/ckeditor5/editablecollection.js';
+
+describe( 'EditableCollection', () => {
+	let collection, editor;
+
+	beforeEach( () => {
+		collection = new EditableCollection();
+		editor = new Editor();
+	} );
+
+	describe( 'constructor', () => {
+		it( 'configures collection to use idProperty=name', () => {
+			collection.add( new Editable( editor, 'foo' ) );
+
+			expect( collection.get( 'foo' ).name ).to.equal( 'foo' );
+		} );
+
+		it( 'sets observable property current', () => {
+			expect( collection ).to.have.property( 'current', null );
+
+			const spy = sinon.spy();
+			collection.on( 'change:current', spy );
+
+			collection.current = 1;
+
+			expect( spy.calledOnce ).to.be.true;
+		} );
+	} );
+
+	describe( 'add', () => {
+		it( 'binds collection.current to editable.isFocused changes', () => {
+			const editable = new Editable( editor, 'foo' );
+
+			collection.add( editable );
+
+			editable.isFocused = true;
+			expect( collection ).to.have.property( 'current', editable );
+
+			editable.isFocused = false;
+			expect( collection ).to.have.property( 'current', null );
+		} );
+	} );
+
+	describe( 'remove', () => {
+		it( 'stops watching editable.isFocused', () => {
+			const editable = new Editable( editor, 'foo' );
+
+			collection.add( editable );
+
+			editable.isFocused = true;
+
+			collection.remove( editable );
+
+			editable.isFocused = false;
+
+			expect( collection ).to.have.property( 'current', editable );
+		} );
+	} );
+
+	describe( 'destroy', () => {
+		let editables;
+
+		beforeEach( () => {
+			editables = [ new Editable( editor, 'foo' ), new Editable( editor, 'bar' ) ];
+
+			collection.add( editables[ 0 ] );
+			collection.add( editables[ 1 ] );
+		} );
+
+		it( 'stops watching all editables', () => {
+			collection.destroy();
+
+			editables[ 0 ].isFocused = true;
+			editables[ 1 ].isFocused = true;
+
+			expect( collection ).to.have.property( 'current', null );
+		} );
+
+		it( 'destroys all children', () => {
+			editables.forEach( editable => {
+				editable.destroy = sinon.spy();
+			} );
+
+			collection.destroy();
+
+			expect( editables.map( editable => editable.destroy.calledOnce ) ).to.deep.equal( [ true, true ] );
+		} );
+
+		it( 'removes all children', () => {
+			collection.destroy();
+
+			expect( collection ).to.have.lengthOf( 0 );
+		} );
+	} );
+} );

+ 131 - 163
tests/editor/creator.js

@@ -10,73 +10,57 @@
 import moduleUtils from '/tests/ckeditor5/_utils/module.js';
 import testUtils from '/tests/ckeditor5/_utils/utils.js';
 import Editor from '/ckeditor5/editor.js';
-import Creator from '/ckeditor5/creator.js';
+import Creator from '/ckeditor5/creator/creator.js';
 import CKEditorError from '/ckeditor5/utils/ckeditorerror.js';
 
-let editor, element;
+let editor;
 
 function initEditor( config ) {
-	element = document.createElement( 'div' );
-	document.body.appendChild( element );
-
-	editor = new Editor( element, config );
+	editor = new Editor( null, config );
 
 	return editor.init();
 }
 
 testUtils.createSinonSandbox();
 
-before( () => {
-	testUtils.defineEditorCreatorMock( 'test1', {
-		create: sinon.spy(),
-		destroy: sinon.spy()
-	} );
-
-	testUtils.defineEditorCreatorMock( 'test-config1', {
-		create: sinon.spy()
-	} );
-	testUtils.defineEditorCreatorMock( 'test-config2', {
-		create: sinon.spy()
-	} );
-
-	moduleUtils.define( 'test3/test3', [ 'plugin' ], ( Plugin ) => {
-		return class extends Plugin {};
-	} );
+testUtils.defineEditorCreatorMock( 'test1', {
+	create: sinon.spy(),
+	destroy: sinon.spy()
+} );
 
-	moduleUtils.define( 'test/creator-async-create', [ 'creator' ], ( Creator ) => {
-		return class extends Creator {
-			create() {
-				return new Promise( ( resolve, reject ) => {
-					reject( new Error( 'Catch me - create.' ) );
-				} );
-			}
+testUtils.defineEditorCreatorMock( 'test-config1', {
+	create: sinon.spy()
+} );
+testUtils.defineEditorCreatorMock( 'test-config2', {
+	create: sinon.spy()
+} );
 
-			destroy() {}
-		};
-	} );
+moduleUtils.define( 'test3/test3', [ 'plugin' ], ( Plugin ) => {
+	return class extends Plugin {};
+} );
 
-	moduleUtils.define( 'test/creator-async-destroy', [ 'creator' ], ( Creator ) => {
-		return class extends Creator {
-			create() {}
+moduleUtils.define( 'test/creator-async-create', [ 'creator/creator' ], ( Creator ) => {
+	return class extends Creator {
+		create() {
+			return new Promise( ( resolve, reject ) => {
+				reject( new Error( 'Catch me - create.' ) );
+			} );
+		}
 
-			destroy() {
-				return new Promise( ( resolve, reject ) => {
-					reject( new Error( 'Catch me - destroy.' ) );
-				} );
-			}
-		};
-	} );
+		destroy() {}
+	};
+} );
 
-	moduleUtils.define( 'test/creator-destroy-order', [ 'creator' ], ( Creator ) => {
-		return class extends Creator {
-			create() {}
+moduleUtils.define( 'test/creator-async-destroy', [ 'creator/creator' ], ( Creator ) => {
+	return class extends Creator {
+		create() {}
 
-			destroy() {
-				editor._elementInsideCreatorDestroy = this.editor.element;
-				editor._destroyOrder.push( 'creator' );
-			}
-		};
-	} );
+		destroy() {
+			return new Promise( ( resolve, reject ) => {
+				reject( new Error( 'Catch me - destroy.' ) );
+			} );
+		}
+	};
 } );
 
 afterEach( () => {
@@ -85,124 +69,108 @@ afterEach( () => {
 
 ///////////////////
 
-describe( 'init', () => {
-	it( 'should instantiate the creator and call create()', () => {
-		return initEditor( {
-				creator: 'creator-test1'
-			} )
-			.then( () => {
-				let creator = editor.plugins.get( 'creator-test1' );
-
-				expect( creator ).to.be.instanceof( Creator );
+describe( 'Editor creator', () => {
+	describe( 'init', () => {
+		it( 'should instantiate the creator and call create()', () => {
+			return initEditor( {
+					creator: 'creator-test1'
+				} )
+				.then( () => {
+					let creator = editor.plugins.get( 'creator-test1' );
 
-				// The create method has been called.
-				sinon.assert.calledOnce( creator.create );
-			} );
-	} );
+					expect( creator ).to.be.instanceof( Creator );
 
-	it( 'should throw if creator is not defined', () => {
-		return initEditor( {} )
-			.then( () => {
-				throw new Error( 'This should not be executed.' );
-			} )
-			.catch( ( err ) => {
-				expect( err ).to.be.instanceof( CKEditorError );
-				expect( err.message ).to.match( /^editor-undefined-creator:/ );
-			} );
-	} );
-
-	it( 'should use the creator specified in config.creator', () => {
-		return initEditor( {
-				creator: 'creator-test-config2',
-				features: [ 'creator-test-config1', 'creator-test-config2' ],
-			} )
-			.then( () => {
-				let creator1 = editor.plugins.get( 'creator-test-config1' );
-				let creator2 = editor.plugins.get( 'creator-test-config2' );
-
-				sinon.assert.calledOnce( creator2.create );
-				sinon.assert.notCalled( creator1.create );
-			} );
-	} );
-
-	it( 'should throw an error if the creator doesn\'t exist', () => {
-		return initEditor( {
-				creator: 'bad'
-			} )
-			.then( () => {
-				throw new Error( 'This should not be executed.' );
-			} )
-			.catch( ( err ) => {
-				// It's the Require.JS error.
-				expect( err ).to.be.an.instanceof( Error );
-				expect( err.message ).to.match( /^Script error for/ );
-			} );
-	} );
-
-	it( 'should chain the promise from the creator (enables async creators)', () => {
-		return initEditor( {
-				creator: 'test/creator-async-create'
-			} )
-			.then( () => {
-				throw new Error( 'This should not be executed.' );
-			} )
-			.catch( ( err ) => {
-				// Unfortunately fake timers don't work with promises, so throwing in the creator's create()
-				// seems to be the only way to test that the promise chain isn't broken.
-				expect( err ).to.have.property( 'message', 'Catch me - create.' );
-			} );
+					// The create method has been called.
+					sinon.assert.calledOnce( creator.create );
+				} );
+		} );
+
+		it( 'should throw if creator is not defined', () => {
+			return initEditor( {} )
+				.then( () => {
+					throw new Error( 'This should not be executed.' );
+				} )
+				.catch( ( err ) => {
+					expect( err ).to.be.instanceof( CKEditorError );
+					expect( err.message ).to.match( /^editor-undefined-creator:/ );
+				} );
+		} );
+
+		it( 'should use the creator specified in config.creator', () => {
+			return initEditor( {
+					creator: 'creator-test-config2',
+					features: [ 'creator-test-config1', 'creator-test-config2' ],
+				} )
+				.then( () => {
+					let creator1 = editor.plugins.get( 'creator-test-config1' );
+					let creator2 = editor.plugins.get( 'creator-test-config2' );
+
+					sinon.assert.calledOnce( creator2.create );
+					sinon.assert.notCalled( creator1.create );
+				} );
+		} );
+
+		it( 'should throw an error if the creator doesn\'t exist', () => {
+			return initEditor( {
+					creator: 'bad'
+				} )
+				.then( () => {
+					throw new Error( 'This should not be executed.' );
+				} )
+				.catch( ( err ) => {
+					// It's the Require.JS error.
+					expect( err ).to.be.an.instanceof( Error );
+					expect( err.message ).to.match( /^Script error for/ );
+				} );
+		} );
+
+		it( 'should chain the promise from the creator (enables async creators)', () => {
+			return initEditor( {
+					creator: 'test/creator-async-create'
+				} )
+				.then( () => {
+					throw new Error( 'This should not be executed.' );
+				} )
+				.catch( ( err ) => {
+					// Unfortunately fake timers don't work with promises, so throwing in the creator's create()
+					// seems to be the only way to test that the promise chain isn't broken.
+					expect( err ).to.have.property( 'message', 'Catch me - create.' );
+				} );
+		} );
 	} );
-} );
-
-describe( 'destroy', () => {
-	it( 'should call "destroy" on the creator', () => {
-		let creator1;
 
-		return initEditor( {
-				creator: 'creator-test1'
-			} )
-			.then( () => {
-				creator1 = editor.plugins.get( 'creator-test1' );
+	describe( 'destroy', () => {
+		it( 'should call "destroy" on the creator', () => {
+			let creator1;
 
-				return editor.destroy();
-			} )
-			.then( () => {
-				sinon.assert.calledOnce( creator1.destroy );
-			} );
-	} );
+			return initEditor( {
+					creator: 'creator-test1'
+				} )
+				.then( () => {
+					creator1 = editor.plugins.get( 'creator-test1' );
 
-	it( 'should chain the promise from the creator (enables async creators)', () => {
-		return initEditor( {
-				creator: 'test/creator-async-destroy'
-			} )
-			.then( () => {
-				return editor.destroy();
-			} )
-			.then( () => {
-				throw new Error( 'This should not be executed.' );
-			} )
-			.catch( ( err ) => {
-				// Unfortunately fake timers don't work with promises, so throwing in the creator's destroy()
-				// seems to be the only way to test that the promise chain isn't broken.
-				expect( err ).to.have.property( 'message', 'Catch me - destroy.' );
-			} );
-	} );
-
-	it( 'should do things in the correct order', () => {
-		return initEditor( {
-				creator: 'test/creator-destroy-order'
-			} )
-			.then( () => {
-				editor._destroyOrder = [];
-				editor.on( 'destroy', () => {
-					editor._destroyOrder.push( 'event' );
+					return editor.destroy();
+				} )
+				.then( () => {
+					sinon.assert.calledOnce( creator1.destroy );
 				} );
-
-				return editor.destroy();
-			} )
-			.then( () => {
-				expect( editor._elementInsideCreatorDestroy ).to.not.be.undefined;
-				expect( editor._destroyOrder ).to.deep.equal( [ 'event', 'creator' ] );
-			} );
+		} );
+
+		it( 'should chain the promise from the creator (enables async creators)', () => {
+			return initEditor( {
+					creator: 'test/creator-async-destroy'
+				} )
+				.then( () => {
+					return editor.destroy();
+				} )
+				.then( () => {
+					throw new Error( 'This should not be executed.' );
+				} )
+				.catch( ( err ) => {
+					// Unfortunately fake timers don't work with promises, so throwing in the creator's destroy()
+					// seems to be the only way to test that the promise chain isn't broken.
+					expect( err ).to.have.property( 'message', 'Catch me - destroy.' );
+				} );
+		} );
 	} );
 } );

+ 251 - 147
tests/editor/editor.js

@@ -11,13 +11,15 @@ import moduleUtils from '/tests/ckeditor5/_utils/module.js';
 import testUtils from '/tests/ckeditor5/_utils/utils.js';
 import Editor from '/ckeditor5/editor.js';
 import EditorConfig from '/ckeditor5/editorconfig.js';
+import PluginCollection from '/ckeditor5/plugincollection.js';
+import EditableCollection from '/ckeditor5/editablecollection.js';
 import Plugin from '/ckeditor5/plugin.js';
 import Command from '/ckeditor5/command/command.js';
 import Locale from '/ckeditor5/utils/locale.js';
 import CKEditorError from '/ckeditor5/utils/ckeditorerror.js';
+import Document from '/ckeditor5/engine/treemodel/document.js';
 
 const pluginClasses = {};
-let element;
 
 before( () => {
 	// Define fake plugins to be used in tests.
@@ -31,218 +33,320 @@ before( () => {
 	pluginDefinition( 'D/D', [ 'C/C' ] );
 } );
 
-beforeEach( () => {
-	element = document.createElement( 'div' );
-	document.body.appendChild( element );
-} );
-
 ///////////////////
 
-describe( 'constructor', () => {
-	it( 'should create a new editor instance', () => {
-		const editor = new Editor( element );
+describe( 'Editor', () => {
+	describe( 'constructor', () => {
+		it( 'should create a new editor instance', () => {
+			const editor = new Editor();
+
+			expect( editor ).to.have.property( 'elements', null );
+			expect( editor.config ).to.be.an.instanceof( EditorConfig );
+			expect( editor.editables ).to.be.an.instanceof( EditableCollection );
+			expect( editor.commands ).to.be.an.instanceof( Map );
 
-		expect( editor ).to.have.property( 'element' ).to.equal( element );
+			expect( editor.plugins ).to.be.an.instanceof( PluginCollection );
+			expect( getPlugins( editor ) ).to.be.empty;
+		} );
 	} );
-} );
 
-describe( 'config', () => {
-	it( 'should be an instance of EditorConfig', () => {
-		const editor = new Editor( element );
+	describe( 'config', () => {
+		it( 'should be an instance of EditorConfig', () => {
+			const editor = new Editor();
 
-		expect( editor.config ).to.be.an.instanceof( EditorConfig );
+			expect( editor.config ).to.be.an.instanceof( EditorConfig );
+		} );
 	} );
-} );
 
-describe( 'locale', () => {
-	it( 'is instantiated and t() is exposed', () => {
-		const editor = new Editor( element );
+	describe( 'locale', () => {
+		it( 'is instantiated and t() is exposed', () => {
+			const editor = new Editor();
 
-		expect( editor.locale ).to.be.instanceof( Locale );
-		expect( editor.t ).to.equal( editor.locale.t );
-	} );
+			expect( editor.locale ).to.be.instanceof( Locale );
+			expect( editor.t ).to.equal( editor.locale.t );
+		} );
 
-	it( 'is configured with the config.lang', () => {
-		const editor = new Editor( element, { lang: 'pl' } );
+		it( 'is configured with the config.lang', () => {
+			const editor = new Editor( null, { lang: 'pl' } );
 
-		expect( editor.locale.lang ).to.equal( 'pl' );
+			expect( editor.locale.lang ).to.equal( 'pl' );
+		} );
 	} );
-} );
 
-describe( 'init', () => {
-	it( 'should return a promise that resolves properly', () => {
-		const editor = new Editor( element, {
-			creator: 'creator-test'
+	describe( 'plugins', () => {
+		it( 'should be empty on new editor', () => {
+			const editor = new Editor();
+
+			expect( getPlugins( editor ) ).to.be.empty;
 		} );
+	} );
 
-		let promise = editor.init();
+	describe( 'firstElement', () => {
+		it( 'should be set to first element', () => {
+			const editor = new Editor( { foo: 'a', bar: 'b' } );
 
-		expect( promise ).to.be.an.instanceof( Promise );
+			expect( editor.firstElement ).to.equal( 'a' );
+		} );
 
-		return promise;
-	} );
+		it( 'should be set to null if there are no elements', () => {
+			const editor = new Editor();
 
-	it( 'should load features and creator', () => {
-		const editor = new Editor( element, {
-			features: [ 'A', 'B' ],
-			creator: 'creator-test'
+			expect( editor.firstElement ).to.be.null;
 		} );
+	} );
+
+	describe( 'firstElementName', () => {
+		it( 'should be set to first element name', () => {
+			const editor = new Editor( { foo: 'a', bar: 'b' } );
 
-		expect( getPlugins( editor ) ).to.be.empty;
+			expect( editor.firstElementName ).to.equal( 'foo' );
+		} );
 
-		return editor.init().then( () => {
-			expect( getPlugins( editor ).length ).to.equal( 3 );
+		it( 'should be set to null if there are no elements', () => {
+			const editor = new Editor();
 
-			expect( editor.plugins.get( 'A' ) ).to.be.an.instanceof( Plugin );
-			expect( editor.plugins.get( 'B' ) ).to.be.an.instanceof( Plugin );
-			expect( editor.plugins.get( 'creator-test' ) ).to.be.an.instanceof( Plugin );
+			expect( editor.firstElementName ).to.be.null;
 		} );
 	} );
 
-	it( 'should load features passed as a string', () => {
-		const editor = new Editor( element, {
-			features: 'A,B',
-			creator: 'creator-test'
-		} );
+	describe( 'init', () => {
+		it( 'should return a promise that resolves properly', () => {
+			const editor = new Editor( null, {
+				creator: 'creator-test'
+			} );
 
-		expect( getPlugins( editor ) ).to.be.empty;
+			let promise = editor.init();
 
-		return editor.init().then( () => {
-			expect( getPlugins( editor ).length ).to.equal( 3 );
+			expect( promise ).to.be.an.instanceof( Promise );
 
-			expect( editor.plugins.get( 'A' ) ).to.be.an.instanceof( Plugin );
-			expect( editor.plugins.get( 'B' ) ).to.be.an.instanceof( Plugin );
+			return promise;
 		} );
-	} );
 
-	it( 'should initialize plugins in the right order', () => {
-		const editor = new Editor( element, {
-			features: [ 'A', 'D' ],
-			creator: 'creator-test'
+		it( 'should load features and creator', () => {
+			const editor = new Editor( null, {
+				features: [ 'A', 'B' ],
+				creator: 'creator-test'
+			} );
+
+			expect( getPlugins( editor ) ).to.be.empty;
+
+			return editor.init().then( () => {
+				expect( getPlugins( editor ).length ).to.equal( 3 );
+
+				expect( editor.plugins.get( 'A' ) ).to.be.an.instanceof( Plugin );
+				expect( editor.plugins.get( 'B' ) ).to.be.an.instanceof( Plugin );
+				expect( editor.plugins.get( 'creator-test' ) ).to.be.an.instanceof( Plugin );
+			} );
 		} );
 
-		return editor.init().then( () => {
-			sinon.assert.callOrder(
-				editor.plugins.get( 'creator-test' ).init,
-				editor.plugins.get( pluginClasses[ 'A/A' ] ).init,
-				editor.plugins.get( pluginClasses[ 'B/B' ] ).init,
-				editor.plugins.get( pluginClasses[ 'C/C' ] ).init,
-				editor.plugins.get( pluginClasses[ 'D/D' ] ).init
-			);
+		it( 'should load features passed as a string', () => {
+			const editor = new Editor( null, {
+				features: 'A,B',
+				creator: 'creator-test'
+			} );
+
+			expect( getPlugins( editor ) ).to.be.empty;
+
+			return editor.init().then( () => {
+				expect( getPlugins( editor ).length ).to.equal( 3 );
+
+				expect( editor.plugins.get( 'A' ) ).to.be.an.instanceof( Plugin );
+				expect( editor.plugins.get( 'B' ) ).to.be.an.instanceof( Plugin );
+			} );
 		} );
-	} );
 
-	it( 'should initialize plugins in the right order, waiting for asynchronous ones', () => {
-		class PluginAsync extends Plugin {}
-		const asyncSpy = sinon.spy().named( 'async-call-spy' );
+		it( 'should initialize plugins in the right order', () => {
+			const editor = new Editor( null, {
+				features: [ 'A', 'D' ],
+				creator: 'creator-test'
+			} );
 
-		// Synchronous plugin that depends on an asynchronous one.
-		pluginDefinition( 'sync/sync', [ 'async/async' ] );
+			return editor.init().then( () => {
+				sinon.assert.callOrder(
+					editor.plugins.get( 'creator-test' ).init,
+					editor.plugins.get( pluginClasses[ 'A/A' ] ).init,
+					editor.plugins.get( pluginClasses[ 'B/B' ] ).init,
+					editor.plugins.get( pluginClasses[ 'C/C' ] ).init,
+					editor.plugins.get( pluginClasses[ 'D/D' ] ).init
+				);
+			} );
+		} );
 
-		moduleUtils.define( 'async/async', () => {
-			PluginAsync.prototype.init = sinon.spy( () => {
-				return new Promise( ( resolve ) => {
-					setTimeout( () => {
-						asyncSpy();
-						resolve();
-					}, 0 );
+		it( 'should initialize plugins in the right order, waiting for asynchronous ones', () => {
+			class PluginAsync extends Plugin {}
+			const asyncSpy = sinon.spy().named( 'async-call-spy' );
+
+			// Synchronous plugin that depends on an asynchronous one.
+			pluginDefinition( 'sync/sync', [ 'async/async' ] );
+
+			moduleUtils.define( 'async/async', () => {
+				PluginAsync.prototype.init = sinon.spy( () => {
+					return new Promise( ( resolve ) => {
+						setTimeout( () => {
+							asyncSpy();
+							resolve();
+						}, 0 );
+					} );
 				} );
+
+				return PluginAsync;
 			} );
 
-			return PluginAsync;
-		} );
+			const editor = new Editor( null, {
+				features: [ 'A', 'sync' ],
+				creator: 'creator-test'
+			} );
 
-		const editor = new Editor( element, {
-			features: [ 'A', 'sync' ],
-			creator: 'creator-test'
+			return editor.init().then( () => {
+				sinon.assert.callOrder(
+					editor.plugins.get( 'creator-test' ).init,
+					editor.plugins.get( pluginClasses[ 'A/A' ] ).init,
+					editor.plugins.get( PluginAsync ).init,
+					// This one is called with delay by the async init.
+					asyncSpy,
+					editor.plugins.get( pluginClasses[ 'sync/sync' ] ).init
+				);
+			} );
 		} );
+	} );
+
+	describe( 'destroy', () => {
+		it( 'should fire "destroy"', () => {
+			const editor = new Editor();
+			let spy = sinon.spy();
 
-		return editor.init().then( () => {
-			sinon.assert.callOrder(
-				editor.plugins.get( 'creator-test' ).init,
-				editor.plugins.get( pluginClasses[ 'A/A' ] ).init,
-				editor.plugins.get( PluginAsync ).init,
-				// This one is called with delay by the async init.
-				asyncSpy,
-				editor.plugins.get( pluginClasses[ 'sync/sync' ] ).init
-			);
+			editor.on( 'destroy', spy );
+
+			return editor.destroy().then( () => {
+				expect( spy.calledOnce ).to.be.true;
+			} );
 		} );
+
+		// Note: Tests for destroying creators are in creator/creator.js.
+		// When destroying creator will be generalized to destroying plugins,
+		// move that code here.
 	} );
-} );
 
-describe( 'plugins', () => {
-	it( 'should be empty on new editor', () => {
-		const editor = new Editor( element );
+	describe( 'execute', () => {
+		it( 'should execute specified command', () => {
+			const editor = new Editor();
 
-		expect( getPlugins( editor ) ).to.be.empty;
-	} );
-} );
+			let command = new Command( editor );
+			sinon.spy( command, '_execute' );
 
-describe( 'destroy', () => {
-	it( 'should fire "destroy"', () => {
-		const editor = new Editor( element );
-		let spy = sinon.spy();
+			editor.commands.set( 'commandName', command );
+			editor.execute( 'commandName' );
 
-		editor.on( 'destroy', spy );
+			expect( command._execute.calledOnce ).to.be.true;
+		} );
 
-		return editor.destroy().then( () => {
-			sinon.assert.called( spy );
+		it( 'should throw an error if specified command has not been added', () => {
+			const editor = new Editor();
+
+			expect( () => {
+				editor.execute( 'command' );
+			} ).to.throw( CKEditorError, /^editor-command-not-found:/ );
 		} );
 	} );
 
-	it( 'should delete the "element" property', () => {
-		const editor = new Editor( element );
+	describe( 'setData', () => {
+		let editor;
+
+		beforeEach( () => {
+			editor = new Editor();
 
-		return editor.destroy().then( () => {
-			expect( editor ).to.not.have.property( 'element' );
+			editor.document = new Document();
+			editor.data = {
+				set: sinon.spy()
+			};
 		} );
-	} );
-} );
 
-describe( 'execute', () => {
-	it( 'should execute specified command', () => {
-		const editor = new Editor( element );
+		it( 'should set data of the first root', () => {
+			editor.document.createRoot( 'firstRoot', 'div' );
 
-		let command = new Command( editor );
-		sinon.spy( command, '_execute' );
+			editor.setData( 'foo' );
 
-		editor.commands.set( 'command_name', command );
-		editor.execute( 'command_name' );
+			expect( editor.data.set.calledOnce ).to.be.true;
+			expect( editor.data.set.calledWithExactly( 'firstRoot', 'foo' ) ).to.be.true;
+		} );
 
-		expect( command._execute.calledOnce ).to.be.true;
-	} );
+		it( 'should set data of the specified root', () => {
+			editor.setData( 'foo', 'someRoot' );
 
-	it( 'should throw an error if specified command has not been added', () => {
-		const editor = new Editor( element );
+			expect( editor.data.set.calledOnce ).to.be.true;
+			expect( editor.data.set.calledWithExactly( 'someRoot', 'foo' ) ).to.be.true;
+		} );
 
-		expect( () => {
-			editor.execute( 'command' );
-		} ).to.throw( CKEditorError, /editor-command-not-found/ );
-	} );
-} );
+		it( 'should throw when no roots', () => {
+			expect( () => {
+				editor.setData( 'foo' );
+			} ).to.throw( CKEditorError, /^editor-no-editable-roots:/ );
+		} );
+
+		it( 'should throw when more than one root and no root name given', () => {
+			editor.document.createRoot( 'firstRoot', 'div' );
+			editor.document.createRoot( 'secondRoot', 'div' );
 
-describe( 'setData', () => {
-	it( 'should set data on the editable', () => {
-		const editor = new Editor( element );
-		editor.editable = {
-			setData: sinon.spy()
-		};
+			expect( () => {
+				editor.setData( 'foo' );
+			} ).to.throw( CKEditorError, /^editor-editable-root-name-missing:/ );
+		} );
 
-		editor.setData( 'foo' );
+		it( 'should throw when no data controller', () => {
+			expect( () => {
+				editor.data = null;
 
-		expect( editor.editable.setData.calledOnce ).to.be.true;
-		expect( editor.editable.setData.args[ 0 ][ 0 ] ).to.equal( 'foo' );
+				editor.setData( 'foo' );
+			} ).to.throw( CKEditorError, /^editor-no-datacontroller:/ );
+		} );
 	} );
 
-	it( 'should get data from the editable', () => {
-		const editor = new Editor( element );
-		editor.editable = {
-			getData() {
-				return 'bar';
-			}
-		};
+	describe( 'getData', () => {
+		let editor;
+
+		beforeEach( () => {
+			editor = new Editor();
+
+			editor.document = new Document();
+			editor.data = {
+				get( rootName ) {
+					return `data for ${ rootName }`;
+				}
+			};
+		} );
+
+		it( 'should get data from the first root', () => {
+			editor.document.createRoot( 'firstRoot', 'div' );
+
+			expect( editor.getData() ).to.equal( 'data for firstRoot' );
+		} );
 
-		expect( editor.getData() ).to.equal( 'bar' );
+		it( 'should get data from the specified root', () => {
+			expect( editor.getData( 'someRoot' ) ).to.equal( 'data for someRoot' );
+		} );
+
+		it( 'should throw when no roots', () => {
+			expect( () => {
+				editor.getData();
+			} ).to.throw( CKEditorError, /^editor-no-editable-roots:/ );
+		} );
+
+		it( 'should throw when more than one root and no root name given', () => {
+			editor.document.createRoot( 'firstRoot', 'div' );
+			editor.document.createRoot( 'secondRoot', 'div' );
+
+			expect( () => {
+				editor.getData();
+			} ).to.throw( CKEditorError, /^editor-editable-root-name-missing:/ );
+		} );
+
+		it( 'should throw when no data controller', () => {
+			expect( () => {
+				editor.data = null;
+
+				editor.getData();
+			} ).to.throw( CKEditorError, /^editor-no-datacontroller:/ );
+		} );
 	} );
 } );
 

+ 1 - 1
tests/plugincollection.js

@@ -10,7 +10,7 @@ import testUtils from '/tests/ckeditor5/_utils/utils.js';
 import Editor from '/ckeditor5/editor.js';
 import PluginCollection from '/ckeditor5/plugincollection.js';
 import Plugin from '/ckeditor5/plugin.js';
-import Creator from '/ckeditor5/creator.js';
+import Creator from '/ckeditor5/creator/creator.js';
 import CKEditorError from '/ckeditor5/utils/ckeditorerror.js';
 import log from '/ckeditor5/utils/log.js';