Skip to content
This repository has been archived by the owner on Jun 26, 2020. It is now read-only.

Commit

Permalink
Merge pull request #154 from ckeditor/t/ckeditor5/1449
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Piotr Jasiun authored Jan 22, 2019
2 parents eac8298 + bb3d978 commit aca1ff1
Show file tree
Hide file tree
Showing 5 changed files with 183 additions and 67 deletions.
70 changes: 59 additions & 11 deletions src/editor/editorui.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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.
Expand All @@ -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.
*
Expand All @@ -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.
*
Expand Down
26 changes: 0 additions & 26 deletions src/editor/editorwithui.jsdoc
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
4 changes: 2 additions & 2 deletions tests/_utils-tests/classictesteditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
}
Expand All @@ -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();
} );
Expand Down
75 changes: 58 additions & 17 deletions tests/_utils/classictesteditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand All @@ -49,7 +46,6 @@ export default class ClassicTestEditor extends Editor {
* @inheritDoc
*/
destroy() {
this._elementReplacer.restore();
this.ui.destroy();

return super.destroy();
Expand All @@ -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' );
Expand All @@ -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 );
75 changes: 64 additions & 11 deletions tests/editor/editorui.js
Original file line number Diff line number Diff line change
Expand Up @@ -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( () => {
Expand All @@ -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 );
} );
Expand All @@ -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();

Expand Down Expand Up @@ -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;
} );
} );
} );

0 comments on commit aca1ff1

Please sign in to comment.