diff --git a/src/editor/editorui.js b/src/editor/editorui.js index f3c5e1af..1480305c 100644 --- a/src/editor/editorui.js +++ b/src/editor/editorui.js @@ -12,6 +12,7 @@ import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker'; import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin'; import mix from '@ckeditor/ckeditor5-utils/src/mix'; +import log from '@ckeditor/ckeditor5-utils/src/log'; /** * A class providing the minimal interface that is required to successfully bootstrap any editor UI. @@ -54,10 +55,10 @@ export default class EditorUI { /** * Stores all editable elements used by the editor instance. * - * @protected + * @private * @member {Map.} */ - this._editableElements = new Map(); + this._editableElementsMap = new Map(); // Informs UI components that should be refreshed after layout change. this.listenTo( editor.editing.view.document, 'layoutChanged', () => this.update() ); @@ -100,7 +101,29 @@ export default class EditorUI { this.focusTracker.destroy(); - this._editableElements = new Map(); + // Clean–up the references to the CKEditor instance stored in the native editable DOM elements. + for ( const domElement of this._editableElementsMap.values() ) { + domElement.ckeditorInstance = null; + } + + this._editableElementsMap = new Map(); + } + + /** + * Store the native DOM editable element used by the editor under + * a unique name. + * + * @param {String} rootName The unique name of the editable element. + * @param {HTMLElement} domElement The native DOM editable element. + */ + setEditableElement( rootName, domElement ) { + this._editableElementsMap.set( rootName, domElement ); + + // Put a reference to the CKEditor instance in the editable native DOM element. + // It helps 3rd–party software (browser extensions, other libraries) access and recognize + // CKEditor 5 instances (editing roots) and use their API (there is no global editor + // instance registry). + domElement.ckeditorInstance = this.editor; } /** @@ -110,7 +133,7 @@ export default class EditorUI { * @returns {HTMLElement|undefined} */ getEditableElement( rootName = 'main' ) { - return this._editableElements.get( rootName ); + return this._editableElementsMap.get( rootName ); } /** @@ -119,7 +142,31 @@ export default class EditorUI { * @returns {Iterable.} */ getEditableElementsNames() { - return this._editableElements.keys(); + return this._editableElementsMap.keys(); + } + + /** + * Stores all editable elements used by the editor instance. + * + * @protected + * @deprecated + * @member {Map.} + */ + get _editableElements() { + /** + * The `EditorUI#_editableElements` property has been deprecated and will be removed in the near future. + * Please use {@link #setEditableElement `setEditableElement()`} and {@link #getEditableElement `getEditableElement()`} + * methods instead. + * + * @warning editor-ui-deprecated-editable-elements + * @param {module:core/editor/editorui~EditorUI} editorUI Editor UI instance the property belongs to. + */ + log.warn( + 'editor-ui-deprecated-editable-elements: ' + + 'The EditorUI#_editableElements property has been deprecated and will be remove in the near future.', + { editorUI: this } ); + + return this._editableElementsMap; } /** diff --git a/tests/_utils/classictesteditor.js b/tests/_utils/classictesteditor.js index 6a3571d5..7c5992bf 100644 --- a/tests/_utils/classictesteditor.js +++ b/tests/_utils/classictesteditor.js @@ -138,7 +138,7 @@ class ClassicTestEditorUI extends EditorUI { view.main.add( view.editable ); - this._editableElements.set( 'main', view.editable.element ); + this.setEditableElement( 'main', view.editable.element ); if ( replacementElement ) { this._elementReplacer.replace( replacementElement, view.element ); diff --git a/tests/editor/editorui.js b/tests/editor/editorui.js index 455c1ba7..3cd7fe9e 100644 --- a/tests/editor/editorui.js +++ b/tests/editor/editorui.js @@ -10,6 +10,7 @@ import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker'; import ComponentFactory from '@ckeditor/ckeditor5-ui/src/componentfactory'; import testUtils from '../_utils/utils'; +import log from '@ckeditor/ckeditor5-utils/src/log'; /* global document */ @@ -85,14 +86,50 @@ describe( 'EditorUI', () => { } ); it( 'should reset editables array', () => { - ui._editableElements.set( 'foo', {} ); - ui._editableElements.set( 'bar', {} ); + ui.setEditableElement( 'foo', {} ); + ui.setEditableElement( 'bar', {} ); - expect( ui._editableElements.size ).to.equal( 2 ); + expect( [ ...ui.getEditableElementsNames() ] ).to.deep.equal( [ 'foo', 'bar' ] ); ui.destroy(); - expect( ui._editableElements.size ).to.equal( 0 ); + expect( [ ...ui.getEditableElementsNames() ] ).to.have.length( 0 ); + } ); + + it( 'removes domElement#ckeditorInstance references from registered root elements', () => { + const fooElement = document.createElement( 'foo' ); + const barElement = document.createElement( 'bar' ); + + ui.setEditableElement( 'foo', fooElement ); + ui.setEditableElement( 'bar', barElement ); + + expect( fooElement.ckeditorInstance ).to.equal( editor ); + expect( barElement.ckeditorInstance ).to.equal( editor ); + + ui.destroy(); + + expect( fooElement.ckeditorInstance ).to.be.null; + expect( barElement.ckeditorInstance ).to.be.null; + } ); + } ); + + describe( 'setEditableElement()', () => { + it( 'should register the editable element under a name', () => { + const ui = new EditorUI( editor ); + const element = document.createElement( 'div' ); + + ui.setEditableElement( 'main', element ); + + expect( ui.getEditableElement( 'main' ) ).to.equal( element ); + } ); + + it( 'puts a reference to the editor instance in domElement#ckeditorInstance', () => { + const ui = new EditorUI( editor ); + const element = document.createElement( 'div' ); + + ui.setEditableElement( 'main', element ); + + expect( element.ckeditorInstance ).to.equal( editor ); } ); } ); @@ -101,7 +138,7 @@ describe( 'EditorUI', () => { const ui = new EditorUI( editor ); const editableMock = { name: 'main', element: document.createElement( 'div' ) }; - ui._editableElements.set( editableMock.name, editableMock.element ); + ui.setEditableElement( editableMock.name, editableMock.element ); expect( ui.getEditableElement() ).to.equal( editableMock.element ); } ); @@ -111,8 +148,8 @@ describe( 'EditorUI', () => { 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 ); + ui.setEditableElement( editableMock1.name, editableMock1.element ); + ui.setEditableElement( editableMock2.name, editableMock2.element ); expect( ui.getEditableElement( 'root1' ) ).to.equal( editableMock1.element ); expect( ui.getEditableElement( 'root2' ) ).to.equal( editableMock2.element ); @@ -131,8 +168,8 @@ describe( 'EditorUI', () => { 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 ); + ui.setEditableElement( editableMock1.name, editableMock1.element ); + ui.setEditableElement( editableMock2.name, editableMock2.element ); const names = ui.getEditableElementsNames(); expect( names[ Symbol.iterator ] ).to.instanceof( Function ); @@ -145,4 +182,14 @@ describe( 'EditorUI', () => { expect( ui.getEditableElementsNames() ).to.be.empty; } ); } ); + + describe( '_editableElements()', () => { + it( 'should warn about deprecation', () => { + const ui = new EditorUI( editor ); + const stub = testUtils.sinon.stub( log, 'warn' ); + + expect( ui._editableElements ).to.be.instanceOf( Map ); + sinon.assert.calledWithMatch( stub, 'editor-ui-deprecated-editable-elements' ); + } ); + } ); } );