Przeglądaj źródła

Merge pull request #154 from ckeditor/t/ckeditor5/1449

Other: Editor UI classes API refactoring. Closes ckeditor/ckeditor5#1449.

BREAKING CHANGE: Removed `EditroWithUI#element` property.  The `EditorUI#element` property should be used instead.
BREAKING CHANGE: Removed `EditroWithUI#uiReady` event.  The `EditorUI#ready` event should be used instead.
BREAKING CHANGE: Removed `view` parameter in `EditorUI` constructor. Only subclasses should use it without passing it further to `EditorUI`.
BREAKING CHANGE: Removed `EditroUI#view` property. The `view` property from subclasses (like `ClassicEditorUI#view`) should be used directly instead.
Piotr Jasiun 7 lat temu
rodzic
commit
6502c7d055

+ 59 - 11
packages/ckeditor5-core/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.<String,HTMLElement>}
+		 */
+		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 `<div>` 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.<String>}
+	 */
+	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.
 	 *

+ 0 - 26
packages/ckeditor5-core/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 `<div>` 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
- */

+ 2 - 2
packages/ckeditor5-core/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();
 				} );

+ 58 - 17
packages/ckeditor5-core/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 );

+ 64 - 11
packages/ckeditor5-core/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;
 		} );
 	} );
 } );