diff --git a/src/editor/editorui.js b/src/editor/editorui.js index 0b594aae..a69c3177 100644 --- a/src/editor/editorui.js +++ b/src/editor/editorui.js @@ -23,9 +23,8 @@ export default class EditorUI { * 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 The view of the UI. */ - constructor( editor, view ) { + constructor( editor ) { /** * The editor that the UI belongs to. * @@ -34,14 +33,6 @@ export default class EditorUI { */ this.editor = editor; - /** - * The main (top–most) view of the editor UI. - * - * @readonly - * @member {module:ui/editorui/editoruiview~EditorUIView} #view - */ - this.view = view; - /** * An instance of the {@link module:ui/componentfactory~ComponentFactory}, a registry used by plugins * to register factories of specific UI components. @@ -60,10 +51,37 @@ export default class EditorUI { */ this.focusTracker = new FocusTracker(); + /** + * Stores all editable elements used by the editor instance. + * + * @protected + * @member {Map.} + */ + this._editableElements = new Map(); + // Informs UI components that should be refreshed after layout change. this.listenTo( editor.editing.view.document, 'layoutChanged', () => this.update() ); } + /** + * The main (outermost) DOM element of the editor UI. + * + * For example, in {@link module:editor-classic/classiceditor~ClassicEditor} it is a `
` which + * wraps the editable element and the toolbar. In {@link module:editor-inline/inlineeditor~InlineEditor} + * it is the editable element itself (as there is no other wrapper). However, in + * {@link module:editor-decoupled/decouplededitor~DecoupledEditor} it is set to `null` because this editor does not + * come with a single "main" HTML element (its editable element and toolbar are separate). + * + * This property can be understood as a shorthand for retrieving the element that a specific editor integration + * considers to be its main DOM element. + * + * @readonly + * @member {HTMLElement|null} #element + */ + get element() { + return null; + } + /** * Fires the {@link module:core/editor/editorui~EditorUI#event:update `update`} event. * @@ -79,10 +97,40 @@ export default class EditorUI { */ destroy() { this.stopListening(); - this.view.destroy(); + this.focusTracker.destroy(); + + this._editableElements = new Map(); + } + + /** + * Returns the editable editor element with the given name or null if editable does not exist. + * + * @param {String} [rootName=main] The editable name. + * @returns {HTMLElement|undefined} + */ + getEditableElement( rootName = 'main' ) { + return this._editableElements.get( rootName ); + } + + /** + * Returns array of names of all editor editable elements. + * + * @returns {Iterable.} + */ + getEditableElementsNames() { + return this._editableElements.keys(); } + /** + * Fired when the editor UI is ready. + * + * Fired after {@link module:core/editor/editor~Editor#event:pluginsReady} and before + * {@link module:core/editor/editor~Editor#event:dataReady}. + * + * @event ready + */ + /** * Fired whenever the UI (all related components) should be refreshed. * diff --git a/src/editor/editorwithui.jsdoc b/src/editor/editorwithui.jsdoc index 10aba4d9..ba938a66 100644 --- a/src/editor/editorwithui.jsdoc +++ b/src/editor/editorwithui.jsdoc @@ -27,29 +27,3 @@ * @readonly * @member {module:core/editor/editorui~EditorUI} #ui */ - -/** - * The main (outermost) DOM element of the editor UI. - * - * For example, in {@link module:editor-classic/classiceditor~ClassicEditor} it is a `
` which - * wraps the editable element and the toolbar. In {@link module:editor-inline/inlineeditor~InlineEditor} - * it is the editable element itself (as there is no other wrapper). However, in - * {@link module:editor-decoupled/decouplededitor~DecoupledEditor} it is set to `null` because this editor does not - * come with a single "main" HTML element (its editable element and toolbar are separate). - * - * This property can be understood as a shorthand for retrieving the element that a specific editor integration - * considers to be its main DOM element. There are always other ways to access these elements, too - * (e.g. via {@link #ui `editor.ui`}). - * - * @readonly - * @member {HTMLElement|null} #element - */ - -/** - * Fired when the editor UI is ready. - * - * Fired after {@link module:core/editor/editor~Editor#event:pluginsReady} and before - * {@link module:core/editor/editor~Editor#event:dataReady}. - * - * @event uiReady - */ diff --git a/tests/_utils-tests/classictesteditor.js b/tests/_utils-tests/classictesteditor.js index 8b13cfba..8b0ad00d 100644 --- a/tests/_utils-tests/classictesteditor.js +++ b/tests/_utils-tests/classictesteditor.js @@ -122,7 +122,7 @@ describe( 'ClassicTestEditor', () => { class EventWatcher extends Plugin { init() { this.editor.on( 'pluginsReady', spy ); - this.editor.on( 'uiReady', spy ); + this.editor.ui.on( 'ready', spy ); this.editor.on( 'dataReady', spy ); this.editor.on( 'ready', spy ); } @@ -133,7 +133,7 @@ describe( 'ClassicTestEditor', () => { plugins: [ EventWatcher ] } ) .then( editor => { - expect( fired ).to.deep.equal( [ 'pluginsReady', 'uiReady', 'dataReady', 'ready' ] ); + expect( fired ).to.deep.equal( [ 'pluginsReady', 'ready', 'dataReady', 'ready' ] ); return editor.destroy(); } ); diff --git a/tests/_utils/classictesteditor.js b/tests/_utils/classictesteditor.js index 2507402f..dd7589e2 100644 --- a/tests/_utils/classictesteditor.js +++ b/tests/_utils/classictesteditor.js @@ -33,14 +33,11 @@ export default class ClassicTestEditor extends Editor { // Use the HTML data processor in this editor. this.data.processor = new HtmlDataProcessor(); - this.ui = new EditorUI( this, new BoxedEditorUIView( this.locale ) ); + this.ui = new ClassicTestEditorUI( this, new BoxedEditorUIView( this.locale ) ); // Expose properties normally exposed by the ClassicEditorUI. this.ui.view.editable = new InlineEditableUIView( this.ui.view.locale ); - // 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(); } @@ -49,7 +46,6 @@ export default class ClassicTestEditor extends Editor { * @inheritDoc */ destroy() { - this._elementReplacer.restore(); this.ui.destroy(); return super.destroy(); @@ -66,18 +62,8 @@ export default class ClassicTestEditor extends Editor { editor.initPlugins() // Simulate EditorUI.init() (e.g. like in ClassicEditorUI). The ui#view // should be rendered after plugins are initialized. - .then( () => { - const view = editor.ui.view; - - view.render(); - view.main.add( view.editable ); - view.editableElement = view.editable.element; - } ) - .then( () => { - editor._elementReplacer.replace( element, editor.ui.view.element ); - editor.fire( 'uiReady' ); - } ) - .then( () => editor.editing.view.attachDomRoot( editor.ui.view.editableElement ) ) + .then( () => editor.ui.init( element ) ) + .then( () => editor.editing.view.attachDomRoot( editor.ui.getEditableElement() ) ) .then( () => editor.data.init( getDataFromElement( element ) ) ) .then( () => { editor.fire( 'dataReady' ); @@ -90,5 +76,60 @@ export default class ClassicTestEditor extends Editor { } } +/** + * A simplified classic editor ui class. + * + * @memberOf tests.core._utils + * @extends core.editor.EditorUI + */ +class ClassicTestEditorUI extends EditorUI { + /** + * @inheritDoc + */ + constructor( editor, view ) { + super( editor ); + + // A helper to easily replace the editor#element with editor.editable#element. + this._elementReplacer = new ElementReplacer(); + + this._view = view; + } + + /** + * The main (top–most) view of the editor UI. + * + * @readonly + * @member {module:ui/editorui/editoruiview~EditorUIView} #view + */ + get view() { + return this._view; + } + + init( element ) { + const view = this.view; + + view.render(); + view.main.add( view.editable ); + view.editableElement = view.editable.element; + + this._editableElements.set( 'main', view.editable.element ); + + this._elementReplacer.replace( element, view.element ); + + this.fire( 'ready' ); + } + + /** + * @inheritDoc + */ + destroy() { + this._elementReplacer.restore(); + + this._view.destroy(); + + super.destroy(); + } +} + mix( ClassicTestEditor, DataApiMixin ); mix( ClassicTestEditor, ElementApiMixin ); diff --git a/tests/editor/editorui.js b/tests/editor/editorui.js index b5ff4936..341fb9d6 100644 --- a/tests/editor/editorui.js +++ b/tests/editor/editorui.js @@ -8,19 +8,19 @@ import Editor from '../../src/editor/editor'; import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker'; import ComponentFactory from '@ckeditor/ckeditor5-ui/src/componentfactory'; -import View from '@ckeditor/ckeditor5-ui/src/view'; import testUtils from '../_utils/utils'; +/* global document */ + describe( 'EditorUI', () => { - let editor, view, ui; + let editor, ui; testUtils.createSinonSandbox(); beforeEach( () => { editor = new Editor(); - view = new View(); - ui = new EditorUI( editor, view ); + ui = new EditorUI( editor ); } ); afterEach( () => { @@ -32,10 +32,6 @@ describe( 'EditorUI', () => { expect( ui.editor ).to.equal( editor ); } ); - it( 'should set #view', () => { - expect( ui.view ).to.equal( view ); - } ); - it( 'should create #componentFactory factory', () => { expect( ui.componentFactory ).to.be.instanceOf( ComponentFactory ); } ); @@ -44,6 +40,10 @@ describe( 'EditorUI', () => { expect( ui.focusTracker ).to.be.instanceOf( FocusTracker ); } ); + it( 'should have #element getter', () => { + expect( ui.element ).to.null; + } ); + it( 'should fire update event after viewDocument#layoutChanged', () => { const spy = sinon.spy(); @@ -84,12 +84,65 @@ describe( 'EditorUI', () => { sinon.assert.called( spy ); } ); - it( 'should destroy the #view', () => { - const spy = sinon.spy( view, 'destroy' ); + it( 'should reset editables array', () => { + ui._editableElements.set( 'foo', {} ); + ui._editableElements.set( 'bar', {} ); + + expect( ui._editableElements.size ).to.equal( 2 ); ui.destroy(); - sinon.assert.called( spy ); + expect( ui._editableElements.size ).to.equal( 0 ); + } ); + } ); + + describe( 'getEditableElement()', () => { + it( 'should return editable element (default root name)', () => { + const ui = new EditorUI( editor ); + const editableMock = { name: 'main', element: document.createElement( 'div' ) }; + + ui._editableElements.set( editableMock.name, editableMock.element ); + + expect( ui.getEditableElement() ).to.equal( editableMock.element ); + } ); + + it( 'should return editable element (custom root name)', () => { + const ui = new EditorUI( editor ); + const editableMock1 = { name: 'root1', element: document.createElement( 'div' ) }; + const editableMock2 = { name: 'root2', element: document.createElement( 'p' ) }; + + ui._editableElements.set( editableMock1.name, editableMock1.element ); + ui._editableElements.set( editableMock2.name, editableMock2.element ); + + expect( ui.getEditableElement( 'root1' ) ).to.equal( editableMock1.element ); + expect( ui.getEditableElement( 'root2' ) ).to.equal( editableMock2.element ); + } ); + + it( 'should return null if editable with specified name does not exist', () => { + const ui = new EditorUI( editor ); + + expect( ui.getEditableElement() ).to.be.undefined; + } ); + } ); + + describe( 'getEditableElementsNames()', () => { + it( 'should return iterable object of names', () => { + const ui = new EditorUI( editor ); + const editableMock1 = { name: 'main', element: document.createElement( 'div' ) }; + const editableMock2 = { name: 'root2', element: document.createElement( 'p' ) }; + + ui._editableElements.set( editableMock1.name, editableMock1.element ); + ui._editableElements.set( editableMock2.name, editableMock2.element ); + + const names = ui.getEditableElementsNames(); + expect( names[ Symbol.iterator ] ).to.instanceof( Function ); + expect( Array.from( names ) ).to.deep.equal( [ 'main', 'root2' ] ); + } ); + + it( 'should return empty array if no editables', () => { + const ui = new EditorUI( editor ); + + expect( ui.getEditableElementsNames() ).to.be.empty; } ); } ); } );