浏览代码

Merge pull request #2 from ckeditor/t/1

Feature: Introduced Inline Editor. Closes: #1.
Aleksander Nowodzinski 8 年之前
父节点
当前提交
1b79626435

+ 7 - 0
packages/ckeditor5-editor-inline/package.json

@@ -4,9 +4,16 @@
   "description": "Inline editor for CKEditor 5.",
   "keywords": [],
   "dependencies": {
+    "@ckeditor/ckeditor5-core": "*",
+    "@ckeditor/ckeditor5-engine": "*",
+    "@ckeditor/ckeditor5-ui": "*",
+    "@ckeditor/ckeditor5-utils": "*"
   },
   "devDependencies": {
+    "@ckeditor/ckeditor5-basic-styles": "*",
     "@ckeditor/ckeditor5-dev-lint": "^2.0.0",
+    "@ckeditor/ckeditor5-paragraph": "*",
+    "@ckeditor/ckeditor5-presets": "*",
     "gulp": "^3.9.0",
     "guppy-pre-commit": "^0.4.0"
   },

+ 87 - 0
packages/ckeditor5-editor-inline/src/inline.js

@@ -0,0 +1,87 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module editor-inline/inline
+ */
+
+import StandardEditor from '@ckeditor/ckeditor5-core/src/editor/standardeditor';
+import HtmlDataProcessor from '@ckeditor/ckeditor5-engine/src/dataprocessor/htmldataprocessor';
+import InlineEditorUI from './inlineeditorui';
+import InlineEditorUIView from './inlineeditoruiview';
+
+import '../theme/theme.scss';
+
+/**
+ * Inline editor. Uses an inline editable and a floating toolbar.
+ *
+ * @extends module:core/editor/standardeditor~StandardEditor
+ */
+export default class InlineEditor extends StandardEditor {
+	/**
+	 * Creates an instance of the inline editor.
+	 *
+	 * @param {HTMLElement} element The DOM element that will be the source for the created editor.
+	 * @param {Object} config The editor configuration.
+	 */
+	constructor( element, config ) {
+		super( element, config );
+
+		this.document.createRoot();
+		this.data.processor = new HtmlDataProcessor();
+		this.ui = new InlineEditorUI( this, new InlineEditorUIView( this.locale, element ) );
+	}
+
+	/**
+	 * Destroys the editor instance, releasing all resources used by it.
+	 *
+	 * Updates the original editor element with the data.
+	 *
+	 * @returns {Promise}
+	 */
+	destroy() {
+		this.updateEditorElement();
+
+		return this.ui.destroy()
+			.then( () => super.destroy() );
+	}
+
+	/**
+	 * Creates an inline editor instance.
+	 *
+	 *		InlineEditor.create( document.querySelector( '#editor' ), {
+	 *			plugins: [ Delete, Enter, Typing, Paragraph, Undo, Bold, Italic ],
+	 *			toolbar: [ 'bold', 'italic', 'undo', 'redo' ]
+	 *		} )
+	 *		.then( editor => {
+	 *			console.log( 'Editor was initialized', editor );
+	 *		} )
+	 *		.catch( err => {
+	 *			console.error( err.stack );
+	 *		} );
+	 *
+	 * @param {HTMLElement} element See {@link module:editor-inline/inline~InlineEditor#constructor}'s parameters.
+	 * @param {Object} config See {@link module:editor-inline/inline~InlineEditor#constructor}'s parameters.
+	 * @returns {Promise} A promise resolved once the 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 InlineEditor( element, config );
+
+			resolve(
+				editor.initPlugins()
+					.then( () => editor.ui.init() )
+					.then( () => editor.fire( 'uiReady' ) )
+					.then( () => editor.loadDataFromEditorElement() )
+					.then( () => {
+						editor.fire( 'dataReady' );
+						editor.fire( 'ready' );
+					} )
+					.then( () => editor )
+			);
+		} );
+	}
+}

+ 93 - 0
packages/ckeditor5-editor-inline/src/inlineeditorui.js

@@ -0,0 +1,93 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module editor-inline/inlineeditorui
+ */
+
+import ComponentFactory from '@ckeditor/ckeditor5-ui/src/componentfactory';
+import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker';
+import enableToolbarKeyboardFocus from '@ckeditor/ckeditor5-ui/src/toolbar/enabletoolbarkeyboardfocus';
+
+/**
+ * The inline editor UI class.
+ *
+ * @implements module:core/editor/editorui~EditorUI
+ */
+export default class InlineEditorUI {
+	/**
+	 * Creates an instance of the editor UI class.
+	 *
+	 * @param {module:core/editor/editor~Editor} editor The editor instance.
+	 * @param {module:ui/editorui/editoruiview~EditorUIView} view View of the ui.
+	 */
+	constructor( editor, view ) {
+		/**
+		 * @inheritDoc
+		 */
+		this.editor = editor;
+
+		/**
+		 * @inheritDoc
+		 */
+		this.view = view;
+
+		/**
+		 * @inheritDoc
+		 */
+		this.componentFactory = new ComponentFactory( editor );
+
+		/**
+		 * @inheritDoc
+		 */
+		this.focusTracker = new FocusTracker();
+
+		// Set–up the view#panel.
+		view.panel.bind( 'isActive' ).to( this.focusTracker, 'isFocused' );
+		view.panel.targetElement = view.editableElement;
+
+		// Setup the editable.
+		const editingRoot = editor.editing.createRoot( view.editableElement );
+		view.editable.bind( 'isReadOnly' ).to( editingRoot );
+
+		// Bind to focusTracker instead of editor.editing.view because otherwise
+		// focused editable styles disappear when view#toolbar is focused.
+		view.editable.bind( 'isFocused' ).to( this.focusTracker );
+		view.editable.name = editingRoot.rootName;
+
+		this.focusTracker.add( view.editableElement );
+	}
+
+	/**
+	 * Initializes the UI.
+	 *
+	 * @returns {Promise} A Promise resolved when the initialization process is finished.
+	 */
+	init() {
+		const editor = this.editor;
+
+		return this.view.init()
+			.then( () => {
+				return this.view.toolbar.fillFromConfig( editor.config.get( 'toolbar' ), this.componentFactory );
+			} )
+			.then( () => {
+				enableToolbarKeyboardFocus( {
+					origin: editor.editing.view,
+					originFocusTracker: this.focusTracker,
+					originKeystrokeHandler: editor.keystrokes,
+					toolbar: this.view.toolbar
+				} );
+			} );
+	}
+
+	/**
+	 * Destroys the UI.
+	 *
+	 * @returns {Promise} A Promise resolved when the destruction process is finished.
+	 */
+	destroy() {
+		return this.view.destroy();
+	}
+}

+ 78 - 0
packages/ckeditor5-editor-inline/src/inlineeditoruiview.js

@@ -0,0 +1,78 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module editor-inline/inlineeditoruiview
+ */
+
+import EditorUIView from '@ckeditor/ckeditor5-ui/src/editorui/editoruiview';
+import InlineEditableUIView from '@ckeditor/ckeditor5-ui/src/editableui/inline/inlineeditableuiview';
+import FloatingPanelView from '@ckeditor/ckeditor5-ui/src/panel/floating/floatingpanelview';
+import ToolbarView from '@ckeditor/ckeditor5-ui/src/toolbar/toolbarview';
+import Template from '@ckeditor/ckeditor5-ui/src/template';
+
+/**
+ * Inline editor UI view. Uses inline editable and floating toolbar.
+ *
+ * @extends module:ui/editorui/editoruiview~EditorUIView
+ */
+export default class InlineEditorUIView extends EditorUIView {
+	/**
+	 * Creates an instance of the inline editor UI view.
+	 *
+	 * @param {module:utils/locale~Locale} locale The {@link module:core/editor/editor~Editor#locale} instance.
+	 */
+	constructor( locale, editableElement ) {
+		super( locale );
+
+		/**
+		 * A floating toolbar view instance.
+		 *
+		 * @readonly
+		 * @member {module:ui/toolbar/toolbarview~ToolbarView}
+		 */
+		this.toolbar = new ToolbarView( locale );
+
+		/**
+		 * A floating panel view instance.
+		 *
+		 * @readonly
+		 * @member {module:ui/panel/floating/floatingpanelview~FloatingPanelView}
+		 */
+		this.panel = new FloatingPanelView( locale );
+
+		Template.extend( this.panel.template, {
+			attributes: {
+				class: 'ck-toolbar__container'
+			}
+		} );
+
+		/**
+		 * Editable UI view.
+		 *
+		 * @readonly
+		 * @member {module:ui/editableui/inline/inlineeditableuiview~InlineEditableUIView}
+		 */
+		this.editable = new InlineEditableUIView( locale, editableElement );
+
+		this.body.add( this.panel );
+		this.addChildren( this.editable );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		return super.init()
+			.then( () => this.panel.content.add( this.toolbar ) );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	get editableElement() {
+		return this.editable.element;
+	}
+}

+ 22 - 0
packages/ckeditor5-editor-inline/tests/.jshintrc

@@ -0,0 +1,22 @@
+{
+  "esnext": true,
+  "expr": true,
+  "immed": true,
+  "loopfunc": true,
+  "noarg": true,
+  "nonbsp": true,
+  "strict": "implied",
+  "undef": true,
+  "unused": true,
+  "varstmt": true,
+  "globals": {
+    "after": false,
+    "afterEach": false,
+    "before": false,
+    "beforeEach": false,
+    "describe": false,
+    "expect": false,
+    "it": false,
+    "sinon": false
+  }
+}

+ 183 - 0
packages/ckeditor5-editor-inline/tests/inline.js

@@ -0,0 +1,183 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals document */
+
+import InlineEditorUI from '../src/inlineeditorui';
+import InlineEditorUIView from '../src/inlineeditoruiview';
+
+import HtmlDataProcessor from '@ckeditor/ckeditor5-engine/src/dataprocessor/htmldataprocessor';
+
+import InlineEditor from '../src/inline';
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
+
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+import count from '@ckeditor/ckeditor5-utils/src/count';
+
+testUtils.createSinonSandbox();
+
+describe( 'InlineEditor', () => {
+	let editor, editorElement;
+
+	beforeEach( () => {
+		editorElement = document.createElement( 'div' );
+		editorElement.innerHTML = '<p><strong>foo</strong> bar</p>';
+
+		document.body.appendChild( editorElement );
+	} );
+
+	afterEach( () => {
+		editorElement.remove();
+	} );
+
+	describe( 'constructor()', () => {
+		beforeEach( () => {
+			editor = new InlineEditor( editorElement );
+		} );
+
+		it( 'creates a single div editable root in the view', () => {
+			expect( editor.editing.view.getRoot() ).to.have.property( 'name', 'div' );
+		} );
+
+		it( 'creates a single document root', () => {
+			expect( count( editor.document.getRootNames() ) ).to.equal( 1 );
+			expect( editor.document.getRoot() ).to.have.property( 'name', '$root' );
+		} );
+
+		it( 'creates the UI using BoxedEditorUI classes', () => {
+			expect( editor.ui ).to.be.instanceof( InlineEditorUI );
+			expect( editor.ui.view ).to.be.instanceof( InlineEditorUIView );
+		} );
+
+		it( 'uses HTMLDataProcessor', () => {
+			expect( editor.data.processor ).to.be.instanceof( HtmlDataProcessor );
+		} );
+	} );
+
+	describe( 'create()', () => {
+		beforeEach( function() {
+			return InlineEditor.create( editorElement, {
+					plugins: [ Paragraph, Bold ]
+				} )
+				.then( newEditor => {
+					editor = newEditor;
+				} );
+		} );
+
+		afterEach( () => {
+			return editor.destroy();
+		} );
+
+		it( 'creates an instance which inherits from the InlineEditor', () => {
+			expect( editor ).to.be.instanceof( InlineEditor );
+		} );
+
+		it( 'creates element–less UI view', () => {
+			expect( editor.ui.view.element ).to.be.null;
+		} );
+
+		it( 'attaches editable UI as view\'s DOM root', () => {
+			expect( editor.editing.view.getDomRoot() ).to.equal( editor.ui.view.editable.element );
+		} );
+
+		it( 'loads data from the editor element', () => {
+			expect( editor.getData() ).to.equal( '<p><strong>foo</strong> bar</p>' );
+		} );
+	} );
+
+	describe( 'create - events', () => {
+		afterEach( () => {
+			return editor.destroy();
+		} );
+
+		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( 'uiReady', spy );
+					this.editor.on( 'dataReady', spy );
+					this.editor.on( 'ready', spy );
+				}
+			}
+
+			return InlineEditor.create( editorElement, {
+					plugins: [ EventWatcher ]
+				} )
+				.then( ( newEditor ) => {
+					expect( fired ).to.deep.equal( [ 'pluginsReady', 'uiReady', 'dataReady', 'ready' ] );
+
+					editor = newEditor;
+				} );
+		} );
+
+		it( 'fires dataReady once data is loaded', () => {
+			let data;
+
+			class EventWatcher extends Plugin {
+				init() {
+					this.editor.on( 'dataReady', () => {
+						data = this.editor.getData();
+					} );
+				}
+			}
+
+			return InlineEditor.create( editorElement, {
+					plugins: [ EventWatcher, Paragraph, Bold ]
+				} )
+				.then( ( newEditor ) => {
+					expect( data ).to.equal( '<p><strong>foo</strong> bar</p>' );
+
+					editor = newEditor;
+				} );
+		} );
+
+		it( 'fires uiReady once UI is ready', () => {
+			let isReady;
+
+			class EventWatcher extends Plugin {
+				init() {
+					this.editor.on( 'uiReady', () => {
+						isReady = this.editor.ui.view.ready;
+					} );
+				}
+			}
+
+			return InlineEditor.create( editorElement, {
+					plugins: [ EventWatcher ]
+				} )
+				.then( ( newEditor ) => {
+					expect( isReady ).to.be.true;
+
+					editor = newEditor;
+				} );
+		} );
+	} );
+
+	describe( 'destroy', () => {
+		beforeEach( function() {
+			return InlineEditor.create( editorElement, { plugins: [ Paragraph ] } )
+				.then( newEditor => {
+					editor = newEditor;
+				} );
+		} );
+
+		it( 'sets the data back to the editor element', () => {
+			editor.setData( '<p>foo</p>' );
+
+			return editor.destroy()
+				.then( () => {
+					expect( editorElement.innerHTML ).to.equal( '<p>foo</p>' );
+				} );
+		} );
+	} );
+} );

+ 189 - 0
packages/ckeditor5-editor-inline/tests/inlineeditorui.js

@@ -0,0 +1,189 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals document, Event */
+
+import ComponentFactory from '@ckeditor/ckeditor5-ui/src/componentfactory';
+import View from '@ckeditor/ckeditor5-ui/src/view';
+
+import InlineEditorUI from '../src/inlineeditorui';
+import InlineEditorUIView from '../src/inlineeditoruiview';
+import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
+
+import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker';
+
+import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+import utils from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
+
+testUtils.createSinonSandbox();
+
+describe( 'InlineEditorUI', () => {
+	let editorElement, editor, editable, view, ui;
+
+	beforeEach( () => {
+		editorElement = document.createElement( 'div' );
+		document.body.appendChild( editorElement );
+
+		editor = new ClassicTestEditor( editorElement, {
+			toolbar: [ 'foo', 'bar' ]
+		} );
+
+		view = new InlineEditorUIView( editor.locale );
+		ui = new InlineEditorUI( editor, view );
+		editable = editor.editing.view.getRoot();
+
+		ui.componentFactory.add( 'foo', viewCreator( 'foo' ) );
+		ui.componentFactory.add( 'bar', viewCreator( 'bar' ) );
+	} );
+
+	describe( 'constructor()', () => {
+		it( 'sets #editor', () => {
+			expect( ui.editor ).to.equal( editor );
+		} );
+
+		it( 'sets #view', () => {
+			expect( ui.view ).to.equal( view );
+		} );
+
+		it( 'creates #componentFactory factory', () => {
+			expect( ui.componentFactory ).to.be.instanceOf( ComponentFactory );
+		} );
+
+		it( 'creates #focusTracker', () => {
+			expect( ui.focusTracker ).to.be.instanceOf( FocusTracker );
+		} );
+
+		describe( 'panel', () => {
+			it( 'binds view.panel#isActive to editor.ui#focusTracker', () => {
+				ui.focusTracker.isFocused = false;
+				expect( view.panel.isActive ).to.be.false;
+
+				ui.focusTracker.isFocused = true;
+				expect( view.panel.isActive ).to.be.true;
+			} );
+
+			it( 'sets view.panel#targetElement', () => {
+				expect( view.panel.targetElement ).to.equal( view.editableElement );
+			} );
+		} );
+
+		describe( 'editable', () => {
+			it( 'registers view.editable#element in editor focus tracker', () => {
+				ui.focusTracker.isFocused = false;
+
+				view.editable.element.dispatchEvent( new Event( 'focus' ) );
+				expect( ui.focusTracker.isFocused ).to.true;
+			} );
+
+			it( 'sets view.editable#name', () => {
+				expect( view.editable.name ).to.equal( editable.rootName );
+			} );
+
+			it( 'binds view.editable#isFocused', () => {
+				utils.assertBinding(
+					view.editable,
+					{ isFocused: false },
+					[
+						[ ui.focusTracker, { isFocused: true } ]
+					],
+					{ isFocused: true }
+				);
+			} );
+
+			it( 'binds view.editable#isReadOnly', () => {
+				utils.assertBinding(
+					view.editable,
+					{ isReadOnly: false },
+					[
+						[ editable, { isReadOnly: true } ]
+					],
+					{ isReadOnly: true }
+				);
+			} );
+		} );
+	} );
+
+	describe( 'init()', () => {
+		afterEach( () => {
+			return ui.destroy();
+		} );
+
+		it( 'returns a promise', () => {
+			const promise = ui.init().then( () => {
+				expect( promise ).to.be.instanceof( Promise );
+			} );
+
+			return promise;
+		} );
+
+		it( 'initializes the #view', () => {
+			const spy = sinon.spy( view, 'init' );
+
+			return ui.init().then( () => {
+				sinon.assert.calledOnce( spy );
+			} );
+		} );
+
+		it( 'fills view.toolbar#items with editor config', () => {
+			const spy = testUtils.sinon.spy( view.toolbar, 'fillFromConfig' );
+
+			return ui.init().then( () => {
+				sinon.assert.calledWithExactly( spy, editor.config.get( 'toolbar' ), ui.componentFactory );
+			} );
+		} );
+
+		it( 'initializes keyboard navigation between view#toolbar and view#editable', () => {
+			const spy = testUtils.sinon.spy( view.toolbar, 'focus' );
+
+			return ui.init().then( () => {
+				ui.focusTracker.isFocused = true;
+				ui.view.toolbar.focusTracker.isFocused = false;
+
+				editor.keystrokes.press( {
+					keyCode: keyCodes.f10,
+					altKey: true,
+					preventDefault: sinon.spy(),
+					stopPropagation: sinon.spy()
+				} );
+
+				sinon.assert.calledOnce( spy );
+			} );
+		} );
+	} );
+
+	describe( 'destroy()', () => {
+		it( 'returns a promise', () => {
+			return ui.init().then( () => {
+				const promise = ui.destroy().then( () => {
+					expect( promise ).to.be.instanceof( Promise );
+				} );
+
+				return promise;
+			} );
+		} );
+
+		it( 'destroys the #view', () => {
+			const spy = sinon.spy( view, 'destroy' );
+
+			return ui.init()
+				.then( () => ui.destroy() )
+				.then( () => {
+					sinon.assert.calledOnce( spy );
+				} );
+		} );
+	} );
+} );
+
+function viewCreator( name ) {
+	return ( locale ) => {
+		const view = new View( locale );
+
+		view.name = name;
+		view.element = document.createElement( 'a' );
+
+		return view;
+	};
+}

+ 91 - 0
packages/ckeditor5-editor-inline/tests/inlineeditoruiview.js

@@ -0,0 +1,91 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import InlineEditorUIView from '../src/inlineeditoruiview';
+import ToolbarView from '@ckeditor/ckeditor5-ui/src/toolbar/toolbarview';
+import FloatingPanelView from '@ckeditor/ckeditor5-ui/src/panel/floating/floatingpanelview';
+import InlineEditableUIView from '@ckeditor/ckeditor5-ui/src/editableui/inline/inlineeditableuiview';
+import Locale from '@ckeditor/ckeditor5-utils/src/locale';
+
+describe( 'InlineEditorUIView', () => {
+	let locale, view;
+
+	beforeEach( () => {
+		locale = new Locale( 'en' );
+		view = new InlineEditorUIView( locale );
+	} );
+
+	describe( 'constructor()', () => {
+		describe( '#toolbar', () => {
+			it( 'is created', () => {
+				expect( view.toolbar ).to.be.instanceof( ToolbarView );
+			} );
+
+			it( 'is given a locale object', () => {
+				expect( view.toolbar.locale ).to.equal( locale );
+			} );
+		} );
+
+		describe( '#panel', () => {
+			it( 'is created', () => {
+				expect( view.panel ).to.be.instanceof( FloatingPanelView );
+			} );
+
+			it( 'is given a locale object', () => {
+				expect( view.panel.locale ).to.equal( locale );
+			} );
+
+			it( 'is given the right CSS class', () => {
+				expect( view.panel.element.classList.contains( 'ck-toolbar__container' ) ).to.be.true;
+			} );
+
+			it( 'is put into the #body collection', () => {
+				expect( view.body.get( 0 ) ).to.equal( view.panel );
+			} );
+		} );
+
+		describe( '#editable', () => {
+			it( 'is created', () => {
+				expect( view.editable ).to.be.instanceof( InlineEditableUIView );
+			} );
+
+			it( 'is given a locate object', () => {
+				expect( view.editable.locale ).to.equal( locale );
+			} );
+
+			it( 'is registered as a child', () => {
+				const spy = sinon.spy( view.editable, 'destroy' );
+
+				return view.init()
+					.then( () => view.destroy() )
+					.then( () => {
+						sinon.assert.calledOnce( spy );
+					} );
+			} );
+		} );
+	} );
+
+	describe( 'init', () => {
+		it( 'appends #toolbar to panel#content', () => {
+			expect( view.panel.content ).to.have.length( 0 );
+
+			return view.init()
+				.then( () => {
+					expect( view.panel.content.get( 0 ) ).to.equal( view.toolbar );
+				} )
+				.then( () => view.destroy() );
+		} );
+	} );
+
+	describe( 'editableElement', () => {
+		it( 'returns editable\'s view element', () => {
+			return view.init()
+				.then( () => {
+					expect( view.editableElement.getAttribute( 'contentEditable' ) ).to.equal( 'true' );
+				} )
+				.then( () => view.destroy() );
+		} );
+	} );
+} );

+ 32 - 0
packages/ckeditor5-editor-inline/tests/manual/inline.html

@@ -0,0 +1,32 @@
+<p>
+	<button id="destroyEditors">Destroy editor</button>
+	<button id="initEditors">Init editors</button>
+</p>
+
+<div id="editor-1" contenteditable="true" class="custom-class" custom-attr="foo">
+	<h2>Editor 1</h2>
+	<p>This is an editor instance.</p>
+</div>
+
+<div id="editor-2" class="custom-class" custom-attr="foo">
+	<h2>Editor 2</h2>
+	<p>This is another editor instance.</p>
+	<p>
+		Unlike Editor 1 it doesn't have contenteditable=true initially.
+		Check if it's editable after initializing the editors and back to non-editable after destroying them.
+	</p>
+</div>
+
+<style>
+	body {
+		width: 10000px;
+		height: 10000px;
+	}
+
+	.custom-class {
+		margin-top: 100px;
+		margin-left: 100px;
+		margin-bottom: 100px;
+		width: 450px;
+	}
+</style>

+ 64 - 0
packages/ckeditor5-editor-inline/tests/manual/inline.js

@@ -0,0 +1,64 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals console:false, document, window */
+
+import InlineEditor from '../../src/inline';
+import ArticlePreset from '@ckeditor/ckeditor5-presets/src/article';
+import testUtils from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
+
+window.editors = {};
+window.editables = [];
+window._observers = [];
+
+function initEditors() {
+	init( '#editor-1' );
+	init( '#editor-2' );
+
+	function init( selector ) {
+		InlineEditor.create( document.querySelector( selector ), {
+			plugins: [ ArticlePreset ],
+			toolbar: [ 'headings', 'bold', 'italic', 'undo', 'redo', 'link', 'unlink' ]
+		} )
+		.then( editor => {
+			console.log( `${ selector } has been initialized`, editor );
+			console.log( 'It has been added to global `editors` and `editables`.' );
+
+			window.editors[ selector ] = editor;
+			window.editables.push( editor.editing.view.getRoot() );
+
+			let observer = testUtils.createObserver();
+
+			observer.observe(
+				`${ selector }.ui.focusTracker`,
+				editor.ui.focusTracker,
+				[ 'isFocused' ]
+			);
+
+			window._observers.push( observer );
+		} )
+		.catch( err => {
+			console.error( err.stack );
+		} );
+	}
+}
+
+function destroyEditors() {
+	for ( let selector in window.editors ) {
+		window.editors[ selector ].destroy().then( () => {
+			console.log( `${ selector } was destroyed.` );
+		} );
+	}
+
+	for ( let observer of window._observers ) {
+		observer.stopListening();
+	}
+
+	window.editors = {};
+	window.editables.length = window._observers.length = 0;
+}
+
+document.getElementById( 'initEditors' ).addEventListener( 'click', initEditors );
+document.getElementById( 'destroyEditors' ).addEventListener( 'click', destroyEditors );

+ 27 - 0
packages/ckeditor5-editor-inline/tests/manual/inline.md

@@ -0,0 +1,27 @@
+1. Click "Init editors".
+2. Expected:
+  * Two inline editor should be created.
+  * Elements used as editables should remain visible.
+    * They should preserve `.custom-class` and `custom-attr="foo"`.
+  * There should be floating toolbars with "Bold", "Italic", "Undo", "Redo", "Link" and "Unlink" buttons.
+3. Scroll the webpage.
+4. Expected:
+  * Focused editor's toolbar should float around but always stick to editable.
+  * Focused editor's toolbar should stick to the bottom of the editable if there's not enough space above.
+5. Press <kbd>Alt+F10</kbd> when focusing the editor.
+6. Expected:
+  * Toolbar should gain focus. Editable should keep its styling.
+7. Click "Destroy editors".
+8. Expected:
+  * Editors should be destroyed.
+  * Element used as editables should remain visible.
+    * They should preserve `.custom-class` and `custom-attr="foo"`.
+  * Elements should contain its data (updated).
+  * `.ck-body` regions should be removed from `<body>`.
+
+## Notes:
+
+* You can play with:
+  * `window.editables[ N ].isReadOnly`,
+* Changes to `window.editors[ name ].focusTracker.isFocused` should be logged to the console.
+* Features should work.

+ 21 - 0
packages/ckeditor5-editor-inline/theme/theme.scss

@@ -0,0 +1,21 @@
+// Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+// For licensing, see LICENSE.md or http://ckeditor.com/license
+
+@import '~@ckeditor/ckeditor5-theme-lark/theme/theme.scss';
+
+// TODO move to make a common style with editor-classic.
+.ck-editor__editable {
+	&.ck-focused {
+		@include ck-focus-ring( 'outline' );
+		@include ck-box-shadow( $ck-inner-shadow );
+	}
+
+	&_inline {
+		overflow: auto;
+		padding: 0 ck-spacing();
+	}
+}
+
+.ck-body .ck-toolbar {
+	@include ck-editor-toolbar();
+}