Ver código fonte

Feature: The first implementation of the balloon toolbar editor. Closes #1.

Aleksander Nowodzinski 8 anos atrás
pai
commit
e9569ed220

+ 92 - 0
packages/ckeditor5-editor-balloon/src/balloontoolbar.js

@@ -0,0 +1,92 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module editor-balloon-toolbar/contextual
+ */
+
+import StandardEditor from '@ckeditor/ckeditor5-core/src/editor/standardeditor';
+import HtmlDataProcessor from '@ckeditor/ckeditor5-engine/src/dataprocessor/htmldataprocessor';
+import ContextualToolbar from '@ckeditor/ckeditor5-ui/src/toolbar/contextual/contextualtoolbar';
+import BalloonToolbarEditorUI from './balloontoolbareditorui';
+import BalloonToolbarEditorUIView from './balloontoolbareditoruiview';
+
+import '../theme/theme.scss';
+
+/**
+ * The balloon toolbar editor. Uses an inline editable and a toolbar based
+ * on the {@link ui/toolbar/contextual/contextualtoolbar~ContextualToolbar}.
+ *
+ * @extends module:core/editor/standardeditor~StandardEditor
+ */
+export default class BalloonToolbarEditor extends StandardEditor {
+	/**
+	 * Creates an instance of the balloon toolbar 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.config.get( 'plugins' ).push( ContextualToolbar );
+		this.config.define( 'contextualToolbar', this.config.get( 'toolbar' ) );
+
+		this.document.createRoot();
+		this.data.processor = new HtmlDataProcessor();
+		this.ui = new BalloonToolbarEditorUI( this, new BalloonToolbarEditorUIView( 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 balloon toolbar editor instance.
+	 *
+	 *		BalloonToolbarEditor.create( document.querySelector( '#editor' ), {
+	 *			plugins: [ Delete, Enter, Typing, Paragraph, Undo, Bold, Italic ],
+	 *			toolbar: [ 'bold', 'italic' ]
+	 *		} )
+	 *		.then( editor => {
+	 *			console.log( 'Editor was initialized', editor );
+	 *		} )
+	 *		.catch( err => {
+	 *			console.error( err.stack );
+	 *		} );
+	 *
+	 * @param {HTMLElement} element See {@link module:editor-balloon-toolbar/contextual~BalloonToolbarEditor#constructor}'s parameters.
+	 * @param {Object} config See {@link module:editor-balloon-toolbar/contextual~BalloonToolbarEditor#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 this( 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 )
+			);
+		} );
+	}
+}

+ 86 - 0
packages/ckeditor5-editor-balloon/src/balloontoolbareditorui.js

@@ -0,0 +1,86 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module editor-balloon-toolbar/balloontoolbareditorui
+ */
+
+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 balloon toolbar editor UI class.
+ *
+ * @implements module:core/editor/editorui~EditorUI
+ */
+export default class BalloonToolbarEditorUI {
+	/**
+	 * Creates an instance of the balloon toolbar 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();
+
+		// 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;
+		const contextualToolbar = editor.plugins.get( 'ui/contextualtoolbar' );
+
+		return this.view.init().then( () => {
+			enableToolbarKeyboardFocus( {
+				origin: editor.editing.view,
+				originFocusTracker: this.focusTracker,
+				originKeystrokeHandler: editor.keystrokes,
+				toolbar: contextualToolbar.toolbarView
+			} );
+		} );
+	}
+
+	/**
+	 * Destroys the UI.
+	 *
+	 * @returns {Promise} A Promise resolved when the destruction process is finished.
+	 */
+	destroy() {
+		return this.view.destroy();
+	}
+}

+ 44 - 0
packages/ckeditor5-editor-balloon/src/balloontoolbareditoruiview.js

@@ -0,0 +1,44 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module editor-balloon-toolbar/balloontoolbareditoruiview
+ */
+
+import EditorUIView from '@ckeditor/ckeditor5-ui/src/editorui/editoruiview';
+import InlineEditableUIView from '@ckeditor/ckeditor5-ui/src/editableui/inline/inlineeditableuiview';
+
+/**
+ * Contextual editor UI view. Uses the {@link module:ui/editableui/inline/inlineeditableuiview~InlineEditableUIView}.
+ *
+ * @extends module:ui/editorui/editoruiview~EditorUIView
+ */
+export default class BalloonToolbarEditorUIView extends EditorUIView {
+	/**
+	 * Creates an instance of the balloon toolbar editor UI view.
+	 *
+	 * @param {module:utils/locale~Locale} locale The {@link module:core/editor/editor~Editor#locale} instance.
+	 */
+	constructor( locale, editableElement ) {
+		super( locale );
+
+		/**
+		 * The editable UI view.
+		 *
+		 * @readonly
+		 * @member {module:ui/editableui/inline/inlineeditableuiview~InlineEditableUIView}
+		 */
+		this.editable = new InlineEditableUIView( locale, editableElement );
+
+		this.addChildren( this.editable );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	get editableElement() {
+		return this.editable.element;
+	}
+}

+ 195 - 0
packages/ckeditor5-editor-balloon/tests/balloontoolbar.js

@@ -0,0 +1,195 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals document */
+
+import BalloonToolbarEditorUI from '../src/balloontoolbareditorui';
+import BalloonToolbarEditorUIView from '../src/balloontoolbareditoruiview';
+
+import HtmlDataProcessor from '@ckeditor/ckeditor5-engine/src/dataprocessor/htmldataprocessor';
+
+import BalloonToolbarEditor from '../src/balloontoolbar';
+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 ContextualToolbar from '@ckeditor/ckeditor5-ui/src/toolbar/contextual/contextualtoolbar';
+
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+import count from '@ckeditor/ckeditor5-utils/src/count';
+
+testUtils.createSinonSandbox();
+
+describe( 'BalloonToolbarEditor', () => {
+	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 BalloonToolbarEditor( editorElement, {
+				plugins: [ Bold ],
+				toolbar: [ 'Bold' ]
+			} );
+		} );
+
+		it( 'pushes ContextualToolbar to the list of plugins', () => {
+			expect( editor.config.get( 'plugins' ) ).to.include( ContextualToolbar );
+		} );
+
+		it( 'pipes config#toolbar to config#contextualToolbar', () => {
+			expect( editor.config.get( 'contextualToolbar' ) ).to.have.members( [ 'Bold' ] );
+		} );
+
+		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( 'uses HTMLDataProcessor', () => {
+			expect( editor.data.processor ).to.be.instanceof( HtmlDataProcessor );
+		} );
+
+		it( 'creates the UI using BalloonToolbarEditorUI classes', () => {
+			expect( editor.ui ).to.be.instanceof( BalloonToolbarEditorUI );
+			expect( editor.ui.view ).to.be.instanceof( BalloonToolbarEditorUIView );
+		} );
+	} );
+
+	describe( 'create()', () => {
+		beforeEach( function() {
+			return BalloonToolbarEditor.create( editorElement, {
+				plugins: [ Paragraph, Bold ]
+			} )
+			.then( newEditor => {
+				editor = newEditor;
+			} );
+		} );
+
+		afterEach( () => {
+			return editor.destroy();
+		} );
+
+		it( 'creates an instance which inherits from the BalloonToolbarEditor', () => {
+			expect( editor ).to.be.instanceof( BalloonToolbarEditor );
+		} );
+
+		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 BalloonToolbarEditor.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 BalloonToolbarEditor.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 BalloonToolbarEditor.create( editorElement, {
+				plugins: [ EventWatcher ]
+			} )
+			.then( newEditor => {
+				expect( isReady ).to.be.true;
+
+				editor = newEditor;
+			} );
+		} );
+	} );
+
+	describe( 'destroy', () => {
+		beforeEach( function() {
+			return BalloonToolbarEditor.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>' );
+				} );
+		} );
+	} );
+} );

+ 157 - 0
packages/ckeditor5-editor-balloon/tests/balloontoolbareditorui.js

@@ -0,0 +1,157 @@
+/**
+ * @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 BalloonToolbarEditorUI from '../src/balloontoolbareditorui';
+import BalloonToolbarEditorUIView from '../src/balloontoolbareditoruiview';
+import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
+import ContextualToolbar from '@ckeditor/ckeditor5-ui/src/toolbar/contextual/contextualtoolbar';
+
+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( 'BalloonToolbarEditorUI', () => {
+	let editorElement, editor, editable, view, ui;
+
+	beforeEach( () => {
+		editorElement = document.createElement( 'div' );
+		document.body.appendChild( editorElement );
+
+		editor = new ClassicTestEditor( editorElement, {
+			plugins: [ ContextualToolbar ]
+		} );
+
+		return editor.initPlugins()
+			.then( () => {
+				view = new BalloonToolbarEditorUIView( editor.locale );
+				ui = new BalloonToolbarEditorUI( editor, view );
+				editable = editor.editing.view.getRoot();
+			} );
+	} );
+
+	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( '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( 'initializes keyboard navigation between view#toolbar and view#editable', () => {
+			const toolbar = editor.plugins.get( 'ui/contextualtoolbar' );
+			const spy = testUtils.sinon.spy( toolbar.toolbarView, 'focus' );
+
+			return ui.init().then( () => {
+				ui.focusTracker.isFocused = true;
+				toolbar.toolbarView.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 );
+				} );
+		} );
+	} );
+} );

+ 49 - 0
packages/ckeditor5-editor-balloon/tests/balloontoolbareditoruiview.js

@@ -0,0 +1,49 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import BalloonToolbarEditorUIView from '../src/balloontoolbareditoruiview';
+import InlineEditableUIView from '@ckeditor/ckeditor5-ui/src/editableui/inline/inlineeditableuiview';
+import Locale from '@ckeditor/ckeditor5-utils/src/locale';
+
+describe( 'BalloonToolbarEditorUIView', () => {
+	let locale, view;
+
+	beforeEach( () => {
+		locale = new Locale( 'en' );
+		view = new BalloonToolbarEditorUIView( locale );
+	} );
+
+	describe( 'constructor()', () => {
+		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( 'editableElement', () => {
+		it( 'returns editable\'s view element', () => {
+			return view.init()
+				.then( () => {
+					expect( view.editableElement.getAttribute( 'contentEditable' ) ).to.equal( 'true' );
+				} )
+				.then( () => view.destroy() );
+		} );
+	} );
+} );

+ 33 - 0
packages/ckeditor5-editor-balloon/tests/manual/balloontoolbar.html

@@ -0,0 +1,33 @@
+<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. And there's <a href="http://ckeditor.com">some link</a>.</p>
+</div>
+
+<div id="editor-2" class="custom-class" custom-attr="foo">
+	<h2>Editor 2</h2>
+	<p>This is another editor instance.</p>
+	<img src="sample.jpg" />
+	<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>

+ 67 - 0
packages/ckeditor5-editor-balloon/tests/manual/balloontoolbar.js

@@ -0,0 +1,67 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals console:false, document, window */
+
+import BalloonToolbarEditor from '../../src/balloontoolbar';
+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 ) {
+		BalloonToolbarEditor.create( document.querySelector( selector ), {
+			plugins: [ ArticlePreset ],
+			toolbar: [ 'headings', 'bold', 'italic', 'link', 'bulletedList', 'numberedList', 'blockQuote' ],
+			image: {
+				toolbar: [ 'imageStyleFull', 'imageStyleSide', '|', 'imageTextAlternative' ]
+			}
+		} )
+		.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() );
+
+			const 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 ( const selector in window.editors ) {
+		window.editors[ selector ].destroy().then( () => {
+			console.log( `${ selector } was destroyed.` );
+		} );
+	}
+
+	for ( const 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-balloon/tests/manual/balloontoolbar.md

@@ -0,0 +1,27 @@
+1. Click the "Init editors" button.
+2. Expected:
+   * Two editor instances should be created.
+   * Elements used as editables should remain visible.
+      * They should preserve `.custom-class` and `custom-attr="foo"`.
+3. Select some text in the editor.
+   * A floating toolbar should appear at the end (forward selection) or at the beginning (backward selection) of the selection.
+3. Scroll the webpage.
+4. Expected:
+   * The toolbar should always stick to the selection.
+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.

BIN
packages/ckeditor5-editor-balloon/tests/manual/sample.jpg


+ 17 - 0
packages/ckeditor5-editor-balloon/theme/theme.scss

@@ -0,0 +1,17 @@
+// 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();
+	}
+}