8
0
Просмотр исходного кода

Merge pull request #48 from ckeditor/t/42

Implemented editor events and Plugin#afterInit support
Aleksander Nowodzinski 9 лет назад
Родитель
Сommit
d1f7dd13bb

+ 39 - 5
packages/ckeditor5-core/src/editor/editor.js

@@ -109,15 +109,19 @@ export default class Editor {
 		const config = this.config;
 
 		return loadPlugins()
-			.then( initPlugins );
+			.then( ( loadedPlugins ) => {
+				return initPlugins( loadedPlugins, 'init' )
+					.then( () => initPlugins( loadedPlugins, 'afterInit' ) );
+			} )
+			.then( () => this.fire( 'pluginsReady' ) );
 
 		function loadPlugins() {
 			return that.plugins.load( config.get( 'plugins' ) || [] );
 		}
 
-		function initPlugins( loadedPlugins ) {
+		function initPlugins( loadedPlugins, method ) {
 			return loadedPlugins.reduce( ( promise, plugin ) => {
-				return promise.then( plugin.init.bind( plugin ) );
+				return promise.then( plugin[ method ].bind( plugin ) );
 			}, Promise.resolve() );
 		}
 	}
@@ -163,9 +167,9 @@ export default class Editor {
 	/**
 	 * Creates a basic editor instance.
 	 *
-	 * @param {Object} config See {@link module:core/editor/standardeditor~StandardEditor}'s param.
+	 * @param {Object} config See {@link module:core/editor/editor~Editor}'s param.
 	 * @returns {Promise} Promise resolved once editor is ready.
-	 * @returns {module:core/editor/standardeditor~StandardEditor} return.editor The editor instance.
+	 * @returns {module:core/editor/editor~Editor} return.editor The editor instance.
 	 */
 	static create( config ) {
 		return new Promise( ( resolve ) => {
@@ -173,6 +177,10 @@ export default class Editor {
 
 			resolve(
 				editor.initPlugins()
+					.then( () => {
+						editor.fire( 'dataReady' );
+						editor.fire( 'ready' );
+					} )
 					.then( () => editor )
 			);
 		} );
@@ -181,6 +189,32 @@ export default class Editor {
 
 mix( Editor, EmitterMixin );
 
+/**
+ * Fired after {@link core.editor.Editor#initPlugins plugins are initialized}.
+ *
+ * @event core.editor.Editor#pluginsReady
+ */
+
+/**
+ * Fired when the editor UI is ready. This event won't be fired if the editor has no UI.
+ *
+ * @event core.editor.Editor#uiReady
+ */
+
+/**
+ * Fired when the data loaded to the editor is ready. If a specific editor doesn't load
+ * any data initially, this event will be fired right before {@link #ready}.
+ *
+ * @event core.editor.Editor#dataReady
+ */
+
+/**
+ * Fired when {@link #pluginsReady plugins}, {@link #uiReady UI} and {@link #dataReady data} and all additional
+ * editor components are ready.
+ *
+ * @event core.editor.Editor#ready
+ */
+
 /**
  * Fired when this editor instance is destroyed. The editor at this point is not usable and this event should be used to
  * perform the clean-up in any plugin.

+ 4 - 0
packages/ckeditor5-core/src/editor/standardeditor.js

@@ -113,6 +113,10 @@ export default class StandardEditor extends Editor {
 
 			resolve(
 				editor.initPlugins()
+					.then( () => {
+						editor.fire( 'dataReady' );
+						editor.fire( 'ready' );
+					} )
 					.then( () => editor )
 			);
 		} );

+ 22 - 2
packages/ckeditor5-core/src/plugin.js

@@ -17,7 +17,16 @@ import mix from '../utils/mix.js';
  */
 export default class Plugin {
 	/**
-	 * Creates a new Plugin instance.
+	 * Creates a new Plugin instance. This is the first step of a plugin initialization.
+	 * See also {@link #init} and {@link #afterInit}.
+	 *
+	 * A plugin is always instantiated after its {@link #requires dependencies} and the
+	 * {@link #init} and {@link #afterInit} methods are called in the same order.
+	 *
+	 * Usually, you'll want to put your plugin's initialization code in the {@link #init} method.
+	 * The constructor can be understood as "before init" and used in special cases, just like
+	 * {@link #afterInit} servers for the special "after init" scenarios (e.g. code which depends on other
+	 * plugins, but which doesn't {@link #requires explicitly require} them).
 	 *
 	 * @param {module:core/editor/editor~Editor} editor
 	 */
@@ -69,12 +78,23 @@ export default class Plugin {
 	 */
 
 	/**
-	 * Initializes the plugin.
+	 * The second stage (after plugin {@link #constructor}) of plugin initialization.
+	 * Unlike the plugin constructor this method can perform asynchronous.
+	 *
+	 * A plugin's `init()` method is called after its {@link #requires dependencies} are initialized,
+	 * so in the same order as constructors of these plugins.
 	 *
 	 * @returns {null|Promise}
 	 */
 	init() {}
 
+	/**
+	 * The third (and last) stage of plugin initialization. See also {@link #constructor} and {@link #init}.
+	 *
+	 * @returns {null|Promise}
+	 */
+	afterInit() {}
+
 	/**
 	 * Destroys the plugin.
 	 *

+ 24 - 0
packages/ckeditor5-core/tests/_utils-tests/classictesteditor.js

@@ -80,6 +80,30 @@ describe( 'ClassicTestEditor', () => {
 					expect( getData( editor.document, { withoutSelection: true } ) ).to.equal( 'foo' );
 				} );
 		} );
+
+		it( 'fires all events in the right order', () => {
+			const fired = [];
+
+			function spy( evt ) {
+				fired.push( evt.name );
+			}
+
+			class EventWatcher extends Plugin {
+				init() {
+					this.editor.on( 'pluginsReady', spy );
+					this.editor.on( 'uiReady', spy );
+					this.editor.on( 'dataReady', spy );
+					this.editor.on( 'ready', spy );
+				}
+			}
+
+			return ClassicTestEditor.create( editorElement, {
+					plugins: [ EventWatcher ]
+				} )
+				.then( () => {
+					expect( fired ).to.deep.equal( [ 'pluginsReady', 'uiReady', 'dataReady', 'ready' ] );
+				} );
+		} );
 	} );
 
 	describe( 'destroy', () => {

+ 26 - 0
packages/ckeditor5-core/tests/_utils-tests/modeltesteditor.js

@@ -5,7 +5,10 @@
 
 import Editor from 'ckeditor5/core/editor/editor.js';
 import ModelTestEditor from 'tests/core/_utils/modeltesteditor.js';
+
+import Plugin from 'ckeditor5/core/plugin.js';
 import HtmlDataProcessor from 'ckeditor5/engine/dataprocessor/htmldataprocessor.js';
+
 import { getData, setData } from 'ckeditor5/engine/dev-utils/model.js';
 
 import testUtils from 'tests/core/_utils/utils.js';
@@ -39,6 +42,29 @@ describe( 'ModelTestEditor', () => {
 					expect( editor.config.get( 'foo' ) ).to.equal( 1 );
 				} );
 		} );
+
+		it( 'fires all events in the right order', () => {
+			const fired = [];
+
+			function spy( evt ) {
+				fired.push( evt.name );
+			}
+
+			class EventWatcher extends Plugin {
+				init() {
+					this.editor.on( 'pluginsReady', spy );
+					this.editor.on( 'dataReady', spy );
+					this.editor.on( 'ready', spy );
+				}
+			}
+
+			return ModelTestEditor.create( {
+					plugins: [ EventWatcher ]
+				} )
+				.then( () => {
+					expect( fired ).to.deep.equal( [ 'pluginsReady', 'dataReady', 'ready' ] );
+				} );
+		} );
 	} );
 
 	describe( 'setData', () => {

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

@@ -5,6 +5,8 @@
 
 import StandardEditor from 'ckeditor5/core/editor/standardeditor.js';
 import VirtualTestEditor from 'tests/core/_utils/virtualtesteditor.js';
+
+import Plugin from 'ckeditor5/core/plugin.js';
 import HtmlDataProcessor from 'ckeditor5/engine/dataprocessor/htmldataprocessor.js';
 
 import testUtils from 'tests/core/_utils/utils.js';
@@ -39,5 +41,28 @@ describe( 'VirtualTestEditor', () => {
 					expect( editor.config.get( 'foo' ) ).to.equal( 1 );
 				} );
 		} );
+
+		it( 'fires all events in the right order', () => {
+			const fired = [];
+
+			function spy( evt ) {
+				fired.push( evt.name );
+			}
+
+			class EventWatcher extends Plugin {
+				init() {
+					this.editor.on( 'pluginsReady', spy );
+					this.editor.on( 'dataReady', spy );
+					this.editor.on( 'ready', spy );
+				}
+			}
+
+			return VirtualTestEditor.create( {
+					plugins: [ EventWatcher ]
+				} )
+				.then( () => {
+					expect( fired ).to.deep.equal( [ 'pluginsReady', 'dataReady', 'ready' ] );
+				} );
+		} );
 	} );
 } );

+ 5 - 0
packages/ckeditor5-core/tests/_utils/classictesteditor.js

@@ -46,7 +46,12 @@ export default class ClassicTestEditor extends StandardEditor {
 			resolve(
 				editor.initPlugins()
 					.then( () => editor.ui.init() )
+					.then( () => editor.fire( 'uiReady' ) )
 					.then( () => editor.loadDataFromEditorElement() )
+					.then( () => {
+						editor.fire( 'dataReady' );
+						editor.fire( 'ready' );
+					} )
 					.then( () => editor )
 			);
 		} );

+ 4 - 0
packages/ckeditor5-core/tests/_utils/modeltesteditor.js

@@ -52,6 +52,10 @@ export default class ModelTestEditor extends Editor {
 
 			resolve(
 				editor.initPlugins()
+					.then( () => {
+						editor.fire( 'dataReady' );
+						editor.fire( 'ready' );
+					} )
 					.then( () => editor )
 			);
 		} );

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

@@ -38,6 +38,10 @@ export default class VirtualTestEditor extends StandardEditor {
 
 			resolve(
 				editor.initPlugins()
+					.then( () => {
+						editor.fire( 'dataReady' );
+						editor.fire( 'ready' );
+					} )
 					.then( () => editor )
 			);
 		} );

+ 86 - 2
packages/ckeditor5-core/tests/editor/editor.js

@@ -15,28 +15,35 @@ class PluginA extends Plugin {
 	constructor( editor ) {
 		super( editor );
 		this.init = sinon.spy().named( 'A' );
+		this.afterInit = sinon.spy().named( 'A-after' );
 	}
 }
+
 class PluginB extends Plugin {
 	constructor( editor ) {
 		super( editor );
 		this.init = sinon.spy().named( 'B' );
+		this.afterInit = sinon.spy().named( 'B-after' );
 	}
 }
+
 class PluginC extends Plugin {
 	constructor( editor ) {
 		super( editor );
 		this.init = sinon.spy().named( 'C' );
+		this.afterInit = sinon.spy().named( 'C-after' );
 	}
 
 	static get requires() {
 		return [ PluginB ];
 	}
 }
+
 class PluginD extends Plugin {
 	constructor( editor ) {
 		super( editor );
 		this.init = sinon.spy().named( 'D' );
+		this.afterInit = sinon.spy().named( 'D-after' );
 	}
 
 	static get requires() {
@@ -84,6 +91,29 @@ describe( 'Editor', () => {
 					expect( editor.plugins.get( PluginA ) ).to.be.an.instanceof( Plugin );
 				} );
 		} );
+
+		it( 'fires all events in the right order', () => {
+			const fired = [];
+
+			function spy( evt ) {
+				fired.push( evt.name );
+			}
+
+			class EventWatcher extends Plugin {
+				init() {
+					this.editor.on( 'pluginsReady', spy );
+					this.editor.on( 'dataReady', spy );
+					this.editor.on( 'ready', spy );
+				}
+			}
+
+			return Editor.create( {
+					plugins: [ EventWatcher ]
+				} )
+				.then( () => {
+					expect( fired ).to.deep.equal( [ 'pluginsReady', 'dataReady', 'ready' ] );
+				} );
+		} );
 	} );
 
 	describe( 'initPlugins', () => {
@@ -107,17 +137,25 @@ describe( 'Editor', () => {
 				plugins: [ PluginA, PluginD ]
 			} );
 
+			const pluginsReadySpy = sinon.spy().named( 'pluginsReady' );
+			editor.on( 'pluginsReady', pluginsReadySpy );
+
 			return editor.initPlugins().then( () => {
 				sinon.assert.callOrder(
 					editor.plugins.get( PluginA ).init,
 					editor.plugins.get( PluginB ).init,
 					editor.plugins.get( PluginC ).init,
-					editor.plugins.get( PluginD ).init
+					editor.plugins.get( PluginD ).init,
+					editor.plugins.get( PluginA ).afterInit,
+					editor.plugins.get( PluginB ).afterInit,
+					editor.plugins.get( PluginC ).afterInit,
+					editor.plugins.get( PluginD ).afterInit,
+					pluginsReadySpy
 				);
 			} );
 		} );
 
-		it( 'should initialize plugins in the right order, waiting for asynchronous ones', () => {
+		it( 'should initialize plugins in the right order, waiting for asynchronous init()', () => {
 			const asyncSpy = sinon.spy().named( 'async-call-spy' );
 
 			// Synchronous plugin that depends on an asynchronous one.
@@ -161,6 +199,52 @@ describe( 'Editor', () => {
 				);
 			} );
 		} );
+
+		it( 'should initialize plugins in the right order, waiting for asynchronous afterInit()', () => {
+			const asyncSpy = sinon.spy().named( 'async-call-spy' );
+
+			// Synchronous plugin that depends on an asynchronous one.
+			class PluginSync extends Plugin {
+				constructor( editor ) {
+					super( editor );
+					this.afterInit = sinon.spy().named( 'sync' );
+				}
+
+				static get requires() {
+					return [ PluginAsync ];
+				}
+			}
+
+			class PluginAsync extends Plugin {
+				constructor( editor ) {
+					super( editor );
+
+					this.afterInit = sinon.spy( () => {
+						return new Promise( ( resolve ) => {
+							setTimeout( () => {
+								asyncSpy();
+								resolve();
+							}, 0 );
+						} );
+					} );
+				}
+			}
+
+			const editor = new Editor( {
+				plugins: [ PluginA, PluginSync ]
+			} );
+
+			return editor.initPlugins().then( () => {
+				sinon.assert.callOrder(
+					editor.plugins.get( PluginA ).afterInit,
+					editor.plugins.get( PluginAsync ).afterInit,
+
+					// This one is called with delay by the async init.
+					asyncSpy,
+					editor.plugins.get( PluginSync ).afterInit
+				);
+			} );
+		} );
 	} );
 } );
 

+ 23 - 0
packages/ckeditor5-core/tests/editor/standardeditor.js

@@ -55,6 +55,29 @@ describe( 'StandardEditor', () => {
 					expect( editor.plugins.get( PluginFoo ) ).to.be.instanceof( PluginFoo );
 				} );
 		} );
+
+		it( 'fires all events in the right order', () => {
+			const fired = [];
+
+			function spy( evt ) {
+				fired.push( evt.name );
+			}
+
+			class EventWatcher extends Plugin {
+				init() {
+					this.editor.on( 'pluginsReady', spy );
+					this.editor.on( 'dataReady', spy );
+					this.editor.on( 'ready', spy );
+				}
+			}
+
+			return StandardEditor.create( editorElement, {
+					plugins: [ EventWatcher ]
+				} )
+				.then( () => {
+					expect( fired ).to.deep.equal( [ 'pluginsReady', 'dataReady', 'ready' ] );
+				} );
+		} );
 	} );
 
 	describe( 'setData', () => {