瀏覽代碼

Merge pull request #176 from ckeditor/t/ckeditor5/746

Feature: Introduced a `secureSourceElement()` utility that prevents from initialising more than one editor on the same DOM element. See ckeditor/ckeditor5#746.
Maciej 6 年之前
父節點
當前提交
af0fd55d35

+ 3 - 1
packages/ckeditor5-core/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;
+		}
 	}
 
 	/**

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

+ 11 - 0
packages/ckeditor5-core/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()', () => {

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