diff --git a/src/editor/editorui.js b/src/editor/editorui.js index 7047d861..0c7f6cc2 100644 --- a/src/editor/editorui.js +++ b/src/editor/editorui.js @@ -124,7 +124,9 @@ export default class EditorUI { // 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; + if ( !domElement.ckeditorInstance ) { + domElement.ckeditorInstance = this.editor; + } } /** diff --git a/src/editor/utils/securesourceelement.js b/src/editor/utils/securesourceelement.js new file mode 100644 index 00000000..f3b62df2 --- /dev/null +++ b/src/editor/utils/securesourceelement.js @@ -0,0 +1,50 @@ +/** + * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; + +/** + * @module core/editor/utils/securesourceelement + */ + +/** + * Marks the source element on which the editor was initialized. This prevents other editor instances from using this element. + * + * Running multiple editor instances on the same source element causes various issues and it is + * crucial this helper is called as soon as the source element is known to prevent collisions. + * + * @param {module:core/editor/editor~Editor} editor Editor instance. + */ +export default function secureSourceElement( editor ) { + const sourceElement = editor.sourceElement; + + // If the editor was initialized without specifying an element, we don't need to secure anything. + if ( !sourceElement ) { + return; + } + + if ( sourceElement.ckeditorInstance ) { + /** + * A DOM element used to create the editor (e.g. + * {@link module:editor-inline/inlineeditor~InlineEditor.create `InlineEditor.create()`}) + * has already been used to create another editor instance. Make sure each editor is + * created with an unique DOM element. + * + * @error editor-source-element-already-used + * @param {HTMLElement} element DOM element that caused the collision. + */ + throw new CKEditorError( + 'editor-source-element-already-used: ' + + 'The DOM element cannot be used to create multiple editor instances.', + editor + ); + } + + sourceElement.ckeditorInstance = editor; + + editor.once( 'destroy', () => { + delete sourceElement.ckeditorInstance; + } ); +} diff --git a/tests/editor/editorui.js b/tests/editor/editorui.js index c7e4b496..8ebb2ab3 100644 --- a/tests/editor/editorui.js +++ b/tests/editor/editorui.js @@ -130,6 +130,17 @@ describe( 'EditorUI', () => { expect( element.ckeditorInstance ).to.equal( editor ); } ); + + it( 'does not override a reference to the editor instance in domElement#ckeditorInstance', () => { + const ui = new EditorUI( editor ); + const element = document.createElement( 'div' ); + + element.ckeditorInstance = 'foo'; + + ui.setEditableElement( 'main', element ); + + expect( element.ckeditorInstance ).to.equal( 'foo' ); + } ); } ); describe( 'getEditableElement()', () => { diff --git a/tests/editor/utils/securesourceelement.js b/tests/editor/utils/securesourceelement.js new file mode 100644 index 00000000..8388e14f --- /dev/null +++ b/tests/editor/utils/securesourceelement.js @@ -0,0 +1,67 @@ +/** + * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/* global document */ + +import secureSourceElement from '../../../src/editor/utils/securesourceelement'; +import Editor from '../../../src/editor/editor'; +import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils'; + +describe( 'secureSourceElement()', () => { + let editor, sourceElement; + + beforeEach( () => { + class CustomEditor extends Editor {} + + sourceElement = document.createElement( 'div' ); + editor = new CustomEditor(); + + editor.sourceElement = sourceElement; + editor.state = 'ready'; + } ); + + afterEach( () => { + if ( editor ) { + return editor.destroy(); + } + } ); + + it( 'does not throw if the editor was not initialized using the source element', () => { + delete editor.sourceElement; + + expect( () => { + secureSourceElement( editor ); + } ).to.not.throw(); + } ); + + it( 'does not throw if the editor was initialized using the element for the first time', () => { + expect( () => { + secureSourceElement( editor ); + } ).to.not.throw(); + } ); + + it( 'sets the property after initializing the editor', () => { + secureSourceElement( editor ); + + expect( sourceElement.ckeditorInstance ).to.equal( editor ); + } ); + + it( 'removes the property after destroying the editor', () => { + secureSourceElement( editor ); + + return editor.destroy() + .then( () => { + expect( sourceElement.ckeditorInstance ).to.be.undefined; + } ); + } ); + + it( 'throws an error if the same element was used twice', () => { + sourceElement.ckeditorInstance = 'foo'; + + expectToThrowCKEditorError( () => { + secureSourceElement( editor ); + }, /^editor-source-element-already-used/, editor ); + } ); +} );