Browse Source

Merge branch 'master' into t/ckeditor5-watchdog/1

Oskar Wróbel 6 years ago
parent
commit
893544121f

+ 52 - 5
packages/ckeditor5-core/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 EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
 import mix from '@ckeditor/ckeditor5-utils/src/mix';
 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.
  * 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.
 		 * Stores all editable elements used by the editor instance.
 		 *
 		 *
-		 * @protected
+		 * @private
 		 * @member {Map.<String,HTMLElement>}
 		 * @member {Map.<String,HTMLElement>}
 		 */
 		 */
-		this._editableElements = new Map();
+		this._editableElementsMap = new Map();
 
 
 		// Informs UI components that should be refreshed after layout change.
 		// Informs UI components that should be refreshed after layout change.
 		this.listenTo( editor.editing.view.document, 'layoutChanged', () => this.update() );
 		this.listenTo( editor.editing.view.document, 'layoutChanged', () => this.update() );
@@ -100,7 +101,29 @@ export default class EditorUI {
 
 
 		this.focusTracker.destroy();
 		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}
 	 * @returns {HTMLElement|undefined}
 	 */
 	 */
 	getEditableElement( rootName = 'main' ) {
 	getEditableElement( rootName = 'main' ) {
-		return this._editableElements.get( rootName );
+		return this._editableElementsMap.get( rootName );
 	}
 	}
 
 
 	/**
 	/**
@@ -119,7 +142,31 @@ export default class EditorUI {
 	 * @returns {Iterable.<String>}
 	 * @returns {Iterable.<String>}
 	 */
 	 */
 	getEditableElementsNames() {
 	getEditableElementsNames() {
-		return this._editableElements.keys();
+		return this._editableElementsMap.keys();
+	}
+
+	/**
+	 * Stores all editable elements used by the editor instance.
+	 *
+	 * @protected
+	 * @deprecated
+	 * @member {Map.<String,HTMLElement>}
+	 */
+	get _editableElements() {
+		/**
+		 * The {@link module:core/editor/editorui~EditorUI#_editableElements `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.
+		 *
+		 * @error editor-ui-deprecated-editable-elements
+		 * @param {module:core/editor/editorui~EditorUI} editorUI Editor UI instance the deprecated property belongs to.
+		 */
+		log.warn(
+			'editor-ui-deprecated-editable-elements: ' +
+			'The EditorUI#_editableElements property has been deprecated and will be removed in the near future.',
+			{ editorUI: this } );
+
+		return this._editableElementsMap;
 	}
 	}
 
 
 	/**
 	/**

+ 99 - 0
packages/ckeditor5-core/src/multicommand.js

@@ -0,0 +1,99 @@
+/**
+ * @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 Command from './command';
+
+/**
+ * @module core/multicommand
+ */
+
+/**
+ * A CKEditor command that aggregates other commands.
+ *
+ * This command is used to proxy multiple commands. The multi-command is enabled when
+ * at least one of its registered child commands is enabled.
+ * When executing a multi-command the first command that is enabled will be executed.
+ *
+ *		const multiCommand = new MultiCommand( editor );
+ *
+ *		const commandFoo = new Command( editor );
+ *		const commandBar = new Command( editor );
+ *
+ *		// Register child commands.
+ *		multiCommand.registerChildCommand( commandFoo );
+ *		multiCommand.registerChildCommand( commandBar );
+ *
+ *		// Enable one of the commands.
+ *		commandBar.isEnabled = true;
+ *
+ *		multiCommand.execute(); // Will execute commandBar.
+ *
+ * @extends module:core/command~Command
+ */
+export default class MultiCommand extends Command {
+	/**
+	 * @inheritDoc
+	 */
+	constructor( editor ) {
+		super( editor );
+
+		/**
+		 * Registered child commands.
+		 *
+		 * @type {Array.<module:core/command~Command>}
+		 * @private
+		 */
+		this._childCommands = [];
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	refresh() {
+		// Override base command refresh(): the command's state is changed when one of child commands changes states.
+	}
+
+	/**
+	 * Executes the first of it registered child commands.
+	 */
+	execute( ...args ) {
+		const command = this._getFirstEnabledCommand();
+
+		command.execute( args );
+	}
+
+	/**
+	 * Registers a child command.
+	 *
+	 * @param {module:core/command~Command} command
+	 */
+	registerChildCommand( command ) {
+		this._childCommands.push( command );
+
+		// Change multi command enabled state when one of registered commands changes state.
+		command.on( 'change:isEnabled', () => this._checkEnabled() );
+
+		this._checkEnabled();
+	}
+
+	/**
+	 * Checks if any of child commands is enabled.
+	 *
+	 * @private
+	 */
+	_checkEnabled() {
+		this.isEnabled = !!this._getFirstEnabledCommand();
+	}
+
+	/**
+	 * Returns a first enabled command or undefined if none of them is enabled.
+	 *
+	 * @returns {module:core/command~Command|undefined}
+	 * @private
+	 */
+	_getFirstEnabledCommand() {
+		return this._childCommands.find( command => command.isEnabled );
+	}
+}

+ 19 - 0
packages/ckeditor5-core/tests/_utils-tests/virtualtesteditor.js

@@ -41,6 +41,8 @@ describe( 'VirtualTestEditor', () => {
 			return VirtualTestEditor.create( { initialData: '<p>foo</p>', plugins: [ Paragraph ] } )
 			return VirtualTestEditor.create( { initialData: '<p>foo</p>', plugins: [ Paragraph ] } )
 				.then( editor => {
 				.then( editor => {
 					expect( editor.getData() ).to.equal( '<p>foo</p>' );
 					expect( editor.getData() ).to.equal( '<p>foo</p>' );
+
+					return editor.destroy();
 				} );
 				} );
 		} );
 		} );
 
 
@@ -48,6 +50,23 @@ describe( 'VirtualTestEditor', () => {
 			return VirtualTestEditor.create()
 			return VirtualTestEditor.create()
 				.then( editor => {
 				.then( editor => {
 					expect( editor.getData() ).to.equal( '' );
 					expect( editor.getData() ).to.equal( '' );
+
+					return editor.destroy();
+				} );
+		} );
+
+		it( 'fires the `data#ready` event once', () => {
+			const dataReadySpy = sinon.spy();
+
+			const Plugin = function( editor ) {
+				editor.data.on( 'ready', dataReadySpy );
+			};
+
+			return VirtualTestEditor.create( { plugins: [ Plugin ] } )
+				.then( editor => {
+					sinon.assert.calledOnce( dataReadySpy );
+
+					return editor.destroy();
 				} );
 				} );
 		} );
 		} );
 	} );
 	} );

+ 1 - 1
packages/ckeditor5-core/tests/_utils/classictesteditor.js

@@ -139,7 +139,7 @@ class ClassicTestEditorUI extends EditorUI {
 
 
 		view.main.add( view.editable );
 		view.main.add( view.editable );
 
 
-		this._editableElements.set( 'main', view.editable.element );
+		this.setEditableElement( 'main', view.editable.element );
 
 
 		if ( replacementElement ) {
 		if ( replacementElement ) {
 			this._elementReplacer.replace( replacementElement, view.element );
 			this._elementReplacer.replace( replacementElement, view.element );

+ 2 - 5
packages/ckeditor5-core/tests/_utils/virtualtesteditor.js

@@ -33,14 +33,11 @@ export default class VirtualTestEditor extends Editor {
 
 
 			resolve(
 			resolve(
 				editor.initPlugins()
 				editor.initPlugins()
+					.then( () => editor.data.init( config.initialData || '' ) )
 					.then( () => {
 					.then( () => {
-						editor.data.init( config.initialData || '' );
-
-						// Fire `data#ready` event manually as `data#init()` method is not used.
-						editor.data.fire( 'ready' );
 						editor.fire( 'ready' );
 						editor.fire( 'ready' );
+						return editor;
 					} )
 					} )
-					.then( () => editor )
 			);
 			);
 		} );
 		} );
 	}
 	}

+ 56 - 9
packages/ckeditor5-core/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 ComponentFactory from '@ckeditor/ckeditor5-ui/src/componentfactory';
 
 
 import testUtils from '../_utils/utils';
 import testUtils from '../_utils/utils';
+import log from '@ckeditor/ckeditor5-utils/src/log';
 
 
 /* global document */
 /* global document */
 
 
@@ -85,14 +86,50 @@ describe( 'EditorUI', () => {
 		} );
 		} );
 
 
 		it( 'should reset editables array', () => {
 		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();
 			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 ui = new EditorUI( editor );
 			const editableMock = { name: 'main', element: document.createElement( 'div' ) };
 			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 );
 			expect( ui.getEditableElement() ).to.equal( editableMock.element );
 		} );
 		} );
@@ -111,8 +148,8 @@ describe( 'EditorUI', () => {
 			const editableMock1 = { name: 'root1', element: document.createElement( 'div' ) };
 			const editableMock1 = { name: 'root1', element: document.createElement( 'div' ) };
 			const editableMock2 = { name: 'root2', element: document.createElement( 'p' ) };
 			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( 'root1' ) ).to.equal( editableMock1.element );
 			expect( ui.getEditableElement( 'root2' ) ).to.equal( editableMock2.element );
 			expect( ui.getEditableElement( 'root2' ) ).to.equal( editableMock2.element );
@@ -131,8 +168,8 @@ describe( 'EditorUI', () => {
 			const editableMock1 = { name: 'main', element: document.createElement( 'div' ) };
 			const editableMock1 = { name: 'main', element: document.createElement( 'div' ) };
 			const editableMock2 = { name: 'root2', element: document.createElement( 'p' ) };
 			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();
 			const names = ui.getEditableElementsNames();
 			expect( names[ Symbol.iterator ] ).to.instanceof( Function );
 			expect( names[ Symbol.iterator ] ).to.instanceof( Function );
@@ -145,4 +182,14 @@ describe( 'EditorUI', () => {
 			expect( ui.getEditableElementsNames() ).to.be.empty;
 			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' );
+		} );
+	} );
 } );
 } );

+ 124 - 0
packages/ckeditor5-core/tests/multicommand.js

@@ -0,0 +1,124 @@
+/**
+ * @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 MultiCommand from '../src/multicommand';
+import ModelTestEditor from './_utils/modeltesteditor';
+import Command from '../src/command';
+
+describe( 'MultiCommand', () => {
+	let editor, multiCommand;
+
+	beforeEach( () => {
+		return ModelTestEditor
+			.create()
+			.then( newEditor => {
+				editor = newEditor;
+				multiCommand = new MultiCommand( editor );
+			} );
+	} );
+
+	afterEach( () => {
+		multiCommand.destroy();
+
+		return editor.destroy();
+	} );
+
+	describe( 'isEnabled', () => {
+		it( 'is always falsy when no child commands are registered', () => {
+			expect( multiCommand.isEnabled ).to.false;
+
+			multiCommand.refresh();
+
+			expect( multiCommand.isEnabled ).to.false;
+		} );
+
+		it( 'is set to true if one of registered commands is true', () => {
+			expect( multiCommand.isEnabled ).to.false;
+
+			const commandA = new Command( editor );
+			const commandB = new Command( editor );
+
+			multiCommand.registerChildCommand( commandA );
+			multiCommand.registerChildCommand( commandB );
+
+			expect( multiCommand.isEnabled ).to.false;
+
+			commandA.isEnabled = true;
+
+			expect( multiCommand.isEnabled ).to.be.true;
+
+			commandA.isEnabled = false;
+
+			expect( multiCommand.isEnabled ).to.be.false;
+
+			commandB.isEnabled = true;
+
+			expect( multiCommand.isEnabled ).to.be.true;
+		} );
+	} );
+
+	describe( 'execute()', () => {
+		it( 'does not call any command if all are disabled', () => {
+			const commandA = new Command( editor );
+			const commandB = new Command( editor );
+
+			multiCommand.registerChildCommand( commandA );
+			multiCommand.registerChildCommand( commandB );
+
+			const spyA = sinon.spy( commandA, 'execute' );
+			const spyB = sinon.spy( commandB, 'execute' );
+
+			multiCommand.execute();
+
+			sinon.assert.notCalled( spyA );
+			sinon.assert.notCalled( spyB );
+		} );
+
+		it( 'executes enabled command', () => {
+			const commandA = new Command( editor );
+			const commandB = new Command( editor );
+			const commandC = new Command( editor );
+
+			multiCommand.registerChildCommand( commandA );
+			multiCommand.registerChildCommand( commandB );
+			multiCommand.registerChildCommand( commandC );
+
+			const spyA = sinon.spy( commandA, 'execute' );
+			const spyB = sinon.spy( commandB, 'execute' );
+			const spyC = sinon.spy( commandC, 'execute' );
+
+			commandC.isEnabled = true;
+
+			multiCommand.execute();
+
+			sinon.assert.notCalled( spyA );
+			sinon.assert.notCalled( spyB );
+			sinon.assert.calledOnce( spyC );
+		} );
+
+		it( 'executes first registered command if many are enabled', () => {
+			const commandA = new Command( editor );
+			const commandB = new Command( editor );
+			const commandC = new Command( editor );
+
+			multiCommand.registerChildCommand( commandA );
+			multiCommand.registerChildCommand( commandB );
+			multiCommand.registerChildCommand( commandC );
+
+			const spyA = sinon.spy( commandA, 'execute' );
+			const spyB = sinon.spy( commandB, 'execute' );
+			const spyC = sinon.spy( commandC, 'execute' );
+
+			commandC.isEnabled = true;
+			commandB.isEnabled = true;
+
+			multiCommand.execute();
+
+			sinon.assert.notCalled( spyA );
+			sinon.assert.calledOnce( spyB );
+			sinon.assert.notCalled( spyC );
+		} );
+	} );
+} );