Преглед на файлове

Moved app files from ckeditor5-core.

Piotrek Koszuliński преди 9 години
родител
ревизия
2220fb6246
променени са 37 файла, в които са добавени 2926 реда и са изтрити 4 реда
  1. 82 0
      src/ckeditor.js
  2. 186 0
      src/command/attributecommand.js
  3. 137 0
      src/command/command.js
  4. 141 0
      src/creator.js
  5. 223 0
      src/editor.js
  6. 35 0
      src/editorconfig.js
  7. 22 0
      src/feature.js
  8. 61 0
      src/plugin.js
  9. 175 0
      src/plugincollection.js
  10. 38 0
      tests/_utils/ui/boxededitorui/boxededitorui.js
  11. 41 0
      tests/_utils/ui/boxededitorui/boxededitoruiview.js
  12. 35 0
      tests/_utils/ui/boxlesseditorui/boxlesseditorui.js
  13. 16 0
      tests/_utils/ui/editable/framed/framededitable.js
  14. 55 0
      tests/_utils/ui/editable/framed/framededitableview.js
  15. 11 0
      tests/_utils/ui/editable/inline/inlineeditable.js
  16. 28 0
      tests/_utils/ui/editable/inline/inlineeditableview.js
  17. 16 0
      tests/_utils/ui/floatingtoolbar/floatingtoolbar.js
  18. 27 0
      tests/_utils/ui/floatingtoolbar/floatingtoolbarview.js
  19. 2 0
      tests/ckeditor.html
  20. 95 4
      tests/ckeditor.js
  21. 3 0
      tests/creator/creator.html
  22. 208 0
      tests/creator/creator.js
  23. 55 0
      tests/creator/manual/_assets/styles.css
  24. 77 0
      tests/creator/manual/_utils/creator/classiccreator.js
  25. 81 0
      tests/creator/manual/_utils/creator/inlinecreator.js
  26. 57 0
      tests/creator/manual/_utils/imitatefeatures.js
  27. 12 0
      tests/creator/manual/creator-classic.html
  28. 50 0
      tests/creator/manual/creator-classic.js
  29. 23 0
      tests/creator/manual/creator-classic.md
  30. 12 0
      tests/creator/manual/creator-inline.html
  31. 46 0
      tests/creator/manual/creator-inline.js
  32. 21 0
      tests/creator/manual/creator-inline.md
  33. 212 0
      tests/editor/creator.js
  34. 280 0
      tests/editor/editor.js
  35. 41 0
      tests/editorconfig.js
  36. 23 0
      tests/plugin.js
  37. 299 0
      tests/plugincollection.js

+ 82 - 0
src/ckeditor.js

@@ -0,0 +1,82 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import Editor from './editor.js';
+import Collection from '../utils/collection.js';
+import Config from '../utils/config.js';
+
+/**
+ * This is the API entry point. The entire CKEditor code runs under this object.
+ *
+ * @namespace CKEDITOR
+ */
+const CKEDITOR = {
+	/**
+	 * A collection containing all editor instances created.
+	 *
+	 * @readonly
+	 * @member {utils.Collection} CKEDITOR.instances
+	 */
+	instances: new Collection(),
+
+	/**
+	 * Creates an editor instance for the provided DOM element.
+	 *
+	 * The creation of editor instances is an asynchronous operation, therefore a promise is returned by this
+	 * method.
+	 *
+	 *		CKEDITOR.create( '#content' );
+	 *
+	 *		CKEDITOR.create( '#content' ).then( ( editor ) => {
+	 *			// Manipulate "editor" here.
+	 *		} );
+	 *
+	 * @method CKEDITOR.create
+	 * @param {String|HTMLElement} element An element selector or a DOM element, which will be the source for the
+	 * created instance.
+	 * @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 );
+
+			this.instances.add( editor );
+
+			// Remove the editor from `instances` when destroyed.
+			editor.once( 'destroy', () => {
+				this.instances.remove( editor );
+			} );
+
+			resolve(
+				// Initializes the editor, which returns a promise.
+				editor.init()
+					.then( () => {
+						// After initialization, return the created editor.
+						return editor;
+					} )
+			);
+		} );
+	},
+
+	/**
+	 * 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;

+ 186 - 0
src/command/attributecommand.js

@@ -0,0 +1,186 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import Command from './command.js';
+import TreeWalker from '../treemodel/treewalker.js';
+import Range from '../treemodel/range.js';
+
+/**
+ * An extension of basic {@link core.command.Command} class, which provides utilities for a command that sets a single
+ * attribute on a text or element with value `true`. AttributeCommand uses {@link core.treeModel.Document#selection} to
+ * decide which nodes (if any) should be changed, and applies or removes attributes from them.
+ * See {@link core.treeView.Converter#execute} for more.
+ *
+ * The command checks {@link core.treeModel.Document#schema} to decide if it should be enabled.
+ * See {@link core.treeView.Converter#checkSchema} for more.
+ *
+ * @memberOf core.command
+ */
+export default class AttributeCommand extends Command {
+	/**
+	 * @see core.command.Command
+	 * @param {core.Editor} editor
+	 * @param {String} attributeKey Attribute that will be set by the command.
+	 */
+	constructor( editor, attributeKey ) {
+		super( editor );
+
+		/**
+		 * Attribute that will be set by the command.
+		 *
+		 * @member {String} core.command.AttributeCommand#attributeKey
+		 */
+		this.attributeKey = attributeKey;
+
+		/**
+		 * Flag indicating whether command is active. For collapsed selection it means that typed characters will have
+		 * the command's attribute set. For range selection it means that all nodes inside have the attribute applied.
+		 *
+		 * @member {Boolean} core.command.AttributeCommand#value
+		 */
+		this.set( 'value', false );
+
+		this.listenTo( this.editor.document.selection, 'change:attribute', () => {
+			this.value = this.editor.document.selection.hasAttribute( this.attributeKey );
+		} );
+	}
+
+	/**
+	 * Checks {@link core.treeModel.Document#schema} to decide if the command should be enabled:
+	 * * if selection is on range, the command is enabled if any of nodes in that range can have bold,
+	 * * if selection is collapsed, the command is enabled if text with bold is allowed in that node.
+	 *
+	 * @private
+	 * @returns {Boolean}
+	 */
+	_checkEnabled() {
+		const selection = this.editor.document.selection;
+		const schema = this.editor.document.schema;
+
+		if ( selection.isCollapsed ) {
+			// Check whether schema allows for a test with `attributeKey` in caret position.
+			return schema.checkAtPosition( selection.getFirstPosition(), '$text', this.attributeKey );
+		} else {
+			const ranges = selection.getRanges();
+
+			// For all ranges, check nodes in them until you find a node that is allowed to have `attributeKey` attribute.
+			for ( let range of ranges ) {
+				const walker = new TreeWalker( { boundaries: range, mergeCharacters: true } );
+				let last = walker.position;
+				let step = walker.next();
+
+				// Walk the range.
+				while ( !step.done ) {
+					// If returned item does not have name property, it is a treeModel.TextFragment.
+					const name = step.value.item.name || '$text';
+
+					if ( schema.checkAtPosition( last, name, this.attributeKey ) ) {
+						// If we found a node that is allowed to have the attribute, return true.
+						return true;
+					}
+
+					last = walker.position;
+					step = walker.next();
+				}
+			}
+		}
+
+		// If we haven't found such node, return false.
+		return false;
+	}
+
+	/**
+	 * Executes the command: adds or removes attributes to nodes or selection.
+	 *
+	 * If the command is active (`value == true`), it will remove attributes. Otherwise, it will set attributes.
+	 *
+	 * The execution result differs, depending on the {@link core.treeModel.Document#selection}:
+	 * * if selection is on a range, the command applies the attribute on all nodes in that ranges
+	 * (if they are allowed to have this attribute by the{@link core.treeModel.Schema schema}),
+	 * * if selection is collapsed in non-empty node, the command applies attribute to the {@link core.treeModel.Document#selection}
+	 * itself (note that typed characters copy attributes from selection),
+	 * * if selection is collapsed in empty node, the command applies attribute to the parent node of selection (note
+	 * that selection inherits all attributes from a node if it is in empty node).
+	 *
+	 * If the command is disabled (`isEnabled == false`) when it is executed, nothing will happen.
+	 *
+	 * @private
+	 * @param {Boolean} [forceValue] If set it will force command behavior. If `true`, command will apply attribute,
+	 * otherwise command will remove attribute. If not set, command will look for it's current value to decide what it should do.
+	 */
+	_doExecute( forceValue ) {
+		const document = this.editor.document;
+		const selection = document.selection;
+		const value = ( forceValue === undefined ) ? !this.value : forceValue;
+
+		if ( selection.isCollapsed ) {
+			if ( value ) {
+				selection.setAttribute( this.attributeKey, true );
+			} else {
+				selection.removeAttribute( this.attributeKey );
+			}
+		} else {
+			// If selection has non-collapsed ranges, we change attribute on nodes inside those ranges.
+			document.enqueueChanges( () => {
+				const ranges = this._getSchemaValidRanges( selection.getRanges() );
+
+				// Keep it as one undo step.
+				const batch = document.batch();
+
+				for ( let range of ranges ) {
+					if ( value ) {
+						batch.setAttr( this.attributeKey, value, range );
+					} else {
+						batch.removeAttr( this.attributeKey, range );
+					}
+				}
+			} );
+		}
+	}
+
+	/**
+	 * Walks through given array of ranges and removes parts of them that are not allowed by schema to have the
+	 * attribute set. This is done by breaking a range in two and omitting the not allowed part.
+	 *
+	 * @private
+	 * @param {Array.<core.treeModel.Range>} ranges Ranges to be validated.
+	 * @returns {Array.<core.treeModel.Range>} Ranges without invalid parts.
+	 */
+	_getSchemaValidRanges( ranges ) {
+		const validRanges = [];
+
+		for ( let range of ranges ) {
+			const walker = new TreeWalker( { boundaries: range, mergeCharacters: true } );
+			let step = walker.next();
+
+			let last = range.start;
+			let from = range.start;
+			let to = range.end;
+
+			while ( !step.done ) {
+				const name = step.value.item.name || '$text';
+
+				if ( !this.editor.document.schema.checkAtPosition( last, name, this.attributeKey ) ) {
+					if ( !from.isEqual( last ) ) {
+						validRanges.push( new Range( from, last ) );
+					}
+
+					from = walker.position;
+				}
+
+				last = walker.position;
+				step = walker.next();
+			}
+
+			if ( from && !from.isEqual( to ) ) {
+				validRanges.push( new Range( from, to ) );
+			}
+		}
+
+		return validRanges;
+	}
+}

+ 137 - 0
src/command/command.js

@@ -0,0 +1,137 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import ObservableMixin from '../../utils/observablemixin.js';
+import utils from '../../utils/utils.js';
+
+/**
+ * The base class for CKEditor commands.
+ *
+ * Commands are main way to manipulate editor contents and state. They are mostly used by UI elements (or by other
+ * commands) to make changes in Tree Model. Commands are available in every part of code that has access to
+ * {@link core.Editor} instance, since they are registered in it and executed through {@link core.Editor#execute}.
+ * Commands instances are available through {@link core.Editor#commands}.
+ *
+ * This is an abstract base class for all commands.
+ *
+ * @memberOf core.command
+ * @mixes utils.ObservableMixin
+ */
+export default class Command {
+	/**
+	 * Creates a new Command instance.
+	 *
+	 * @param {core.Editor} editor Editor on which this command will be used.
+	 */
+	constructor( editor ) {
+		/**
+		 * Editor on which this command will be used.
+		 *
+		 * @member {core.Editor} core.command.Command#editor
+		 */
+		this.editor = editor;
+
+		/**
+		 * Flag indicating whether a command is enabled or disabled.
+		 * A disabled command should do nothing upon it's execution.
+		 *
+		 * @member {Boolean} core.command.Command#isEnabled
+		 */
+		this.set( 'isEnabled', true );
+
+		// If schema checking function is specified, add it to the `refreshState` listeners.
+		// Feature will be disabled if it does not apply to schema requirements.
+		if ( this._checkEnabled ) {
+			this.on( 'refreshState', ( evt, data ) => {
+				data.isEnabled = this._checkEnabled();
+			} );
+		}
+	}
+
+	/**
+	 * Checks if a command should be enabled according to its own rules. Mostly it will check schema to see if the command
+	 * is allowed to be executed in given position. This method can be defined in child class (but is not obligatory).
+	 * If it is defined, it will be added as a callback to `refreshState` event.
+	 *
+	 * @protected
+	 * @method core.command.Command#_checkEnabled
+	 * @returns {Boolean} `true` if command should be enabled according to {@link core.treeModel.Document#schema}. `false` otherwise.
+	 */
+
+	/**
+	 * Fires `refreshState` event and checks it's resolve value to decide whether command should be enabled or not.
+	 * Other parts of code might listen to `refreshState` event on this command and add their callbacks. This
+	 * way the responsibility of deciding whether a command should be enabled is shared.
+	 *
+	 * @fires {@link core.command.Command#refreshState refreshState}
+	 */
+	refreshState() {
+		const data = { isEnabled: true };
+		this.fire( 'refreshState', data );
+
+		this.isEnabled = data.isEnabled;
+	}
+
+	/**
+	 * Executes the command if it is enabled.
+	 *
+	 * @protected
+	 * @param {*} param Parameter passed to {@link core.command.Command#execute execute} method of this command.
+	 */
+	_execute( param ) {
+		if ( this.isEnabled ) {
+			this._doExecute( param );
+		}
+	}
+
+	/**
+	 * Disables the command. This should be used only by the command itself. Other parts of code should add
+	 * listeners to `refreshState` event.
+	 *
+	 * @protected
+	 */
+	_disable() {
+		this.on( 'refreshState', disableCallback );
+		this.refreshState();
+	}
+
+	/**
+	 * Enables the command (internally). This should be used only by the command itself. Command will be enabled if
+	 * other listeners does not return false on `refreshState` event callbacks. Firing {@link core.command.Command#_enable}
+	 * does not guarantee that {@link core.command.Command#isEnabled} will be set to true, as it depends on other listeners.
+	 *
+	 * @protected
+	 */
+	_enable() {
+		this.off( 'refreshState', disableCallback );
+		this.refreshState();
+	}
+
+	/**
+	 * Executes command.
+	 * This is an abstract method that should be overwritten in child classes.
+	 *
+	 * @protected
+	 */
+	_doExecute() {}
+}
+
+function disableCallback( evt, data ) {
+	data.isEnabled = false;
+}
+
+utils.mix( Command, ObservableMixin );
+
+/**
+ * Fired whenever command has to have its {@link core.command.Command#isEnabled} property refreshed. Every feature,
+ * command or other class which needs to disable command (set `isEnabled` to `false`) should listen to this
+ * event.
+ *
+ * @event core.command.Command#refreshState
+ * @param {Object} data
+ * @param {Boolean} [data.isEnabled=true]
+ */

+ 141 - 0
src/creator.js

@@ -0,0 +1,141 @@
+/**
+ * @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 core
+ * @extends core.Plugin
+ */
+export default class Creator extends Plugin {
+	/**
+	 * The element used to {@link core.Creator#_replaceElement _replaceElement} the editor element.
+	 *
+	 * @private
+	 * @member {HTMLElement} core.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 core.Editor#element editor element}'s content with the data.
+	 *
+	 */
+	updateEditorElement() {
+		Creator.setDataInElement( this.editor.element, this.editor.getData() );
+	}
+
+	/**
+	 * Loads the data from the {@link core.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 core.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 core.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 core.Creator#_replaceElement _replaceElement} did.
+	 *
+	 * @protected
+	 */
+	_restoreElement() {
+		this.editor.element.style.display = '';
+		this._elementReplacement.remove();
+		this._elementReplacement = null;
+	}
+}

+ 223 - 0
src/editor.js

@@ -0,0 +1,223 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import ObservableMixin from '../utils/observablemixin.js';
+import EditorConfig from './editorconfig.js';
+import PluginCollection from './plugincollection.js';
+import Document from './treemodel/document.js';
+import CKEditorError from '../utils/ckeditorerror.js';
+import Locale from '../utils/locale.js';
+import isArray from '../utils/lib/lodash/isArray.js';
+import utils from '../utils/utils.js';
+
+/**
+ * Represents a single editor instance.
+ *
+ * @memberOf core
+ * @mixes utils.ObservaleMixin
+ */
+export default class Editor {
+	/**
+	 * Creates a new instance of the Editor class.
+	 *
+	 * 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} config The editor config.
+	 */
+	constructor( element, 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.
+		 *
+		 * @readonly
+		 * @member {HTMLElement} core.Editor#element
+		 */
+		this.element = element;
+
+		/**
+		 * Holds all configurations specific to this editor instance.
+		 *
+		 * This instance of the {@link utils.Config} class is customized so its {@link utils.Config#get} method will retrieve
+		 * global configurations available in {@link CKEDITOR.config} if configurations are not found in the
+		 * instance itself.
+		 *
+		 * @readonly
+		 * @member {utils.Config} core.Editor#config
+		 */
+		this.config = config = new EditorConfig( config );
+
+		/**
+		 * The plugins loaded and in use by this editor instance.
+		 *
+		 * @readonly
+		 * @member {core.PluginCollection} core.Editor#plugins
+		 */
+		this.plugins = new PluginCollection( this );
+
+		/**
+		 * Tree Model document managed by this editor.
+		 *
+		 * @readonly
+		 * @member {core.treeModel.Document} core.Editor#document
+		 */
+		this.document = new Document();
+
+		/**
+		 * Commands registered to the editor.
+		 *
+		 * @readonly
+		 * @member {Map} core.Editor#commands
+		 */
+		this.commands = new Map();
+
+		/**
+		 * @readonly
+		 * @member {utils.Locale} core.Editor#locale
+		 */
+		this.locale = new Locale( config.lang );
+
+		/**
+		 * Shorthand for {@link utils.Locale#t}.
+		 *
+		 * @see utils.Locale#t
+		 * @method core.Editor#t
+		 */
+		this.t = this.locale.t;
+
+		/**
+		 * The chosen creator.
+		 *
+		 * @protected
+		 * @member {core.Creator} core.Editor#_creator
+		 */
+	}
+
+	/**
+	 * Initializes the editor instance object after its creation.
+	 *
+	 * The initialization consists of the following procedures:
+	 *
+	 * * Loading and initializing the configured features and creator.
+	 * * Firing the editor creator.
+	 *
+	 * This method should be rarely used as {@link CKEDITOR#create} calls it one should never use the `Editor`
+	 * constructor directly.
+	 *
+	 * @returns {Promise} A promise which resolves once the initialization is completed.
+	 */
+	init() {
+		const that = this;
+		const config = this.config;
+		let creatorName = config.creator;
+
+		if ( !creatorName ) {
+			/**
+			 * The `config.creator` option was not defined.
+			 *
+			 * @error editor-undefined-creator
+			 */
+			return Promise.reject(
+				new CKEditorError( 'editor-undefined-creator: The config.creator option was not defined.' )
+			);
+		}
+
+		return loadPlugins()
+			.then( initPlugins )
+			.then( fireCreator );
+
+		function loadPlugins() {
+			let plugins = config.features || [];
+
+			// Handle features passed as a string.
+			if ( !isArray( plugins ) ) {
+				plugins = plugins.split( ',' );
+			}
+
+			plugins.push( creatorName );
+
+			return that.plugins.load( plugins );
+		}
+
+		function initPlugins( loadedPlugins ) {
+			return loadedPlugins.reduce( ( promise, plugin ) => {
+				return promise.then( plugin.init.bind( plugin ) );
+			}, Promise.resolve() );
+		}
+
+		function fireCreator() {
+			// We can always get the creator by its name because config.creator (which is requried) is passed
+			// to PluginCollection.load().
+			that._creator = that.plugins.get( creatorName );
+
+			// Finally fire the creator. It may be asynchronous, returning a promise.
+			return that._creator.create();
+		}
+	}
+
+	/**
+	 * Destroys the editor instance, releasing all resources used by it. If the editor replaced an element, the
+	 * element will be recovered.
+	 *
+	 * @fires core.Editor#destroy
+	 * @returns {Promise} A promise that resolves once the editor instance is fully destroyed.
+	 */
+	destroy() {
+		const that = this;
+
+		this.fire( 'destroy' );
+		this.stopListening();
+
+		return Promise.resolve()
+			.then( () => {
+				return that._creator && that._creator.destroy();
+			} )
+			.then( () => {
+				delete this.element;
+			} );
+	}
+
+	setData( data ) {
+		this.editable.setData( data );
+	}
+
+	getData() {
+		return this.editable.getData();
+	}
+
+	/**
+	 * Executes specified command with given parameter.
+	 *
+	 * @param {String} commandName Name of command to execute.
+	 * @param {*} [commandParam] If set, command will be executed with this parameter.
+	 */
+	execute( commandName, commandParam ) {
+		let command = this.commands.get( commandName );
+
+		if ( !command ) {
+			/**
+			 * Specified command has not been added to the editor.
+			 *
+			 * @error editor-command-not-found
+			 */
+			throw new CKEditorError( 'editor-command-not-found: Specified command has not been added to the editor.' );
+		}
+
+		command._execute( commandParam );
+	}
+}
+
+utils.mix( Editor, ObservableMixin );
+
+/**
+ * Fired when this editor instance is destroyed. The editor at this point is not usable and this event should be used to
+ * perform the clean-up in any plugin.
+ *
+ * @memberOf core.Editor
+ * @event destroy
+ */

+ 35 - 0
src/editorconfig.js

@@ -0,0 +1,35 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import CKEDITOR from '../../ckeditor.js';
+import Config from '../utils/config.js';
+
+/**
+ * Handles a configuration dictionary for an editor instance.
+ *
+ * The basic difference between {@link core.EditorConfig} and {@link utils.Config} is that {@link core.EditorConfig#get} retrieves
+ * configurations from {@link CKEDITOR#config} if they are not found.
+ *
+ * @memberOf core
+ * @extends utils.Config
+ */
+export default class EditorConfig extends Config {
+	/**
+	 * @inheritDoc
+	 */
+	get() {
+		// Try to take it from this editor instance.
+		let value = super.get.apply( this, arguments );
+
+		// If the configuration is not defined in the instance, try to take it from CKEDITOR.config.
+		if ( typeof value == 'undefined' ) {
+			value = super.get.apply( CKEDITOR.config, arguments );
+		}
+
+		return value;
+	}
+}

+ 22 - 0
src/feature.js

@@ -0,0 +1,22 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import Plugin from './plugin.js';
+
+/**
+ * The base class for CKEditor feature classes. Features are main way to enhance CKEditor abilities with tools,
+ * utilities, services and components.
+ *
+ * The main responsibilities for Feature are:
+ * * setting required dependencies (see {@link core.Plugin#requires},
+ * * configuring, instantiating and registering commands to editor,
+ * * registering converters to editor (if the feature operates on Tree Model),
+ * * setting and registering UI components (if the feature uses it).
+ *
+ * @memberOf core
+ */
+export default class Feature extends Plugin {}

+ 61 - 0
src/plugin.js

@@ -0,0 +1,61 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import ObservableMixin from '../utils/observablemixin.js';
+import utils from '../utils/utils.js';
+
+/**
+ * The base class for CKEditor plugin classes.
+ *
+ * @memberOf core
+ * @mixes utils.ObservaleMixin
+ */
+export default class Plugin {
+	/**
+	 * Creates a new Plugin instance.
+	 *
+	 * @param {core.Editor} editor
+	 */
+	constructor( editor ) {
+		/**
+		 * @readonly
+		 * @member {core.Editor} core.Plugin#editor
+		 */
+		this.editor = editor;
+	}
+
+	/**
+	 * An array of plugins required by this plugin.
+	 *
+	 * To keep a plugin class definition tight it's recommended to define this property as a static getter:
+	 *
+	 *		import Image from './image.js';
+	 *
+	 *		export default class ImageCaption extends Feature {
+     *			static get requires() {
+     *				return [ Image ];
+     *			}
+	 *		}
+	 *
+	 * @static
+	 * @member {Function[]} core.Plugin.requires
+	 */
+
+	/**
+	 * @returns {null|Promise}
+	 */
+	init() {}
+
+	/**
+	 * Destroys the plugin.
+	 *
+	 * TODO waits to be implemented (#186).
+	 */
+	destroy() {}
+}
+
+utils.mix( Plugin, ObservableMixin );

+ 175 - 0
src/plugincollection.js

@@ -0,0 +1,175 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import Plugin from './plugin.js';
+import CKEditorError from '../utils/ckeditorerror.js';
+import log from '../utils/log.js';
+import load from '../load.js';
+
+/**
+ * Manages a list of CKEditor plugins, including loading, resolving dependencies and initialization.
+ *
+ * @memberOf core
+ */
+export default class PluginCollection {
+	/**
+	 * Creates an instance of the PluginCollection class, initializing it with a set of plugins.
+	 *
+	 * @param {core.Editor} editor
+	 */
+	constructor( editor ) {
+		/**
+		 * @protected
+		 * @member {core.Editor} core.PluginCollection#_editor
+		 */
+		this._editor = editor;
+
+		/**
+		 * @protected
+		 * @member {Map} core.PluginCollection#_plugins
+		 */
+		this._plugins = new Map();
+	}
+
+	/**
+	 * Collection iterator. Returns `[ key, plugin ]` pairs. Plugins which are
+	 * kept in the collection twice (under their name and class) will be returned twice.
+	 */
+	[ Symbol.iterator ]() {
+		return this._plugins[ Symbol.iterator ]();
+	}
+
+	/**
+	 * Gets the plugin instance by its name or class.
+	 *
+	 * @param {String/Function} key The name of the plugin or the class.
+	 * @returns {core.Plugin}
+	 */
+	get( key ) {
+		return this._plugins.get( key );
+	}
+
+	/**
+	 * Loads a set of plugins and add them to the collection.
+	 *
+	 * @param {String[]} plugins An array of plugins to load.
+	 * @returns {Promise} A promise which gets resolved once all plugins are loaded and available into the
+	 * collection.
+	 * @param {core.Plugin[]} returns.loadedPlugins The array of loaded plugins.
+	 */
+	load( plugins ) {
+		const that = this;
+		const editor = this._editor;
+		const loading = new Set();
+		const loaded = [];
+
+		return Promise.all( plugins.map( loadPlugin ) )
+			.then( () => loaded );
+
+		function loadPlugin( pluginClassOrName ) {
+			// The plugin is already loaded or being loaded - do nothing.
+			if ( that.get( pluginClassOrName ) || loading.has( pluginClassOrName ) ) {
+				return;
+			}
+
+			let promise = ( typeof pluginClassOrName == 'string' ) ?
+				loadPluginByName( pluginClassOrName ) :
+				loadPluginByClass( pluginClassOrName );
+
+			return promise
+				.catch( ( err ) => {
+					/**
+					 * It was not possible to load the plugin.
+					 *
+					 * @error plugincollection-load
+					 * @param {String} plugin The name of the plugin that could not be loaded.
+					 */
+					log.error( 'plugincollection-load: It was not possible to load the plugin.', { plugin: pluginClassOrName } );
+
+					throw err;
+				} );
+		}
+
+		function loadPluginByName( pluginName ) {
+			return load( PluginCollection.getPluginPath( pluginName ) )
+				.then( ( PluginModule ) => {
+					return loadPluginByClass( PluginModule.default, pluginName );
+				} );
+		}
+
+		function loadPluginByClass( PluginClass, pluginName ) {
+			return new Promise( ( resolve ) => {
+				loading.add( PluginClass );
+
+				assertIsPlugin( PluginClass );
+
+				if ( PluginClass.requires ) {
+					PluginClass.requires.forEach( loadPlugin );
+				}
+
+				const plugin = new PluginClass( editor );
+				that._add( PluginClass, plugin );
+				loaded.push( plugin );
+
+				// Expose the plugin also by its name if loaded through load() by name.
+				if ( pluginName ) {
+					that._add( pluginName, plugin );
+				}
+
+				resolve();
+			} );
+		}
+
+		function assertIsPlugin( LoadedPlugin ) {
+			if ( !( LoadedPlugin.prototype instanceof Plugin ) ) {
+				/**
+				 * The loaded plugin module is not an instance of Plugin.
+				 *
+				 * @error plugincollection-instance
+				 * @param {LoadedPlugin} plugin The class which is meant to be loaded as a plugin.
+				 */
+				throw new CKEditorError(
+					'plugincollection-instance: The loaded plugin module is not an instance of Plugin.',
+					{ plugin: LoadedPlugin }
+				);
+			}
+		}
+	}
+
+	/**
+	 * Resolves a simplified plugin name to a real path. The returned
+	 * paths are relative to the main `ckeditor.js` file, but they do not start with `./`.
+	 *
+	 * For instance:
+	 *
+	 * * `foo` will be transformed to `ckeditor5/foo/foo.js`,
+	 * * `core/editor` to `ckeditor5/core/editor.js` and
+	 * * `foo/bar/bom` to `ckeditor5/foo/bar/bom.js`.
+	 *
+	 * @param {String} name
+	 * @returns {String} Path to the module.
+	 */
+	static getPluginPath( name ) {
+		// Resolve shortened feature names to `featureName/featureName`.
+		if ( name.indexOf( '/' ) < 0 ) {
+			name = name + '/' + name;
+		}
+
+		return 'ckeditor5/' + name + '.js';
+	}
+
+	/**
+	 * Adds the plugin to the collection. Exposed mainly for testing purposes.
+	 *
+	 * @protected
+	 * @param {String/Function} key The name or the plugin class.
+	 * @param {core.Plugin} plugin The instance of the plugin.
+	 */
+	_add( key, plugin ) {
+		this._plugins.set( key, plugin );
+	}
+}

+ 38 - 0
tests/_utils/ui/boxededitorui/boxededitorui.js

@@ -0,0 +1,38 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import EditorUI from '/ckeditor5/ui/editorui/editorui.js';
+import ControllerCollection from '/ckeditor5/ui/controllercollection.js';
+
+export default class BoxedEditorUI extends EditorUI {
+	constructor( editor ) {
+		super( editor );
+
+		this.collections.add( new ControllerCollection( 'top' ) );
+		this.collections.add( new ControllerCollection( 'main' ) );
+
+		const config = editor.config;
+
+		/**
+		 * @property {Number} width
+		 */
+		this.set( 'width', config.get( 'ui.width' ) );
+
+		/**
+		 * @property {Number} height
+		 */
+		this.set( 'height', config.get( 'ui.height' ) );
+	}
+
+	/**
+	 * @readonly
+	 * @property {Model} viewModel
+	 */
+	get viewModel() {
+		return this;
+	}
+}

+ 41 - 0
tests/_utils/ui/boxededitorui/boxededitoruiview.js

@@ -0,0 +1,41 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import EditorUIView from '/ckeditor5/ui/editorui/editoruiview.js';
+
+export default class BoxedEditorUIView extends EditorUIView {
+	constructor( model, locale ) {
+		super( model, locale );
+
+		this.template = {
+			tag: 'div',
+
+			attributes: {
+				class: 'ck-box'
+			},
+
+			children: [
+				{
+					tag: 'div',
+					attributes: {
+						class: 'ck-box-region ck-top'
+					}
+				},
+
+				{
+					tag: 'div',
+					attributes: {
+						class: 'ck-box-region ck-main'
+					}
+				}
+			]
+		};
+
+		this.register( 'top', '.ck-top' );
+		this.register( 'main', '.ck-main' );
+	}
+}

+ 35 - 0
tests/_utils/ui/boxlesseditorui/boxlesseditorui.js

@@ -0,0 +1,35 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import EditorUI from '/ckeditor5/ui/editorui/editorui.js';
+import ControllerCollection from '/ckeditor5/ui/controllercollection.js';
+
+export default class BoxlessEditorUI extends EditorUI {
+	constructor( editor ) {
+		super( editor );
+
+		this.collections.add( new ControllerCollection( 'editable' ) );
+
+		/**
+		 * @private
+		 * @type {ui.View}
+		 * @property _view
+		 */
+	}
+
+	get view() {
+		return this._view;
+	}
+
+	set view( view ) {
+		if ( view ) {
+			this._view = view;
+
+			view.register( 'editable', true );
+		}
+	}
+}

+ 16 - 0
tests/_utils/ui/editable/framed/framededitable.js

@@ -0,0 +1,16 @@
+/**
+ * @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 FramedEditable extends Editable {
+	constructor( editor ) {
+		super( editor );
+
+		this.viewModel.bind( 'width', 'height' ).to( editor.ui );
+	}
+}

+ 55 - 0
tests/_utils/ui/editable/framed/framededitableview.js

@@ -0,0 +1,55 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import EditableView from '/ckeditor5/ui/editable/editableview.js';
+
+export default class FramedEditableView extends EditableView {
+	constructor( model, locale ) {
+		super( model, locale );
+
+		const bind = this.attributeBinder;
+
+		// Here's the tricky part - we must return the promise from init()
+		// because iframe loading may be asynchronous. However, we can't start
+		// listening to 'load' in init(), because at this point the element is already in the DOM
+		// and the 'load' event might already be fired.
+		// So here we store both - the promise and the deferred object so we're able to resolve
+		// the promise in _iframeLoaded.
+		this._iframePromise = new Promise( ( resolve, reject ) => {
+			this._iframeDeferred = { resolve, reject };
+		} );
+
+		this.template = {
+			tag: 'iframe',
+			attributes: {
+				class: [ 'ck-framededitable' ],
+				// It seems that we need to allow scripts in order to be able to listen to events.
+				// TODO: Research that. Perhaps the src must be set?
+				sandbox: 'allow-same-origin allow-scripts',
+				width: bind.to( 'width' ),
+				height: bind.to( 'height' )
+			},
+			on: {
+				load: 'loaded'
+			}
+		};
+
+		this.on( 'loaded', this._iframeLoaded, this );
+	}
+
+	init() {
+		super.init();
+
+		return this._iframePromise;
+	}
+
+	_iframeLoaded() {
+		this.setEditableElement( this.element.contentDocument.body );
+
+		this._iframeDeferred.resolve();
+	}
+}

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

@@ -0,0 +1,11 @@
+/**
+ * @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 {
+}

+ 28 - 0
tests/_utils/ui/editable/inline/inlineeditableview.js

@@ -0,0 +1,28 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import EditableView from '/ckeditor5/ui/editable/editableview.js';
+
+export default class InlineEditableView extends EditableView {
+	constructor( model, locale, editableElement ) {
+		super( model, locale );
+
+		this.element = editableElement;
+	}
+
+	init() {
+		this.setEditableElement( this.element );
+
+		super.init();
+	}
+
+	destroy() {
+		super.destroy();
+
+		this.editableElement.contentEditable = false;
+	}
+}

+ 16 - 0
tests/_utils/ui/floatingtoolbar/floatingtoolbar.js

@@ -0,0 +1,16 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import Toolbar from '/ckeditor5/ui/bindings/toolbar.js';
+
+export default class FloatingToolbar extends Toolbar {
+	constructor( model, view, editor ) {
+		super( model, view, editor );
+
+		model.bind( 'isVisible' ).to( editor.editable, 'isFocused' );
+	}
+}

+ 27 - 0
tests/_utils/ui/floatingtoolbar/floatingtoolbarview.js

@@ -0,0 +1,27 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import ToolbarView from '/ckeditor5/ui/toolbar/toolbarview.js';
+
+export default class FloatingToolbarView extends ToolbarView {
+	constructor( model, locale ) {
+		super( model, locale );
+
+		const bind = this.attributeBinder;
+
+		this.template.attributes.class.push(
+			bind.to( 'isVisible', value => value ? 'ck-visible' : 'ck-hidden' )
+		);
+
+		// This has a high risk of breaking if someone defines "on" in the parent template.
+		// See https://github.com/ckeditor/ckeditor5-core/issues/219
+		this.template.on = {
+			// Added just for fun, but needed to keep the focus in the editable.
+			mousedown: ( evt ) => evt.preventDefault()
+		};
+	}
+}

+ 2 - 0
tests/ckeditor.html

@@ -0,0 +1,2 @@
+<div id="content"></div>
+<div class="editor"></div>

+ 95 - 4
tests/ckeditor.js

@@ -5,10 +5,101 @@
 
 'use strict';
 
+import testUtils from '/tests/_utils/utils.js';
+import coreTestUtils from '/tests/core/_utils/utils.js';
+
 import CKEDITOR from '/ckeditor.js';
+import Editor from '/ckeditor5/core/editor.js';
+import Config from '/ckeditor5/utils/config.js';
+
+let content = document.getElementById( 'content' );
+let editorConfig = { creator: 'creator-test' };
+
+testUtils.createSinonSandbox();
+coreTestUtils.defineEditorCreatorMock( 'test' );
+
+beforeEach( () => {
+	// Destroy all editor instances.
+	while ( CKEDITOR.instances.length ) {
+		CKEDITOR.instances.get( 0 ).destroy();
+	}
+} );
+
+describe( 'create', () => {
+	it( 'should return a promise', () => {
+		expect( CKEDITOR.create( content, editorConfig ) ).to.be.instanceof( Promise );
+	} );
+
+	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' ) );
+		} );
+	} );
+
+	it( 'should set configurations on the new editor', () => {
+		return CKEDITOR.create( content, { test: 1, creator: 'creator-test' } ).then( ( editor ) => {
+			expect( editor.config.test ).to.equal( 1 );
+		} );
+	} );
+
+	it( 'should add the editor to the `instances` collection', () => {
+		return CKEDITOR.create( content, editorConfig ).then( ( editor ) => {
+			expect( CKEDITOR.instances ).to.have.length( 1 );
+			expect( CKEDITOR.instances.get( 0 ) ).to.equal( editor );
+		} );
+	} );
+
+	it( 'should remove the editor from the `instances` collection on `destroy` event', () => {
+		let editor1, editor2;
+
+		// Create the first editor.
+		return CKEDITOR.create( content, editorConfig ).then( ( editor ) => {
+			editor1 = editor;
+
+			// Create the second editor.
+			return CKEDITOR.create( '.editor', editorConfig ).then( ( editor ) => {
+				editor2 = editor;
+
+				// It should have 2 editors.
+				expect( CKEDITOR.instances ).to.have.length( 2 );
+
+				// Destroy one of them.
+				editor1.destroy();
+
+				// It should have 1 editor now.
+				expect( CKEDITOR.instances ).to.have.length( 1 );
+
+				// Ensure that the remaining is the right one.
+				expect( CKEDITOR.instances.get( 0 ) ).to.equal( editor2 );
+			} );
+		} );
+	} );
+
+	it( 'should be rejected on element not found', () => {
+		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( 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 );
+		} );
+	} );
+} );
 
-describe( 'CKEDITOR', () => {
-	it( 'is an object', () => {
-		expect( CKEDITOR ).to.be.an( 'object' );
+describe( 'config', () => {
+	it( 'should be an instance of Config', () => {
+		expect( CKEDITOR.config ).to.be.an.instanceof( Config );
 	} );
-} );
+} );

+ 3 - 0
tests/creator/creator.html

@@ -0,0 +1,3 @@
+<textarea id="getData-textarea">&lt;b&gt;foo&lt;/b&gt;</textarea>
+<div id="getData-div"><b>foo</b></div>
+<template id="getData-template"><b>foo</b></template>

+ 208 - 0
tests/creator/creator.js

@@ -0,0 +1,208 @@
+/**
+ * @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/_utils/utils.js';
+import Creator from '/ckeditor5/core/creator.js';
+import Editor from '/ckeditor5/core/editor.js';
+import Plugin from '/ckeditor5/core/plugin.js';
+
+testUtils.createSinonSandbox();
+
+describe( 'Creator', () => {
+	let creator, editor;
+
+	beforeEach( () => {
+		const editorElement = document.createElement( 'div' );
+		document.body.appendChild( editorElement );
+
+		editor = new Editor( editorElement );
+		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( '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;
+		} );
+	} );
+} );

+ 55 - 0
tests/creator/manual/_assets/styles.css

@@ -0,0 +1,55 @@
+.ck-body {
+	margin: 10px 0;
+	border: solid 3px red;
+}
+.ck-body::before {
+	content: '[[ ck-body region ]]';
+}
+
+.ck-box {
+	margin: 10px 0;
+}
+
+.ck-main {
+	border: solid 1px rgb( 200, 200, 200 );
+	padding: 5px
+}
+
+.ck-toolbar {
+	border: solid 1px rgb( 200, 200, 200 );
+	background: rgb( 240, 240, 240 );
+	padding: 5px;
+}
+
+.ck-toolbar.ck-hidden {
+	opacity: 0.3;
+}
+
+.ck-button {
+	border: solid 1px transparent;
+	background: transparent;
+	display: inline-block;
+	padding: 5px 10px;
+	border-radius: 1px;
+	margin-right: 5px;
+
+	font-size: 16px;
+	color: rgb( 69, 69, 69 );
+
+	cursor: pointer;
+}
+
+.ck-button:hover:not(.ck-disabled) {
+	border: solid 1px rgb( 180, 180, 180 );
+	box-shadow: 0 0 2px rgba( 0, 0, 0, 0.1 );
+}
+
+.ck-button.ck-on {
+	border: solid 1px rgb( 200, 200, 200 );
+	box-shadow: inset 0 0 3px rgba( 0, 0, 0, 0.1 );
+	background: rgba( 0, 0, 0, 0.05 );
+}
+
+.ck-button.ck-disabled {
+	color: rgb( 180, 180, 180 );
+}

+ 77 - 0
tests/creator/manual/_utils/creator/classiccreator.js

@@ -0,0 +1,77 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import Creator from '/ckeditor5/core/creator.js';
+import BoxedEditorUI from '/tests/core/_utils/ui/boxededitorui/boxededitorui.js';
+import BoxedEditorUIView from '/tests/core/_utils/ui/boxededitorui/boxededitoruiview.js';
+import FramedEditable from '/tests/core/_utils/ui/editable/framed/framededitable.js';
+import FramedEditableView from '/tests/core/_utils/ui/editable/framed/framededitableview.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 {
+	constructor( editor ) {
+		super( editor );
+
+		editor.ui = this._createEditorUI();
+	}
+
+	create() {
+		imitateFeatures( this.editor );
+
+		this._replaceElement();
+		this._setupEditable();
+		this._setupToolbar();
+
+		return super.create()
+			.then( () => this.loadDataFromEditorElement() );
+	}
+
+	destroy() {
+		imitateDestroyFeatures();
+
+		this.updateEditorElement();
+
+		return super.destroy();
+	}
+
+	_setupEditable() {
+		const editable = this._createEditable();
+
+		this.editor.editable = editable;
+		this.editor.ui.add( 'main', editable );
+	}
+
+	_setupToolbar() {
+		const toolbarModel = new Model();
+		const toolbar = new Toolbar( toolbarModel, new ToolbarView( toolbarModel, this.editor.locale ), this.editor );
+
+		toolbar.addButtons( this.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 );
+
+		editorUI.view = editorUIView;
+
+		return editorUI;
+	}
+}

+ 81 - 0
tests/creator/manual/_utils/creator/inlinecreator.js

@@ -0,0 +1,81 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import Creator from '/ckeditor5/core/creator.js';
+import EditorUIView from '/ckeditor5/ui/editorui/editoruiview.js';
+import BoxlessEditorUI from '/tests/core/_utils/ui/boxlesseditorui/boxlesseditorui.js';
+import InlineEditable from '/tests/core/_utils/ui/editable/inline/inlineeditable.js';
+import InlineEditableView from '/tests/core/_utils/ui/editable/inline/inlineeditableview.js';
+import Model from '/ckeditor5/ui/model.js';
+import FloatingToolbar from '/tests/core/_utils/ui/floatingtoolbar/floatingtoolbar.js';
+import FloatingToolbarView from '/tests/core/_utils/ui/floatingtoolbar/floatingtoolbarview.js';
+import { imitateFeatures, imitateDestroyFeatures } from '../imitatefeatures.js';
+
+export default class InlineCreator extends Creator {
+	constructor( editor ) {
+		super( editor );
+
+		editor.ui = this._createEditorUI();
+	}
+
+	create() {
+		imitateFeatures( this.editor );
+
+		this._setupEditable();
+		this._setupToolbar();
+
+		return super.create()
+			.then( () => this.loadDataFromEditorElement() );
+	}
+
+	destroy() {
+		imitateDestroyFeatures();
+
+		this.updateEditorElement();
+
+		return super.destroy();
+	}
+
+	_setupEditable() {
+		this.editor.editable = this._createEditable();
+
+		this.editor.ui.add( 'editable', this.editor.editable );
+	}
+
+	_setupToolbar() {
+		const locale = this.editor.locale;
+
+		const toolbar1Model = new Model();
+		const toolbar2Model = new Model();
+
+		const toolbar1 = new FloatingToolbar( toolbar1Model, new FloatingToolbarView( toolbar1Model, locale ), this.editor );
+		const toolbar2 = new FloatingToolbar( toolbar2Model, new FloatingToolbarView( toolbar2Model, locale ), this.editor );
+
+		toolbar1.addButtons( this.editor.config.toolbar );
+		toolbar2.addButtons( this.editor.config.toolbar.reverse() );
+
+		this.editor.ui.add( 'body', toolbar1 );
+		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 );
+
+		editorUI.view = new EditorUIView( editorUI.viewModel, this.editor.locale );
+
+		return editorUI;
+	}
+}

+ 57 - 0
tests/creator/manual/_utils/imitatefeatures.js

@@ -0,0 +1,57 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import Model from '/ckeditor5/ui/model.js';
+import Button from '/ckeditor5/ui/button/button.js';
+import ButtonView from '/ckeditor5/ui/button/buttonview.js';
+
+/**
+ * Immitates that some features were loaded and did their job.
+ *
+ * @param {core.Editor} editor
+ */
+export function imitateFeatures( editor ) {
+	const t = editor.t;
+
+	const boldModel = new Model( {
+		isEnabled: true,
+		isOn: false,
+		label: t( 'Bold' )
+	} );
+
+	boldModel.on( 'execute', () => {
+		/* global console */
+		console.log( 'bold executed' );
+
+		boldModel.isOn = !boldModel.isOn;
+	} );
+
+	editor.ui.featureComponents.add( 'bold', Button, ButtonView, boldModel );
+
+	const italicModel = new Model( {
+		isEnabled: true,
+		isOn: false,
+		label: t( 'Italic' )
+	} );
+
+	italicModel.on( 'execute', () => {
+		/* global console */
+		console.log( 'italic executed' );
+
+		italicModel.isOn = !italicModel.isOn;
+	} );
+
+	editor.ui.featureComponents.add( 'italic', Button, ButtonView, italicModel );
+
+	window.boldModel = boldModel;
+	window.italicModel = italicModel;
+}
+
+export function imitateDestroyFeatures() {
+	delete window.boldModel;
+	delete window.italicModel;
+}

+ 12 - 0
tests/creator/manual/creator-classic.html

@@ -0,0 +1,12 @@
+<head>
+	<link rel="stylesheet" href="%TEST_DIR%_assets/styles.css">
+</head>
+
+<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>
+

+ 50 - 0
tests/creator/manual/creator-classic.js

@@ -0,0 +1,50 @@
+/**
+ * @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 ClassicCreator from '/tests/core/creator/manual/_utils/creator/classiccreator.js';
+import testUtils from '/tests/utils/_utils/utils.js';
+
+let editor, observer;
+
+function initEditor() {
+	CKEDITOR.create( '#editor', {
+		creator: ClassicCreator,
+		ui: {
+			width: 400,
+			height: 400
+		},
+		toolbar: [ 'bold', 'italic' ]
+	} )
+	.then( ( newEditor ) => {
+		console.log( 'Editor was initialized', newEditor );
+		console.log( 'You can now play with it using global `editor` variable.' );
+
+		window.editor = editor = newEditor;
+
+		observer = testUtils.createObserver();
+		observer.observe( 'Editable', editor.editable );
+	} );
+}
+
+function destroyEditor() {
+	editor.destroy()
+		.then( () => {
+			window.editor = null;
+			editor = null;
+
+			observer.stopListening();
+			observer = null;
+
+			console.log( 'Editor was destroyed' );
+		} );
+}
+
+document.getElementById( 'initEditor' ).addEventListener( 'click', initEditor );
+document.getElementById( 'destroyEditor' ).addEventListener( 'click', destroyEditor );

+ 23 - 0
tests/creator/manual/creator-classic.md

@@ -0,0 +1,23 @@
+@bender-ui: collapsed
+
+1. Click "Init editor".
+2. Expected:
+  * Framed editor should be created.
+  * It should be rectangular (400x400).
+  * Original element should disappear.
+  * There should be a toolbar with "Bold" and "Italic" buttons.
+3. Click "Destroy editor".
+4. Expected:
+  * Editor should be destroyed.
+  * Original element should be visible.
+  * The element should contain its data (updated).
+  * The 'ck-body region' should be removed.
+
+## Notes:
+
+* You can play with:
+  * `editor.editable.isEditable`,
+  * `editor.ui.width/height`.
+  * `boldModel.isEnabled` and `italicModel.isEnabled`.
+* Changes to `editable.isFocused/isEditable` should be logged to the console.
+* Clicks on the buttons should be logged to the console.

+ 12 - 0
tests/creator/manual/creator-inline.html

@@ -0,0 +1,12 @@
+<head>
+	<link rel="stylesheet" href="%TEST_DIR%_assets/styles.css">
+</head>
+
+<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>
+

+ 46 - 0
tests/creator/manual/creator-inline.js

@@ -0,0 +1,46 @@
+/**
+ * @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 InlineCreator from '/tests/core/creator/manual/_utils/creator/inlinecreator.js';
+import testUtils from '/tests/utils/_utils/utils.js';
+
+let editor, observer;
+
+function initEditor() {
+	CKEDITOR.create( '#editor', {
+		creator: InlineCreator,
+		toolbar: [ 'bold', 'italic' ]
+	} )
+	.then( ( newEditor ) => {
+		console.log( 'Editor was initialized', newEditor );
+		console.log( 'You can now play with it using global `editor` variable.' );
+
+		window.editor = editor = newEditor;
+
+		observer = testUtils.createObserver();
+		observer.observe( 'Editable', editor.editable );
+	} );
+}
+
+function destroyEditor() {
+	editor.destroy()
+		.then( () => {
+			window.editor = null;
+			editor = null;
+
+			observer.stopListening();
+			observer = null;
+
+			console.log( 'Editor was destroyed' );
+		} );
+}
+
+document.getElementById( 'initEditor' ).addEventListener( 'click', initEditor );
+document.getElementById( 'destroyEditor' ).addEventListener( 'click', destroyEditor );

+ 21 - 0
tests/creator/manual/creator-inline.md

@@ -0,0 +1,21 @@
+@bender-ui: collapsed
+
+1. Click "Init editor".
+2. Expected:
+  * Inline editor should be created.
+  * There should be **two** toolbars:
+    * one with "Bold" and "Italic" buttons,
+    * second with "Italic" and "Bold" buttons.
+3. Click "Destroy editor".
+4. Expected:
+  * Editor should be destroyed (the element should not be editable).
+  * The element should contain its data (updated).
+  * The 'ck-body region' should be removed.
+
+## Notes:
+
+* You can play with:
+  * `editor.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).

+ 212 - 0
tests/editor/creator.js

@@ -0,0 +1,212 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+/* bender-tags: editor, creator */
+
+import moduleUtils from '/tests/_utils/module.js';
+import testUtils from '/tests/_utils/utils.js';
+import coreTestUtils from '/tests/core/_utils/utils.js';
+import Editor from '/ckeditor5/core/editor.js';
+import Creator from '/ckeditor5/core/creator.js';
+import CKEditorError from '/ckeditor5/utils/ckeditorerror.js';
+
+let editor, element;
+
+function initEditor( config ) {
+	element = document.createElement( 'div' );
+	document.body.appendChild( element );
+
+	editor = new Editor( element, config );
+
+	return editor.init();
+}
+
+testUtils.createSinonSandbox();
+
+before( () => {
+	coreTestUtils.defineEditorCreatorMock( 'test1', {
+		create: sinon.spy(),
+		destroy: sinon.spy()
+	} );
+
+	coreTestUtils.defineEditorCreatorMock( 'test-throw-on-many1' );
+	coreTestUtils.defineEditorCreatorMock( 'test-throw-on-many2' );
+
+	coreTestUtils.defineEditorCreatorMock( 'test-config1', {
+		create: sinon.spy()
+	} );
+	coreTestUtils.defineEditorCreatorMock( 'test-config2', {
+		create: sinon.spy()
+	} );
+
+	moduleUtils.define( 'test3', [ 'core/plugin' ], ( Plugin ) => {
+		return class extends Plugin {};
+	} );
+
+	moduleUtils.define( 'creator-async-create', [ 'core/creator' ], ( Creator ) => {
+		return class extends Creator {
+			create() {
+				return new Promise( ( resolve, reject ) => {
+					reject( new Error( 'Catch me - create.' ) );
+				} );
+			}
+
+			destroy() {}
+		};
+	} );
+
+	moduleUtils.define( 'creator-async-destroy', [ 'core/creator' ], ( Creator ) => {
+		return class extends Creator {
+			create() {}
+
+			destroy() {
+				return new Promise( ( resolve, reject ) => {
+					reject( new Error( 'Catch me - destroy.' ) );
+				} );
+			}
+		};
+	} );
+
+	moduleUtils.define( 'creator-destroy-order', [ 'core/creator' ], ( Creator ) => {
+		return class extends Creator {
+			create() {}
+
+			destroy() {
+				editor._elementInsideCreatorDestroy = this.editor.element;
+				editor._destroyOrder.push( 'creator' );
+			}
+		};
+	} );
+} );
+
+afterEach( () => {
+	editor = null; // To make sure we're using the freshly inited editor.
+} );
+
+///////////////////
+
+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 );
+
+				// 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: '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' );
+
+				return editor.destroy();
+			} )
+			.then( () => {
+				sinon.assert.calledOnce( creator1.destroy );
+			} );
+	} );
+
+	it( 'should chain the promise from the creator (enables async creators)', () => {
+		return initEditor( {
+				creator: '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: 'creator-destroy-order'
+			} )
+			.then( () => {
+				editor._destroyOrder = [];
+				editor.on( 'destroy', () => {
+					editor._destroyOrder.push( 'event' );
+				} );
+
+				return editor.destroy();
+			} )
+			.then( () => {
+				expect( editor._elementInsideCreatorDestroy ).to.not.be.undefined;
+				expect( editor._destroyOrder ).to.deep.equal( [ 'event', 'creator' ] );
+			} );
+	} );
+} );

+ 280 - 0
tests/editor/editor.js

@@ -0,0 +1,280 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+/* bender-tags: editor */
+
+import moduleUtils from '/tests/_utils/module.js';
+import coreTestUtils from '/tests/core/_utils/utils.js';
+import Editor from '/ckeditor5/core/editor.js';
+import EditorConfig from '/ckeditor5/core/editorconfig.js';
+import Plugin from '/ckeditor5/core/plugin.js';
+import Command from '/ckeditor5/core/command/command.js';
+import Locale from '/ckeditor5/utils/locale.js';
+import CKEditorError from '/ckeditor5/utils/ckeditorerror.js';
+
+const pluginClasses = {};
+let element;
+
+before( () => {
+	// Define fake plugins to be used in tests.
+	coreTestUtils.defineEditorCreatorMock( 'test', {
+		init: sinon.spy().named( 'creator-test' )
+	} );
+
+	pluginDefinition( 'A' );
+	pluginDefinition( 'B' );
+	pluginDefinition( 'C', [ 'B' ] );
+	pluginDefinition( 'D', [ 'C' ] );
+} );
+
+beforeEach( () => {
+	element = document.createElement( 'div' );
+	document.body.appendChild( element );
+} );
+
+///////////////////
+
+describe( 'constructor', () => {
+	it( 'should create a new editor instance', () => {
+		const editor = new Editor( element );
+
+		expect( editor ).to.have.property( 'element' ).to.equal( element );
+	} );
+} );
+
+describe( 'config', () => {
+	it( 'should be an instance of EditorConfig', () => {
+		const editor = new Editor( element );
+
+		expect( editor.config ).to.be.an.instanceof( EditorConfig );
+	} );
+} );
+
+describe( 'locale', () => {
+	it( 'is instantiated and t() is exposed', () => {
+		const editor = new Editor( element );
+
+		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' } );
+
+		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'
+		} );
+
+		let promise = editor.init();
+
+		expect( promise ).to.be.an.instanceof( Promise );
+
+		return promise;
+	} );
+
+	it( 'should load features and creator', () => {
+		const editor = new Editor( element, {
+			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 );
+		} );
+	} );
+
+	it( 'should load features passed as a string', () => {
+		const editor = new Editor( element, {
+			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', () => {
+		const editor = new Editor( element, {
+			features: [ 'A', 'D' ],
+			creator: 'creator-test'
+		} );
+
+		return editor.init().then( () => {
+			sinon.assert.callOrder(
+				editor.plugins.get( 'creator-test' ).init,
+				editor.plugins.get( pluginClasses.A ).init,
+				editor.plugins.get( pluginClasses.B ).init,
+				editor.plugins.get( pluginClasses.C ).init,
+				editor.plugins.get( pluginClasses.D ).init
+			);
+		} );
+	} );
+
+	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', [ 'async' ] );
+
+		moduleUtils.define( 'async', () => {
+			PluginAsync.prototype.init = sinon.spy( () => {
+				return new Promise( ( resolve ) => {
+					setTimeout( () => {
+						asyncSpy();
+						resolve();
+					}, 0 );
+				} );
+			} );
+
+			return PluginAsync;
+		} );
+
+		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 ).init,
+				editor.plugins.get( PluginAsync ).init,
+				// This one is called with delay by the async init.
+				asyncSpy,
+				editor.plugins.get( pluginClasses.sync ).init
+			);
+		} );
+	} );
+} );
+
+describe( 'plugins', () => {
+	it( 'should be empty on new editor', () => {
+		const editor = new Editor( element );
+
+		expect( getPlugins( editor ) ).to.be.empty;
+	} );
+} );
+
+describe( 'destroy', () => {
+	it( 'should fire "destroy"', () => {
+		const editor = new Editor( element );
+		let spy = sinon.spy();
+
+		editor.on( 'destroy', spy );
+
+		return editor.destroy().then( () => {
+			sinon.assert.called( spy );
+		} );
+	} );
+
+	it( 'should delete the "element" property', () => {
+		const editor = new Editor( element );
+
+		return editor.destroy().then( () => {
+			expect( editor ).to.not.have.property( 'element' );
+		} );
+	} );
+} );
+
+describe( 'execute', () => {
+	it( 'should execute specified command', () => {
+		const editor = new Editor( element );
+
+		let command = new Command( editor );
+		sinon.spy( command, '_execute' );
+
+		editor.commands.set( 'command_name', command );
+		editor.execute( 'command_name' );
+
+		expect( command._execute.calledOnce ).to.be.true;
+	} );
+
+	it( 'should throw an error if specified command has not been added', () => {
+		const editor = new Editor( element );
+
+		expect( () => {
+			editor.execute( 'command' );
+		} ).to.throw( CKEditorError, /editor-command-not-found/ );
+	} );
+} );
+
+describe( 'setData', () => {
+	it( 'should set data on the editable', () => {
+		const editor = new Editor( element );
+		editor.editable = {
+			setData: sinon.spy()
+		};
+
+		editor.setData( 'foo' );
+
+		expect( editor.editable.setData.calledOnce ).to.be.true;
+		expect( editor.editable.setData.args[ 0 ][ 0 ] ).to.equal( 'foo' );
+	} );
+
+	it( 'should get data from the editable', () => {
+		const editor = new Editor( element );
+		editor.editable = {
+			getData() {
+				return 'bar';
+			}
+		};
+
+		expect( editor.getData() ).to.equal( 'bar' );
+	} );
+} );
+
+/**
+ * @param {String} name Name of the plugin.
+ * @param {String[]} deps Dependencies of the plugin (only other plugins).
+ */
+function pluginDefinition( name, deps ) {
+	moduleUtils.define( name, deps || [], function() {
+		class NewPlugin extends Plugin {}
+
+		NewPlugin.prototype.init = sinon.spy().named( name );
+		NewPlugin.requires = Array.from( arguments );
+
+		pluginClasses[ name ] = NewPlugin;
+
+		return NewPlugin;
+	} );
+}
+
+/**
+ * Returns an array of loaded plugins.
+ */
+function getPlugins( editor ) {
+	const plugins = [];
+
+	for ( let entry of editor.plugins ) {
+		// Keep only plugins kept under their classes.
+		if ( typeof entry[ 0 ] == 'function' ) {
+			plugins.push( entry[ 1 ] );
+		}
+	}
+
+	return plugins;
+}

+ 41 - 0
tests/editorconfig.js

@@ -0,0 +1,41 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import EditorConfig from '/ckeditor5/core/editorconfig.js';
+import CKEDITOR from '/ckeditor.js';
+
+let config;
+
+beforeEach( () => {
+	config = new EditorConfig( {
+		test: 1
+	} );
+} );
+
+describe( 'constructor', () => {
+	it( 'should set configurations', () => {
+		expect( config ).to.have.property( 'test' ).to.equal( 1 );
+	} );
+} );
+
+describe( 'get', () => {
+	it( 'should retrieve a configuration', () => {
+		expect( config.get( 'test' ) ).to.equal( 1 );
+	} );
+
+	it( 'should fallback to CKEDITOR.config', () => {
+		CKEDITOR.config.set( {
+			globalConfig: 2
+		} );
+
+		expect( config.get( 'globalConfig' ) ).to.equal( 2 );
+	} );
+
+	it( 'should return undefined for non existing configuration', () => {
+		expect( config.get( 'invalid' ) ).to.be.undefined();
+	} );
+} );

+ 23 - 0
tests/plugin.js

@@ -0,0 +1,23 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import Plugin from '/ckeditor5/core/plugin.js';
+import Editor from '/ckeditor5/core/editor.js';
+
+let editor;
+
+before( () => {
+	editor = new Editor( document.body.appendChild( document.createElement( 'div' ) ) );
+} );
+
+describe( 'constructor', () => {
+	it( 'should set the `editor` property', () => {
+		let plugin = new Plugin( editor );
+
+		expect( plugin ).to.have.property( 'editor' ).to.equal( editor );
+	} );
+} );

+ 299 - 0
tests/plugincollection.js

@@ -0,0 +1,299 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import moduleUtils from '/tests/_utils/module.js';
+import testUtils from '/tests/_utils/utils.js';
+import Editor from '/ckeditor5/core/editor.js';
+import PluginCollection from '/ckeditor5/core/plugincollection.js';
+import Plugin from '/ckeditor5/core/plugin.js';
+import Creator from '/ckeditor5/core/creator.js';
+import CKEditorError from '/ckeditor5/utils/ckeditorerror.js';
+import log from '/ckeditor5/utils/log.js';
+
+let editor;
+let PluginA, PluginB, PluginC, PluginD, PluginE, PluginF, PluginG;
+class TestError extends Error {}
+
+testUtils.createSinonSandbox();
+
+before( () => {
+	PluginA = createPlugin( 'A' );
+	PluginB = createPlugin( 'B' );
+	PluginC = createPlugin( 'C' );
+	PluginD = createPlugin( 'D' );
+	PluginE = createPlugin( 'E' );
+	PluginF = createPlugin( 'F' );
+	PluginG = createPlugin( 'G', Creator );
+
+	PluginC.requires = [ PluginB ];
+	PluginD.requires = [ PluginA, PluginC ];
+	PluginF.requires = [ PluginE ];
+	PluginE.requires = [ PluginF ];
+
+	editor = new Editor( document.body.appendChild( document.createElement( 'div' ) ) );
+} );
+
+// Create fake plugins that will be used on tests.
+
+moduleUtils.define( 'A', () => {
+	return PluginA;
+} );
+
+moduleUtils.define( 'B', () => {
+	return PluginB;
+} );
+
+moduleUtils.define( 'C', [ 'core/editor', 'B' ], () => {
+	return PluginC;
+} );
+
+moduleUtils.define( 'D', [ 'core/editor', 'A', 'C' ], () => {
+	return PluginD;
+} );
+
+moduleUtils.define( 'E', [ 'core/editor', 'F' ], () => {
+	return PluginE;
+} );
+
+moduleUtils.define( 'F', [ 'core/editor', 'E' ], () => {
+	return PluginF;
+} );
+
+moduleUtils.define( 'G', () => {
+	return PluginG;
+} );
+
+// Erroneous cases.
+
+moduleUtils.define( 'X', () => {
+	throw new TestError( 'Some error inside a plugin' );
+} );
+
+moduleUtils.define( 'Y', () => {
+	return class {};
+} );
+
+/////////////
+
+describe( 'load', () => {
+	it( 'should not fail when trying to load 0 plugins (empty array)', () => {
+		let plugins = new PluginCollection( editor );
+
+		return plugins.load( [] )
+			.then( () => {
+				expect( getPlugins( plugins ) ).to.be.empty();
+			} );
+	} );
+
+	it( 'should add collection items for loaded plugins', () => {
+		let plugins = new PluginCollection( editor );
+
+		return plugins.load( [ 'A', 'B' ] )
+			.then( () => {
+				expect( getPlugins( plugins ).length ).to.equal( 2 );
+
+				expect( plugins.get( PluginA ) ).to.be.an.instanceof( PluginA );
+				expect( plugins.get( PluginB ) ).to.be.an.instanceof( PluginB );
+
+				expect( plugins.get( 'A' ) ).to.be.an.instanceof( PluginA );
+				expect( plugins.get( 'B' ) ).to.be.an.instanceof( PluginB );
+			} );
+	} );
+
+	it( 'should load dependency plugins', () => {
+		let plugins = new PluginCollection( editor );
+		let spy = sinon.spy( plugins, '_add' );
+
+		return plugins.load( [ 'A', 'C' ] )
+			.then( ( loadedPlugins ) => {
+				expect( getPlugins( plugins ).length ).to.equal( 3 );
+
+				expect( getPluginNames( getPluginsFromSpy( spy ) ) )
+					.to.deep.equal( [ 'A', 'B', 'C' ], 'order by plugins._add()' );
+				expect( getPluginNames( loadedPlugins ) )
+					.to.deep.equal( [ 'A', 'B', 'C' ], 'order by returned value' );
+			} );
+	} );
+
+	it( 'should be ok when dependencies are loaded first', () => {
+		let plugins = new PluginCollection( editor );
+		let spy = sinon.spy( plugins, '_add' );
+
+		return plugins.load( [ 'A', 'B', 'C' ] )
+			.then( ( loadedPlugins ) => {
+				expect( getPlugins( plugins ).length ).to.equal( 3 );
+
+				expect( getPluginNames( getPluginsFromSpy( spy ) ) )
+					.to.deep.equal( [ 'A', 'B', 'C' ], 'order by plugins._add()' );
+				expect( getPluginNames( loadedPlugins ) )
+					.to.deep.equal( [ 'A', 'B', 'C' ], 'order by returned value' );
+			} );
+	} );
+
+	it( 'should load deep dependency plugins', () => {
+		let plugins = new PluginCollection( editor );
+		let spy = sinon.spy( plugins, '_add' );
+
+		return plugins.load( [ 'D' ] )
+			.then( ( loadedPlugins ) => {
+				expect( getPlugins( plugins ).length ).to.equal( 4 );
+
+				// The order must have dependencies first.
+				expect( getPluginNames( getPluginsFromSpy( spy ) ) )
+					.to.deep.equal( [ 'A', 'B', 'C', 'D' ], 'order by plugins._add()' );
+				expect( getPluginNames( loadedPlugins ) )
+					.to.deep.equal( [ 'A', 'B', 'C', 'D' ], 'order by returned value' );
+			} );
+	} );
+
+	it( 'should handle cross dependency plugins', () => {
+		let plugins = new PluginCollection( editor );
+		let spy = sinon.spy( plugins, '_add' );
+
+		return plugins.load( [ 'A', 'E' ] )
+			.then( ( loadedPlugins ) => {
+				expect( getPlugins( plugins ).length ).to.equal( 3 );
+
+				// The order must have dependencies first.
+				expect( getPluginNames( getPluginsFromSpy( spy ) ) )
+					.to.deep.equal( [ 'A', 'F', 'E' ], 'order by plugins._add()' );
+				expect( getPluginNames( loadedPlugins ) )
+					.to.deep.equal( [ 'A', 'F', 'E' ], 'order by returned value' );
+			} );
+	} );
+
+	it( 'should load grand child classes', () => {
+		let plugins = new PluginCollection( editor );
+
+		return plugins.load( [ 'G' ] )
+			.then( () => {
+				expect( getPlugins( plugins ).length ).to.equal( 1 );
+			} );
+	} );
+
+	it( 'should set the `editor` property on loaded plugins', () => {
+		let plugins = new PluginCollection( editor );
+
+		return plugins.load( [ 'A', 'B' ] )
+			.then( () => {
+				expect( plugins.get( 'A' ).editor ).to.equal( editor );
+				expect( plugins.get( 'B' ).editor ).to.equal( editor );
+			} );
+	} );
+
+	it( 'should reject on invalid plugin names (forward require.js loading error)', () => {
+		let logSpy = testUtils.sinon.stub( log, 'error' );
+
+		let plugins = new PluginCollection( editor );
+
+		return plugins.load( [ 'A', 'BAD', 'B' ] )
+			// Throw here, so if by any chance plugins.load() was resolved correctly catch() will be stil executed.
+			.then( () => {
+				throw new Error( 'Test error: this promise should not be resolved successfully' );
+			} )
+			.catch( ( err ) => {
+				expect( err ).to.be.an.instanceof( Error );
+				// Make sure it's the Require.JS error, not the one thrown above.
+				expect( err.message ).to.match( /^Script error for/ );
+
+				sinon.assert.calledOnce( logSpy );
+				expect( logSpy.args[ 0 ][ 0 ] ).to.match( /^plugincollection-load:/ );
+			} );
+	} );
+
+	it( 'should reject on broken plugins (forward the error thrown in a plugin)', () => {
+		let logSpy = testUtils.sinon.stub( log, 'error' );
+
+		let plugins = new PluginCollection( editor );
+
+		return plugins.load( [ 'A', 'X', 'B' ] )
+			// Throw here, so if by any chance plugins.load() was resolved correctly catch() will be stil executed.
+			.then( () => {
+				throw new Error( 'Test error: this promise should not be resolved successfully' );
+			} )
+			.catch( ( err ) => {
+				expect( err ).to.be.an.instanceof( TestError );
+				expect( err ).to.have.property( 'message', 'Some error inside a plugin' );
+
+				sinon.assert.calledOnce( logSpy );
+				expect( logSpy.args[ 0 ][ 0 ] ).to.match( /^plugincollection-load:/ );
+			} );
+	} );
+
+	it( 'should reject when loading a module which is not a plugin', () => {
+		let logSpy = testUtils.sinon.stub( log, 'error' );
+
+		let plugins = new PluginCollection( editor );
+
+		return plugins.load( [ 'Y' ] )
+			// Throw here, so if by any chance plugins.load() was resolved correctly catch() will be stil executed.
+			.then( () => {
+				throw new Error( 'Test error: this promise should not be resolved successfully' );
+			} )
+			.catch( ( err ) => {
+				expect( err ).to.be.an.instanceof( CKEditorError );
+				expect( err.message ).to.match( /^plugincollection-instance/ );
+
+				sinon.assert.calledOnce( logSpy );
+				expect( logSpy.args[ 0 ][ 0 ] ).to.match( /^plugincollection-load:/ );
+			} );
+	} );
+} );
+
+describe( 'getPluginPath()', () => {
+	it( 'generates path for modules within some package', () => {
+		const p = PluginCollection.getPluginPath( 'some/ba' );
+
+		expect( p ).to.equal( 'ckeditor5/some/ba.js' );
+	} );
+
+	it( 'generates path from simplified feature name', () => {
+		const p = PluginCollection.getPluginPath( 'foo' );
+
+		expect( p ).to.equal( 'ckeditor5/foo/foo.js' );
+	} );
+} );
+
+function createPlugin( name, baseClass ) {
+	baseClass = baseClass || Plugin;
+
+	const P = class extends baseClass {
+		constructor( editor ) {
+			super( editor );
+			this._pluginName = name;
+		}
+	};
+
+	P._pluginName = name;
+
+	return P;
+}
+
+function getPlugins( pluginCollection ) {
+	const plugins = [];
+
+	for ( let entry of pluginCollection ) {
+		// Keep only plugins kept under their classes.
+		if ( typeof entry[ 0 ] == 'function' ) {
+			plugins.push( entry[ 1 ] );
+		}
+	}
+
+	return plugins;
+}
+
+function getPluginsFromSpy( addSpy ) {
+	return addSpy.args
+		.map( ( arg ) => arg[ 0 ] )
+		// Entries may be kept twice in the plugins map - once as a pluginName => plugin, once as pluginClass => plugin.
+		// Return only pluginClass => plugin entries as these will always represent all plugins.
+		.filter( ( plugin ) => typeof plugin == 'function' );
+}
+
+function getPluginNames( plugins ) {
+	return plugins.map( ( plugin ) => plugin._pluginName );
+}