Przeglądaj źródła

Merge pull request #116 from ckeditor/t/115

Fix: Removed `StandardEditor` class in favor of `DataInterface` and `ElementInterface` mixins. Added `EditorWithUI` interface. Closes #115. Closes #113. Closes https://github.com/ckeditor/ckeditor5/issues/303.

BREAKING CHANGE: `StandardEditor` class is removed. Use `Editor` class with `DataInterface` and `ElementInterface` mixins.
Piotr Jasiun 8 lat temu
rodzic
commit
ad3e8542e0

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

@@ -11,7 +11,7 @@ import KeystrokeHandler from '@ckeditor/ckeditor5-utils/src/keystrokehandler';
 
 /**
  * A keystroke handler for editor editing. Its instance is available
- * in {@link module:core/editor/standardeditor~StandardEditor#keystrokes} so plugins
+ * in {@link module:core/editor/editor~Editor#keystrokes} so plugins
  * can register their keystrokes.
  *
  * E.g. an undo plugin would do this:

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

@@ -20,11 +20,13 @@ import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
 import mix from '@ckeditor/ckeditor5-utils/src/mix';
 
 /**
- * Class representing the base of the editor. It is the API all plugins can expect to get when using editor property.
- * Editors implementation (like Classic Editor or Inline Editor) should extend this class. They can add their own
- * methods and properties.
+ * Class representing the base of the editor. It is the API all plugins can expect to get when using `editor` property.
+ * It should be enough to implement editing part of feature (schema definition, conversion, commands, keystrokes, etc.).
+ * However it does not define editor UI, which is defined in {@link module:core/editor/editorwithui~EditorWithUI}.
  *
- * See also {@link module:core/editor/standardeditor~StandardEditor}.
+ * All editors implementation (like {@link module:editor-classic/classiceditor~ClassicEditor} or
+ * {@link module:editor-inline/inlineeditor~InlineEditor}) should extend this class. They can add their
+ * own methods and properties.
  *
  * @mixes module:utils/observablemixin~ObservableMixin
  */
@@ -98,9 +100,6 @@ export default class Editor {
 		 */
 		this.model = new Model();
 
-		// Creates main root.
-		this.model.document.createRoot();
-
 		/**
 		 * The {@link module:engine/controller/datacontroller~DataController data controller}.
 		 * Used e.g. for setting or retrieving editor data.
@@ -233,12 +232,6 @@ mix( Editor, ObservableMixin );
  */
 
 /**
- * Fired when the editor UI is ready. This event won't be fired if the editor has no UI.
- *
- * @event uiReady
- */
-
-/**
  * Fired when the data loaded to the editor is ready. If a specific editor doesn't load
  * any data initially, this event will be fired right before {@link #event:ready}.
  *
@@ -246,7 +239,7 @@ mix( Editor, ObservableMixin );
  */
 
 /**
- * Fired when {@link #event:pluginsReady plugins}, {@link #event:uiReady UI} and {@link #event:dataReady data} and all additional
+ * Fired when {@link #event:pluginsReady plugins}, and {@link #event:dataReady data} and all additional
  * editor components are ready.
  *
  * Note: This event is most useful for plugin developers. When integrating the editor with your website or

+ 31 - 0
packages/ckeditor5-core/src/editor/editorwithui.jsdoc

@@ -0,0 +1,31 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module core/editor/editorwithui
+ */
+
+/**
+ * Interface defining UI part of the editor. Most editor (like {@link module:editor-classic/classiceditor~ClassicEditor} or
+ * {@link module:editor-inline/inlineeditor~InlineEditor}) implements this interface, however it is not required to do so. Editors with external UI (i.e. Bootstrap based) or headless editor may not implement this interface. Keep it in mind when developing features.
+ *
+ * @interface EditorWithUI
+ */
+
+/**
+ * Editor UI instance.
+ *
+ * @readonly
+ * @member {module:core/editor/editorui~EditorUI} #ui
+ */
+
+/**
+ * Fired when the editor UI is ready.
+ *
+ * Fired after {@link module:core/editor/editor~Editor#event:pluginsReady}, before
+ * {@link module:core/editor/editor~Editor#event:dataReady}.
+ *
+ * @event uiReady
+ */

+ 0 - 151
packages/ckeditor5-core/src/editor/standardeditor.js

@@ -1,151 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/**
- * @module core/editor/standardeditor
- */
-
-import Editor from './editor';
-import isFunction from '@ckeditor/ckeditor5-utils/src/lib/lodash/isFunction';
-
-import getDataFromElement from '@ckeditor/ckeditor5-utils/src/dom/getdatafromelement';
-import setDataInElement from '@ckeditor/ckeditor5-utils/src/dom/setdatainelement';
-
-/**
- * The standard editor class, extending base editor by adding methods needed in the typical
- * editor implementations: editor with a single editable area created based on the DOM element.
- * It provides base API to manage data and to integrate the editor with the form when it is
- * initialized on the textarea element.
- *
- * @extends module:core/editor/editor~Editor
- */
-export default class StandardEditor extends Editor {
-	/**
-	 * Creates a new instance of the standard editor.
-	 *
-	 * @param {HTMLElement} element The DOM element that will be the source
-	 * for the created editor.
-	 * @param {Object} config The editor config.
-	 */
-	constructor( element, config ) {
-		super( config );
-
-		/**
-		 * The element on which the editor has been initialized.
-		 *
-		 * @readonly
-		 * @member {HTMLElement}
-		 */
-		this.element = element;
-
-		/**
-		 * Editor UI instance.
-		 *
-		 * This property is set by more specialized editor constructors. However, it's required
-		 * for plugins to work (their UI-related part will try to interact with editor UI),
-		 * so every editor class which is meant to work with default plugins should set this property.
-		 *
-		 * @readonly
-		 * @member {module:core/editor/editorui~EditorUI} #ui
-		 */
-
-		this._attachToForm();
-	}
-
-	/**
-	 * Sets the data in the editor's main root.
-	 *
-	 * @param {*} data The data to load.
-	 */
-	setData( data ) {
-		this.data.set( data );
-	}
-
-	/**
-	 * Gets the data from the editor's main root.
-	 */
-	getData() {
-		return this.data.get();
-	}
-
-	/**
-	 * Updates the {@link #element editor element}'s content with the data.
-	 */
-	updateEditorElement() {
-		setDataInElement( this.element, this.getData() );
-	}
-
-	/**
-	 * Loads the data from the {@link #element editor element} to the main root.
-	 */
-	loadDataFromEditorElement() {
-		this.setData( getDataFromElement( this.element ) );
-	}
-
-	/**
-	 * Checks if editor is initialized on textarea element that belongs to a form. If yes - updates editor's element
-	 * contents before submitting the form.
-	 *
-	 * @private
-	 */
-	_attachToForm() {
-		const element = this.element;
-
-		// Only when replacing a textarea which is inside of a form element.
-		if ( element && element.tagName.toLowerCase() === 'textarea' && element.form ) {
-			let originalSubmit;
-			const form = element.form;
-			const onSubmit = () => this.updateEditorElement();
-
-			// Replace the original form#submit() to call a custom submit function first.
-			// Check if #submit is a function because the form might have an input named "submit".
-			if ( isFunction( form.submit ) ) {
-				originalSubmit = form.submit;
-
-				form.submit = () => {
-					onSubmit();
-					originalSubmit.apply( form );
-				};
-			}
-
-			// Update the replaced textarea with data before each form#submit event.
-			form.addEventListener( 'submit', onSubmit );
-
-			// Remove the submit listener and revert the original submit method on
-			// editor#destroy.
-			this.on( 'destroy', () => {
-				form.removeEventListener( 'submit', onSubmit );
-
-				if ( originalSubmit ) {
-					form.submit = originalSubmit;
-				}
-			} );
-		}
-	}
-
-	/**
-	 * Creates a standard editor instance.
-	 *
-	 * @param {HTMLElement} element See {@link module:core/editor/standardeditor~StandardEditor}'s param.
-	 * @param {Object} config The editor config. You can find the list of config options in
-	 * {@link module:core/editor/editorconfig~EditorConfig}.
-	 * @returns {Promise} Promise resolved once editor is ready.
-	 * @returns {module:core/editor/standardeditor~StandardEditor} return.editor The editor instance.
-	 */
-	static create( element, config ) {
-		return new Promise( resolve => {
-			const editor = new this( element, config );
-
-			resolve(
-				editor.initPlugins()
-					.then( () => {
-						editor.fire( 'dataReady' );
-						editor.fire( 'ready' );
-					} )
-					.then( () => editor )
-			);
-		} );
-	}
-}

+ 63 - 0
packages/ckeditor5-core/src/editor/utils/attachtoform.js

@@ -0,0 +1,63 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import isFunction from '@ckeditor/ckeditor5-utils/src/lib/lodash/isFunction';
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+
+/**
+ * @module core/editor/utils/attachtoform
+ */
+
+/**
+ * Checks if editor is initialized on textarea element that belongs to a form. If yes - updates editor's element
+ * contents before submitting the form.
+ *
+ * This helper requires {@link module:core/editor/utils/elementapimixin~ElementApi ElementApi interface}.
+ *
+ * @param {module:core/editor/editor~Editor} editor Editor instance.
+ */
+export default function attachToForm( editor ) {
+	if ( !isFunction( editor.updateElement ) ) {
+		/**
+		 * {@link module:core/editor/utils/elementapimixin~ElementApi ElementApi interface} is required.
+		 *
+		 * @error attachtoform-missing-elementapi-interface
+		 */
+		throw new CKEditorError( 'attachtoform-missing-elementapi-interface: ElementApi interface is required.' );
+	}
+
+	const element = editor.element;
+
+	// Only when replacing a textarea which is inside of a form element.
+	if ( element && element.tagName.toLowerCase() === 'textarea' && element.form ) {
+		let originalSubmit;
+		const form = element.form;
+		const onSubmit = () => editor.updateElement();
+
+		// Replace the original form#submit() to call a custom submit function first.
+		// Check if #submit is a function because the form might have an input named "submit".
+		if ( isFunction( form.submit ) ) {
+			originalSubmit = form.submit;
+
+			form.submit = () => {
+				onSubmit();
+				originalSubmit.apply( form );
+			};
+		}
+
+		// Update the replaced textarea with data before each form#submit event.
+		form.addEventListener( 'submit', onSubmit );
+
+		// Remove the submit listener and revert the original submit method on
+		// editor#destroy.
+		editor.on( 'destroy', () => {
+			form.removeEventListener( 'submit', onSubmit );
+
+			if ( originalSubmit ) {
+				form.submit = originalSubmit;
+			}
+		} );
+	}
+}

+ 53 - 0
packages/ckeditor5-core/src/editor/utils/dataapimixin.js

@@ -0,0 +1,53 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module core/editor/utils/dataapimixin
+ */
+
+/**
+ * Implementation of the {@link module:core/editor/utils/dataapimixin~DataApi}.
+ *
+ * @mixin DataApiMixin
+ * @implements module:core/editor/utils/dataapimixin~DataApi
+ */
+const DataApiMixin = {
+	/**
+	 * @inheritDoc
+	 */
+	setData( data ) {
+		this.data.set( data );
+	},
+
+	/**
+	 * @inheritDoc
+	 */
+	getData() {
+		return this.data.get();
+	}
+};
+
+export default DataApiMixin;
+
+/**
+ * Mixin provides methods for setting and getting data to/from editor main root element of the model tree
+ * using {@link module:core/editor/editor~Editor#data data pipeline}.
+ *
+ * @interface DataApi
+ */
+
+/**
+ * Sets the data in the editor's main root.
+ *
+ * @method #setData
+ * @param {String} data Input data.
+ */
+
+/**
+ * Gets the data from the editor's main root.
+ *
+ * @method #getData
+ * @returns {String} Output data.
+ */

+ 60 - 0
packages/ckeditor5-core/src/editor/utils/elementapimixin.js

@@ -0,0 +1,60 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import getDataFromElement from '@ckeditor/ckeditor5-utils/src/dom/getdatafromelement';
+import setDataInElement from '@ckeditor/ckeditor5-utils/src/dom/setdatainelement';
+
+/**
+ * @module core/editor/utils/elementapimixin
+ */
+
+/**
+ * Implementation of the {@link module:core/editor/utils/elementapimixin~ElementApi}.
+ *
+ * @mixin ElementApiMixin
+ * @implements module:core/editor/utils/elementapimixin~ElementApi
+ */
+const ElementApiMixin = {
+	/**
+	 * @inheritDoc
+	 */
+	updateElement() {
+		setDataInElement( this.element, this.data.get() );
+	},
+
+	/**
+	 * @inheritDoc
+	 */
+	loadDataFromElement() {
+		this.data.set( getDataFromElement( this.element ) );
+	}
+};
+
+export default ElementApiMixin;
+
+/**
+ * Mixin provides method for setting and getting data from/to element on which editor has been initialized.
+ *
+ * @interface ElementApi
+ */
+
+/**
+ * The element on which the editor has been initialized.
+ *
+ * @readonly
+ * @member {HTMLElement} #element
+ */
+
+/**
+ * Updates the {@link #element editor element}'s content with the data.
+ *
+ * @method #updateElement
+ */
+
+/**
+ * Loads the data from the {@link #element editor element} to the main root.
+ *
+ * @method #loadDataFromElement
+ */

+ 10 - 0
packages/ckeditor5-core/src/plugin.js

@@ -24,6 +24,16 @@ export default class Plugin {
 		/**
 		 * The editor instance.
 		 *
+		 * Note that most editors implements {@link module:core/editor/editorwithui~EditorWithUI} interface in addition
+		 * to the base {@link module:core/editor/editor~Editor} interface. However, editors with external UI
+		 * (i.e. Bootstrap based) or headless editor may not implement {@link module:core/editor/editorwithui~EditorWithUI}
+		 * interface.
+		 *
+		 * Because of above, to make plugins more universal, it is recommended to split features into:
+		 *  - "Editing" part which use only {@link module:core/editor/editor~Editor} interface,
+		 *  - "UI" part which use both {@link module:core/editor/editor~Editor} interface and
+		 *  {@link module:core/editor/editorwithui~EditorWithUI} interface.
+		 *
 		 * @readonly
 		 * @member {module:core/editor/editor~Editor} #editor
 		 */

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

@@ -5,7 +5,7 @@
 
 /* globals document */
 
-import StandardEditor from '../../src/editor/standardeditor';
+import Editor from '../../src/editor/editor';
 import ClassicTestEditor from '../../tests/_utils/classictesteditor';
 
 import Plugin from '../../src/plugin';
@@ -15,6 +15,10 @@ import ClassicTestEditorUI from '../../tests/_utils/classictesteditorui';
 import BoxedEditorUIView from '@ckeditor/ckeditor5-ui/src/editorui/boxed/boxededitoruiview';
 import InlineEditableUIView from '@ckeditor/ckeditor5-ui/src/editableui/inline/inlineeditableuiview';
 
+import DataApiMixin from '../../src/editor/utils/dataapimixin';
+import ElementApiMixin from '../../src/editor/utils/elementapimixin';
+import RootElement from '@ckeditor/ckeditor5-engine/src/model/rootelement';
+
 import { getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import testUtils from '../../tests/_utils/utils';
 
@@ -32,7 +36,7 @@ describe( 'ClassicTestEditor', () => {
 		it( 'creates an instance of editor', () => {
 			const editor = new ClassicTestEditor( editorElement, { foo: 1 } );
 
-			expect( editor ).to.be.instanceof( StandardEditor );
+			expect( editor ).to.be.instanceof( Editor );
 			expect( editor.config.get( 'foo' ) ).to.equal( 1 );
 			expect( editor.element ).to.equal( editorElement );
 			expect( editor.ui ).to.be.instanceOf( ClassicTestEditorUI );
@@ -48,6 +52,20 @@ describe( 'ClassicTestEditor', () => {
 			expect( editor.ui.view.editableElement.tagName ).to.equal( 'DIV' );
 			expect( editor.ui.view.editableElement ).to.equal( editor.ui.view.editable.element );
 		} );
+
+		it( 'creates main root element', () => {
+			const editor = new ClassicTestEditor( editorElement );
+
+			expect( editor.model.document.getRoot( 'main' ) ).to.instanceof( RootElement );
+		} );
+
+		it( 'mixes DataApiMixin', () => {
+			expect( testUtils.isMixed( ClassicTestEditor, DataApiMixin ) ).to.true;
+		} );
+
+		it( 'mixes ElementApiMixin', () => {
+			expect( testUtils.isMixed( ClassicTestEditor, ElementApiMixin ) ).to.true;
+		} );
 	} );
 
 	describe( 'create()', () => {
@@ -61,7 +79,7 @@ describe( 'ClassicTestEditor', () => {
 				} );
 		} );
 
-		it( 'creates and initilizes the UI', () => {
+		it( 'creates and initializes the UI', () => {
 			return ClassicTestEditor.create( editorElement, { foo: 1 } )
 				.then( editor => {
 					expect( editor.ui ).to.be.instanceOf( ClassicTestEditorUI );
@@ -134,7 +152,7 @@ describe( 'ClassicTestEditor', () => {
 		it( 'destroys UI and calls super.destroy()', () => {
 			return ClassicTestEditor.create( editorElement, { foo: 1 } )
 				.then( editor => {
-					const superSpy = testUtils.sinon.spy( StandardEditor.prototype, 'destroy' );
+					const superSpy = testUtils.sinon.spy( Editor.prototype, 'destroy' );
 					const uiSpy = sinon.spy( editor.ui, 'destroy' );
 
 					return editor.destroy()

+ 15 - 6
packages/ckeditor5-core/tests/_utils-tests/modeltesteditor.js

@@ -4,10 +4,13 @@
  */
 
 import Editor from '../../src/editor/editor';
+import EditingController from '@ckeditor/ckeditor5-engine/src/controller/editingcontroller';
 import ModelTestEditor from '../../tests/_utils/modeltesteditor';
 
 import Plugin from '../../src/plugin';
 import HtmlDataProcessor from '@ckeditor/ckeditor5-engine/src/dataprocessor/htmldataprocessor';
+import DataApiMixin from '../../src/editor/utils/dataapimixin';
+import RootElement from '@ckeditor/ckeditor5-engine/src/model/rootelement';
 
 import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
@@ -25,18 +28,24 @@ describe( 'ModelTestEditor', () => {
 			expect( editor.data.processor ).to.be.instanceof( HtmlDataProcessor );
 		} );
 
-		it( 'creates model root', () => {
+		it( 'should disable editing pipeline', () => {
+			const spy = sinon.spy( EditingController.prototype, 'destroy' );
 			const editor = new ModelTestEditor( { foo: 1 } );
 
-			expect( editor.model.document.getRoot() ).to.have.property( 'name', '$root' );
+			sinon.assert.calledOnce( spy );
+
+			spy.restore();
+			return editor.destroy();
 		} );
 
-		it( 'should disable editing pipeline and clear main root', () => {
-			const editor = new ModelTestEditor( { foo: 1 } );
+		it( 'creates main root element', () => {
+			const editor = new ModelTestEditor();
 
-			editor.model.document.createRoot( 'second', 'second' );
+			expect( editor.model.document.getRoot( 'main' ) ).to.instanceof( RootElement );
+		} );
 
-			expect( Array.from( editor.editing.view.roots ) ).to.length( 0 );
+		it( 'mixes DataApiMixin', () => {
+			expect( testUtils.isMixed( ModelTestEditor, DataApiMixin ) ).to.true;
 		} );
 	} );
 

+ 34 - 0
packages/ckeditor5-core/tests/_utils-tests/utils.js

@@ -4,6 +4,7 @@
  */
 
 import testUtils from '../_utils/utils';
+import mix from '@ckeditor/ckeditor5-utils/src/mix';
 
 testUtils.createSinonSandbox();
 
@@ -45,4 +46,37 @@ describe( 'utils', () => {
 			expect( assertionGreen.called ).to.equal( false );
 		} );
 	} );
+
+	describe( 'isMixed()', () => {
+		let mixin, CustomClass;
+
+		beforeEach( () => {
+			CustomClass = class {};
+			mixin = {
+				foo() {
+					return 'bar';
+				}
+			};
+		} );
+
+		it( 'should return true when given mixin is mixed to target class', () => {
+			mix( CustomClass, mixin );
+
+			expect( testUtils.isMixed( CustomClass, mixin ) ).to.true;
+		} );
+
+		it( 'should return false when given mixin is not mixed to target class', () => {
+			expect( testUtils.isMixed( CustomClass, mixin ) ).to.false;
+		} );
+
+		it( 'should return false when class has mixin like interface', () => {
+			CustomClass = class {
+				foo() {
+					return 'biz';
+				}
+			};
+
+			expect( testUtils.isMixed( CustomClass, mixin ) ).to.false;
+		} );
+	} );
 } );

+ 14 - 2
packages/ckeditor5-core/tests/_utils-tests/virtualtesteditor.js

@@ -3,11 +3,13 @@
  * For licensing, see LICENSE.md.
  */
 
-import StandardEditor from '../../src/editor/standardeditor';
+import Editor from '../../src/editor/editor';
 import VirtualTestEditor from '../../tests/_utils/virtualtesteditor';
 
 import Plugin from '../../src/plugin';
 import HtmlDataProcessor from '@ckeditor/ckeditor5-engine/src/dataprocessor/htmldataprocessor';
+import DataApiMixin from '../../src/editor/utils/dataapimixin';
+import RootElement from '@ckeditor/ckeditor5-engine/src/model/rootelement';
 
 import testUtils from '../../tests/_utils/utils';
 
@@ -18,10 +20,20 @@ describe( 'VirtualTestEditor', () => {
 		it( 'creates an instance of editor', () => {
 			const editor = new VirtualTestEditor( { foo: 1 } );
 
-			expect( editor ).to.be.instanceof( StandardEditor );
+			expect( editor ).to.be.instanceof( Editor );
 			expect( editor.data.processor ).to.be.instanceof( HtmlDataProcessor );
 			expect( editor.config.get( 'foo' ) ).to.equal( 1 );
 		} );
+
+		it( 'creates main root element', () => {
+			const editor = new VirtualTestEditor();
+
+			expect( editor.model.document.getRoot( 'main' ) ).to.instanceof( RootElement );
+		} );
+
+		it( 'mixes DataApiMixin', () => {
+			expect( testUtils.isMixed( VirtualTestEditor, DataApiMixin ) ).to.true;
+		} );
 	} );
 
 	describe( 'create', () => {

+ 19 - 5
packages/ckeditor5-core/tests/_utils/classictesteditor.js

@@ -3,26 +3,33 @@
  * For licensing, see LICENSE.md.
  */
 
-import StandardEditor from '../../src/editor/standardeditor';
+import Editor from '../../src/editor/editor';
+import ElementApiMixin from '../../src/editor/utils/elementapimixin';
+import DataApiMixin from '../../src/editor/utils/dataapimixin';
 import HtmlDataProcessor from '@ckeditor/ckeditor5-engine/src/dataprocessor/htmldataprocessor';
 import ClassicTestEditorUI from './classictesteditorui';
 import BoxedEditorUIView from '@ckeditor/ckeditor5-ui/src/editorui/boxed/boxededitoruiview';
 import ElementReplacer from '@ckeditor/ckeditor5-utils/src/elementreplacer';
 import InlineEditableUIView from '@ckeditor/ckeditor5-ui/src/editableui/inline/inlineeditableuiview';
+import mix from '@ckeditor/ckeditor5-utils/src/mix';
 
 /**
  * A simplified classic editor. Useful for testing features.
  *
  * @memberOf tests.core._utils
- * @extends core.editor.StandardEditor
+ * @extends core.editor.Editor
  */
-export default class ClassicTestEditor extends StandardEditor {
+export default class ClassicTestEditor extends Editor {
 	/**
 	 * @inheritDoc
 	 */
 	constructor( element, config ) {
-		super( element, config );
+		super( config );
 
+		// The element on which the editor has been initialized.
+		this.element = element;
+
+		// Use the HTML data processor in this editor.
 		this.data.processor = new HtmlDataProcessor();
 
 		this.ui = new ClassicTestEditorUI( this, new BoxedEditorUIView( this.locale ) );
@@ -32,7 +39,11 @@ export default class ClassicTestEditor extends StandardEditor {
 		this.ui.view.main.add( this.ui.view.editable );
 		this.ui.view.editableElement = this.ui.view.editable.element;
 
+		// A helper to easily replace the editor#element with editor.editable#element.
 		this._elementReplacer = new ElementReplacer();
+
+		// Create the ("main") root element of the model tree.
+		this.model.document.createRoot();
 	}
 
 	/**
@@ -60,7 +71,7 @@ export default class ClassicTestEditor extends StandardEditor {
 						editor.fire( 'uiReady' );
 					} )
 					.then( () => editor.editing.view.attachDomRoot( editor.ui.view.editableElement ) )
-					.then( () => editor.loadDataFromEditorElement() )
+					.then( () => editor.loadDataFromElement() )
 					.then( () => {
 						editor.fire( 'dataReady' );
 						editor.fire( 'ready' );
@@ -70,3 +81,6 @@ export default class ClassicTestEditor extends StandardEditor {
 		} );
 	}
 }
+
+mix( ClassicTestEditor, DataApiMixin );
+mix( ClassicTestEditor, ElementApiMixin );

+ 9 - 18
packages/ckeditor5-core/tests/_utils/modeltesteditor.js

@@ -4,7 +4,9 @@
  */
 
 import Editor from '../../src/editor/editor';
+import DataApiMixin from '../../src/editor/utils/dataapimixin';
 import HtmlDataProcessor from '@ckeditor/ckeditor5-engine/src/dataprocessor/htmldataprocessor';
+import mix from '@ckeditor/ckeditor5-utils/src/mix';
 
 /**
  * A simple editor implementation with a functional model part of the engine (the document).
@@ -18,33 +20,20 @@ export default class ModelTestEditor extends Editor {
 	constructor( config ) {
 		super( config );
 
+		// Use the HTML data processor in this editor.
 		this.data.processor = new HtmlDataProcessor();
 
-		// Disable editing pipeline for model editor.
+		// Disable editing pipeline.
 		this.editing.destroy();
-		this.editing.view.roots.clear();
-	}
 
-	/**
-	 * Sets the data in the editor's main root.
-	 *
-	 * @param {*} data The data to load.
-	 */
-	setData( data ) {
-		this.data.set( data );
-	}
-
-	/**
-	 * Gets the data from the editor's main root.
-	 */
-	getData() {
-		return this.data.get();
+		// Create the ("main") root element of the model tree.
+		this.model.document.createRoot();
 	}
 
 	/**
 	 * Creates a virtual, element-less editor instance.
 	 *
-	 * @param {Object} config See {@link core.editor.StandardEditor}'s param.
+	 * @param {Object} config See {@link core.editor.Editor}'s param.
 	 * @returns {Promise} Promise resolved once editor is ready.
 	 * @returns {core.editor.VirtualTestEditor} return.editor The editor instance.
 	 */
@@ -63,3 +52,5 @@ export default class ModelTestEditor extends Editor {
 		} );
 	}
 }
+
+mix( ModelTestEditor, DataApiMixin );

+ 21 - 0
packages/ckeditor5-core/tests/_utils/utils.js

@@ -73,6 +73,27 @@ const utils = {
 		}
 
 		throw new Error( errors.join( '\n\n' ) );
+	},
+
+	/**
+	 * Checks if given mixin i mixed to given class using {@link module:utils/mix mix} util.
+	 *
+	 * @param {Function} targetClass Class to check.
+	 * @param {Object} mixin Mixin to check.
+	 * @returns {Boolean} `True` when mixin is mixed to to target class, `false` otherwise.
+	 */
+	isMixed( targetClass, mixin ) {
+		let isValid = true;
+
+		for ( const property in mixin ) {
+			if ( mixin.hasOwnProperty( property ) ) {
+				if ( targetClass.prototype[ property ] !== mixin[ property ] ) {
+					isValid = false;
+				}
+			}
+		}
+
+		return isValid;
 	}
 };
 

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

@@ -3,8 +3,10 @@
  * For licensing, see LICENSE.md.
  */
 
-import StandardEditor from '../../src/editor/standardeditor';
+import Editor from '../../src/editor/editor';
+import DataApiMixin from '../../src/editor/utils/dataapimixin';
 import HtmlDataProcessor from '@ckeditor/ckeditor5-engine/src/dataprocessor/htmldataprocessor';
+import mix from '@ckeditor/ckeditor5-utils/src/mix';
 
 /**
  * A simple editor implementation useful for testing the engine part of the features.
@@ -14,17 +16,21 @@ import HtmlDataProcessor from '@ckeditor/ckeditor5-engine/src/dataprocessor/html
  *
  * @memberOf tests.core._utils
  */
-export default class VirtualTestEditor extends StandardEditor {
+export default class VirtualTestEditor extends Editor {
 	constructor( config ) {
-		super( null, config );
+		super( config );
 
+		// Use the HTML data processor in this editor.
 		this.data.processor = new HtmlDataProcessor();
+
+		// Create the ("main") root element of the model tree.
+		this.model.document.createRoot();
 	}
 
 	/**
 	 * Creates a virtual, element-less editor instance.
 	 *
-	 * @param {Object} config See {@link core.editor.StandardEditor}'s param.
+	 * @param {Object} config See {@link core.editor.Editor}'s param.
 	 * @returns {Promise} Promise resolved once editor is ready.
 	 * @returns {core.editor.VirtualTestEditor} return.editor The editor instance.
 	 */
@@ -43,3 +49,5 @@ export default class VirtualTestEditor extends StandardEditor {
 		} );
 	}
 }
+
+mix( VirtualTestEditor, DataApiMixin );

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

@@ -150,12 +150,6 @@ describe( 'Editor', () => {
 			expect( editor.editing.view.isReadOnly ).to.true;
 		} );
 
-		it( 'should create main root element', () => {
-			const editor = new Editor();
-
-			expect( editor.model.document.getRoot( 'main' ) ).to.ok;
-		} );
-
 		it( 'should activate #keystrokes', () => {
 			const spy = sinon.spy( EditingKeystrokeHandler.prototype, 'listenTo' );
 			const editor = new Editor();

+ 0 - 330
packages/ckeditor5-core/tests/editor/standardeditor.js

@@ -1,330 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/* globals document, Event */
-
-import Editor from '../../src/editor/editor';
-import StandardEditor from '../../src/editor/standardeditor';
-import HtmlDataProcessor from '@ckeditor/ckeditor5-engine/src/dataprocessor/htmldataprocessor';
-import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
-
-import Plugin from '../../src/plugin';
-
-describe( 'StandardEditor', () => {
-	let editorElement;
-
-	beforeEach( () => {
-		editorElement = document.createElement( 'div' );
-		document.body.appendChild( editorElement );
-	} );
-
-	describe( 'constructor()', () => {
-		it( 'sets all properties', () => {
-			const editor = new StandardEditor( editorElement, { foo: 1 } );
-
-			expect( editor ).to.have.property( 'element', editorElement );
-		} );
-
-		it( 'sets config', () => {
-			const editor = new StandardEditor( editorElement, { foo: 1 } );
-
-			expect( editor.config.get( 'foo' ) ).to.equal( 1 );
-		} );
-	} );
-
-	describe( 'destroy()', () => {
-		it( 'returns a Promise', () => {
-			const editor = new StandardEditor( editorElement, { foo: 1 } );
-
-			expect( editor.destroy() ).to.be.an.instanceof( Promise );
-		} );
-
-		it( 'destroys the parent', () => {
-			const editor = new StandardEditor( editorElement, { foo: 1 } );
-			const spy = sinon.spy( Editor.prototype, 'destroy' );
-
-			sinon.assert.notCalled( spy );
-
-			return editor.destroy()
-				.then( () => {
-					sinon.assert.calledOnce( spy );
-				} );
-		} );
-	} );
-
-	describe( 'create()', () => {
-		it( 'initializes editor with plugins and config', () => {
-			class PluginFoo extends Plugin {}
-
-			return StandardEditor.create( editorElement, { foo: 1, plugins: [ PluginFoo ] } )
-				.then( editor => {
-					expect( editor ).to.be.instanceof( StandardEditor );
-
-					expect( editor.config.get( 'foo' ) ).to.equal( 1 );
-					expect( editor ).to.have.property( 'element', editorElement );
-
-					expect( editor.plugins.get( PluginFoo ) ).to.be.instanceof( PluginFoo );
-				} );
-		} );
-
-		it( 'fires all events in the right order', () => {
-			const fired = [];
-
-			function spy( evt ) {
-				fired.push( evt.name );
-			}
-
-			class EventWatcher extends Plugin {
-				init() {
-					this.editor.on( 'pluginsReady', spy );
-					this.editor.on( 'dataReady', spy );
-					this.editor.on( 'ready', spy );
-				}
-			}
-
-			return StandardEditor.create( editorElement, { plugins: [ EventWatcher ] } )
-				.then( () => {
-					expect( fired ).to.deep.equal( [ 'pluginsReady', 'dataReady', 'ready' ] );
-				} );
-		} );
-	} );
-
-	describe( 'setData()', () => {
-		let editor;
-
-		beforeEach( () => {
-			return StandardEditor.create( editorElement )
-				.then( newEditor => {
-					editor = newEditor;
-
-					editor.data.processor = new HtmlDataProcessor();
-
-					editor.model.schema.extend( '$text', { allowIn: '$root' } );
-				} );
-		} );
-
-		it( 'should set data of the first root', () => {
-			editor.model.document.createRoot( '$root', 'secondRoot' );
-
-			editor.setData( 'foo' );
-
-			expect( getData( editor.model, { rootName: 'main', withoutSelection: true } ) ).to.equal( 'foo' );
-		} );
-	} );
-
-	describe( 'getData()', () => {
-		let editor;
-
-		beforeEach( () => {
-			return StandardEditor.create( editorElement )
-				.then( newEditor => {
-					editor = newEditor;
-
-					editor.data.processor = new HtmlDataProcessor();
-
-					editor.model.schema.extend( '$text', { allowIn: '$root' } );
-				} );
-		} );
-
-		it( 'should get data of the first root', () => {
-			editor.model.document.createRoot( '$root', 'secondRoot' );
-
-			setData( editor.model, 'foo' );
-
-			expect( editor.getData() ).to.equal( 'foo' );
-		} );
-	} );
-
-	describe( 'updateEditorElement()', () => {
-		it( 'sets data to editor element', () => {
-			const editor = new StandardEditor( editorElement );
-
-			editor.data.get = () => '<p>foo</p>';
-
-			editor.updateEditorElement();
-
-			expect( editorElement.innerHTML ).to.equal( '<p>foo</p>' );
-		} );
-	} );
-
-	describe( 'loadDataFromEditorElement()', () => {
-		it( 'sets data to editor element', () => {
-			const editor = new StandardEditor( editorElement );
-
-			sinon.stub( editor.data, 'set' );
-			editorElement.innerHTML = '<p>foo</p>';
-
-			editor.loadDataFromEditorElement();
-
-			expect( editor.data.set.calledWithExactly( '<p>foo</p>' ) ).to.be.true;
-		} );
-	} );
-
-	describe( 'attaching to a form', () => {
-		let editor, form, textarea, submitStub;
-
-		beforeEach( () => {
-			form = document.createElement( 'form' );
-			textarea = document.createElement( 'textarea' );
-			form.appendChild( textarea );
-			document.body.appendChild( form );
-			submitStub = sinon.stub( form, 'submit' );
-
-			// Prevents page realods in Firefox ;|
-			form.addEventListener( 'submit', evt => {
-				evt.preventDefault();
-			} );
-		} );
-
-		afterEach( () => {
-			submitStub.restore();
-			form.remove();
-		} );
-
-		it( 'should update editor#element after calling the submit() method', () => {
-			return createEditor( textarea )
-				.then( editor => {
-					expect( textarea.value ).to.equal( '' );
-
-					// Submit method is replaced by our implementation.
-					expect( form.submit ).to.not.equal( submitStub );
-					form.submit();
-
-					expect( textarea.value ).to.equal( '<p>foo</p>' );
-					sinon.assert.calledOnce( submitStub );
-
-					// Check if original function was called in correct context.
-					sinon.assert.calledOn( submitStub, form );
-
-					return editor.destroy();
-				} );
-		} );
-
-		it( 'should update editor#element after the "submit" event', () => {
-			return createEditor( textarea )
-				.then( editor => {
-					expect( textarea.value ).to.equal( '' );
-
-					form.dispatchEvent( new Event( 'submit', {
-						// We need to be able to do preventDefault() to prevent page reloads in Firefox.
-						cancelable: true
-					} ) );
-
-					expect( textarea.value ).to.equal( '<p>foo</p>' );
-
-					return editor.destroy();
-				} );
-		} );
-
-		it( 'should not update editor#element if it is not a textarea in a form', () => {
-			const element = document.createElement( 'div' );
-			form.appendChild( element );
-
-			return createEditor( element )
-				.then( editor => {
-					expect( textarea.value ).to.equal( '' );
-
-					// Submit method is not replaced by our implementation.
-					expect( form.submit ).to.equal( submitStub );
-					form.submit();
-
-					expect( textarea.value ).to.equal( '' );
-
-					return editor.destroy();
-				} )
-				.then( () => {
-					element.remove();
-				} );
-		} );
-
-		it( 'should not update editor#element not belonging to a form', () => {
-			const textarea = document.createElement( 'textarea' );
-			document.body.appendChild( textarea );
-
-			return createEditor( textarea )
-				.then( editor => {
-					expect( textarea.value ).to.equal( '' );
-
-					// Submit method is not replaced by our implementation.
-					expect( form.submit ).to.equal( submitStub );
-					form.submit();
-
-					expect( textarea.value ).to.equal( '' );
-
-					return editor.destroy();
-				} )
-				.then( () => {
-					textarea.remove();
-				} );
-		} );
-
-		it( 'should not update editor#element after destruction of the editor - form.submit()', () => {
-			return createEditor( textarea )
-				.then( editor => editor.destroy() )
-				.then( () => {
-					expect( textarea.value ).to.equal( '' );
-
-					// Submit method is no longer replaced by our implementation.
-					expect( form.submit ).to.equal( submitStub );
-					form.submit();
-
-					expect( textarea.value ).to.equal( '' );
-				} );
-		} );
-
-		it( 'should not update the editor#element after destruction of the editor - "submit" event', () => {
-			return createEditor( textarea )
-				.then( editor => editor.destroy() )
-				.then( () => {
-					expect( textarea.value ).to.equal( '' );
-
-					form.dispatchEvent( new Event( 'submit', {
-						// We need to be able to do preventDefault() to prevent page reloads in Firefox.
-						cancelable: true
-					} ) );
-
-					expect( textarea.value ).to.equal( '' );
-				} );
-		} );
-
-		it( 'should not replace submit() method when one of the elements in a form is named "submit"', () => {
-			// Restore stub since we want to mask submit function with input with name="submit".
-			submitStub.restore();
-
-			const input = document.createElement( 'input' );
-			input.setAttribute( 'name', 'submit' );
-			form.appendChild( input );
-
-			return createEditor( textarea )
-				.then( () => {
-					expect( form.submit ).to.equal( input );
-					expect( textarea.value ).to.equal( '' );
-
-					form.dispatchEvent( new Event( 'submit', {
-						// We need to be able to do preventDefault() to prevent page reloads in Firefox.
-						cancelable: true
-					} ) );
-
-					expect( textarea.value ).to.equal( '<p>foo</p>' );
-
-					return editor.destroy();
-				} )
-				.then( () => {
-					expect( form.submit ).to.equal( input );
-					input.remove();
-				} );
-		} );
-
-		function createEditor( element ) {
-			return StandardEditor.create( element )
-				.then( newEditor => {
-					editor = newEditor;
-					editor.data.get = () => '<p>foo</p>';
-
-					return editor;
-				} );
-		}
-	} );
-} );

+ 176 - 0
packages/ckeditor5-core/tests/editor/utils/attachtoform.js

@@ -0,0 +1,176 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import attachToForm from '../../../src/editor/utils/attachtoform';
+import ElementApiMixin from '../../../src/editor/utils/elementapimixin';
+import Editor from '../../../src/editor/editor';
+import HtmlDataProcessor from '@ckeditor/ckeditor5-engine/src/dataprocessor/htmldataprocessor';
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+import mix from '@ckeditor/ckeditor5-utils/src/mix';
+
+/* global document, Event */
+
+describe( 'attachToForm()', () => {
+	let editor, form, textarea, submitStub;
+
+	beforeEach( () => {
+		form = document.createElement( 'form' );
+		textarea = document.createElement( 'textarea' );
+		form.appendChild( textarea );
+		document.body.appendChild( form );
+		submitStub = sinon.stub( form, 'submit' );
+
+		// Prevents page realods in Firefox ;|
+		form.addEventListener( 'submit', evt => {
+			evt.preventDefault();
+		} );
+
+		class CustomEditor extends Editor {}
+		mix( CustomEditor, ElementApiMixin );
+
+		editor = new CustomEditor();
+		editor.data.processor = new HtmlDataProcessor();
+		editor.model.document.createRoot();
+		editor.model.schema.extend( '$text', { allowIn: '$root' } );
+
+		editor.data.set( 'foo bar' );
+	} );
+
+	afterEach( () => {
+		submitStub.restore();
+		form.remove();
+
+		return editor.destroy();
+	} );
+
+	it( 'should throw an error when is used with editor without `ElementApiMixin`', () => {
+		expect( () => {
+			attachToForm( new Editor() );
+		} ).to.throw( CKEditorError, /^attachtoform-missing-elementapi-interface/ );
+	} );
+
+	it( 'should update editor#element after the "submit" event', () => {
+		editor.element = textarea;
+		attachToForm( editor );
+
+		expect( textarea.value ).to.equal( '' );
+
+		form.dispatchEvent( new Event( 'submit', {
+			// We need to be able to do preventDefault() to prevent page reloads in Firefox.
+			cancelable: true
+		} ) );
+
+		expect( textarea.value ).to.equal( 'foo bar' );
+	} );
+
+	it( 'should update editor#element after calling the submit() method', () => {
+		editor.element = textarea;
+		attachToForm( editor );
+
+		expect( textarea.value ).to.equal( '' );
+
+		// Submit method is replaced by our implementation.
+		expect( form.submit ).to.not.equal( submitStub );
+		form.submit();
+
+		expect( textarea.value ).to.equal( 'foo bar' );
+		sinon.assert.calledOnce( submitStub );
+
+		// Check if original function was called in correct context.
+		sinon.assert.calledOn( submitStub, form );
+	} );
+
+	it( 'should not update editor#element if it is not a textarea in a form', () => {
+		const element = document.createElement( 'div' );
+		form.appendChild( element );
+
+		editor.element = element;
+		attachToForm( editor );
+
+		expect( textarea.value ).to.equal( '' );
+
+		// Submit method is not replaced by our implementation.
+		expect( form.submit ).to.equal( submitStub );
+		form.submit();
+
+		expect( textarea.value ).to.equal( '' );
+	} );
+
+	it( 'should not update editor#element not belonging to a form', () => {
+		const standaloneTextarea = document.createElement( 'textarea' );
+		document.body.appendChild( standaloneTextarea );
+
+		editor.element = standaloneTextarea;
+		attachToForm( editor );
+
+		expect( standaloneTextarea.value ).to.equal( '' );
+
+		// Submit method is not replaced by our implementation.
+		expect( form.submit ).to.equal( submitStub );
+		form.submit();
+
+		expect( standaloneTextarea.value ).to.equal( '' );
+
+		standaloneTextarea.remove();
+	} );
+
+	it( 'should not update editor#element after destruction of the editor - form.submit()', () => {
+		editor.element = textarea;
+		attachToForm( editor );
+
+		expect( textarea.value ).to.equal( '' );
+
+		return editor.destroy().then( () => {
+			// Submit method is no longer replaced by our implementation.
+			expect( form.submit ).to.equal( submitStub );
+			form.submit();
+
+			expect( textarea.value ).to.equal( '' );
+		} );
+	} );
+
+	it( 'should not update the editor#element after destruction of the editor - "submit" event', () => {
+		editor.element = textarea;
+		attachToForm( editor );
+
+		expect( textarea.value ).to.equal( '' );
+
+		return editor.destroy().then( () => {
+			form.dispatchEvent( new Event( 'submit', {
+				// We need to be able to do preventDefault() to prevent page reloads in Firefox.
+				cancelable: true
+			} ) );
+
+			expect( textarea.value ).to.equal( '' );
+		} );
+	} );
+
+	it( 'should not replace submit() method when one of the elements in a form is named "submit"', () => {
+		// Restore stub since we want to mask submit function with input with name="submit".
+		submitStub.restore();
+
+		const input = document.createElement( 'input' );
+		input.setAttribute( 'name', 'submit' );
+		form.appendChild( input );
+
+		editor.element = textarea;
+		attachToForm( editor );
+
+		expect( form.submit ).to.equal( input );
+		expect( textarea.value ).to.equal( '' );
+
+		form.dispatchEvent( new Event( 'submit', {
+			// We need to be able to do preventDefault() to prevent page reloads in Firefox.
+			cancelable: true
+		} ) );
+
+		expect( textarea.value ).to.equal( 'foo bar' );
+
+		return editor.destroy().then( () => {
+			expect( form.submit ).to.equal( input );
+			input.remove();
+		} );
+	} );
+} );

+ 53 - 0
packages/ckeditor5-core/tests/editor/utils/dataapimixin.js

@@ -0,0 +1,53 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import DataApiMixin from '../../../src/editor/utils/dataapimixin';
+import Editor from '../../../src/editor/editor';
+import HtmlDataProcessor from '@ckeditor/ckeditor5-engine/src/dataprocessor/htmldataprocessor';
+import mix from '@ckeditor/ckeditor5-utils/src/mix';
+import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+
+describe( 'DataApiMixin', () => {
+	let editor;
+
+	beforeEach( () => {
+		class CustomEditor extends Editor {}
+		mix( CustomEditor, DataApiMixin );
+
+		editor = new CustomEditor();
+		editor.data.processor = new HtmlDataProcessor();
+		editor.model.document.createRoot( '$root', 'main' );
+		editor.model.document.createRoot( '$root', 'secondRoot' );
+		editor.model.schema.extend( '$text', { allowIn: '$root' } );
+	} );
+
+	afterEach( () => {
+		editor.destroy();
+	} );
+
+	describe( 'setData()', () => {
+		it( 'should be added to editor interface', () => {
+			expect( editor ).have.property( 'setData' ).to.be.a( 'function' );
+		} );
+
+		it( 'should set data of the first root', () => {
+			editor.setData( 'foo' );
+
+			expect( getData( editor.model, { rootName: 'main', withoutSelection: true } ) ).to.equal( 'foo' );
+		} );
+	} );
+
+	describe( 'getData()', () => {
+		it( 'should be added to editor interface', () => {
+			expect( editor ).have.property( 'getData' ).to.be.a( 'function' );
+		} );
+
+		it( 'should get data of the first root', () => {
+			setData( editor.model, 'foo' );
+
+			expect( editor.getData() ).to.equal( 'foo' );
+		} );
+	} );
+} );

+ 64 - 0
packages/ckeditor5-core/tests/editor/utils/elementapimixin.js

@@ -0,0 +1,64 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* global document */
+
+import ElementApiMixin from '../../../src/editor/utils/elementapimixin';
+import Editor from '../../../src/editor/editor';
+import HtmlDataProcessor from '@ckeditor/ckeditor5-engine/src/dataprocessor/htmldataprocessor';
+import mix from '@ckeditor/ckeditor5-utils/src/mix';
+
+describe( 'ElementApiMixin', () => {
+	let editor;
+
+	beforeEach( () => {
+		class CustomEditor extends Editor {}
+		mix( CustomEditor, ElementApiMixin );
+
+		editor = new CustomEditor();
+		editor.data.processor = new HtmlDataProcessor();
+		editor.model.document.createRoot();
+		editor.model.schema.extend( '$text', { allowIn: '$root' } );
+	} );
+
+	afterEach( () => {
+		editor.destroy();
+	} );
+
+	describe( 'loadDataFromElement()', () => {
+		it( 'should be added to editor interface', () => {
+			expect( editor ).have.property( 'loadDataFromElement' ).to.be.a( 'function' );
+		} );
+
+		it( 'sets data to editor element', () => {
+			const editorElement = document.createElement( 'div' );
+
+			editor.element = editorElement;
+			editorElement.innerHTML = 'foo bar';
+
+			editor.loadDataFromElement();
+
+			expect( editorElement.innerHTML ).to.equal( 'foo bar' );
+		} );
+	} );
+
+	describe( 'updateEditorElement()', () => {
+		it( 'should be added to editor interface', () => {
+			expect( editor ).have.property( 'updateElement' ).to.be.a( 'function' );
+		} );
+
+		it( 'sets data to editor element', () => {
+			const editorElement = document.createElement( 'div' );
+
+			editor.data.set( 'foo bar' );
+
+			editor.element = editorElement;
+
+			editor.updateElement();
+
+			expect( editorElement.innerHTML ).to.equal( 'foo bar' );
+		} );
+	} );
+} );