Jelajahi Sumber

Introduced basic mechanism for detecting and firing creators.

Piotrek Koszuliński 10 tahun lalu
induk
melakukan
8006bc059d

+ 20 - 0
packages/ckeditor5-engine/src/creator.js

@@ -0,0 +1,20 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+/**
+ * Basic creator class.
+ *
+ * @class Creator
+ * @extends Plugin
+ */
+
+CKEDITOR.define( [ 'plugin' ], function( Plugin ) {
+	class Creator extends Plugin {
+	}
+
+	return Creator;
+} );

+ 73 - 6
packages/ckeditor5-engine/src/editor.js

@@ -9,14 +9,17 @@
  * Represents a single editor instance.
  *
  * @class Editor
+ * @extends Model
  */
 
 CKEDITOR.define( [
 	'model',
 	'editorconfig',
 	'plugincollection',
-	'promise'
-], function( Model, EditorConfig, PluginCollection, Promise ) {
+	'promise',
+	'creator',
+	'ckeditorerror'
+], function( Model, EditorConfig, PluginCollection, Promise, Creator, CKEditorError ) {
 	class Editor extends Model {
 		/**
 		 * Creates a new instance of the Editor class.
@@ -56,6 +59,20 @@ CKEDITOR.define( [
 			 * @type {PluginCollection}
 			 */
 			this.plugins = new PluginCollection( this );
+
+			/**
+			 * The chosen creator.
+			 *
+			 * @property {Creator} _creator
+			 * @protected
+			 */
+
+			/**
+			 * The list of detected creators.
+			 *
+			 * @protected
+			 */
+			this._creators = {};
 		}
 
 		/**
@@ -64,7 +81,7 @@ CKEDITOR.define( [
 		 * The initialization consists of the following procedures:
 		 *
 		 *  * Load and initialize the configured plugins.
-		 *  * TODO: Add other procedures here.
+		 *  * Fires the editor creator.
 		 *
 		 * This method should be rarely used as `CKEDITOR.create` calls it one should never use the `Editor` constructor
 		 * directly.
@@ -75,9 +92,10 @@ CKEDITOR.define( [
 			var that = this;
 			var config = this.config;
 
-			return Promise.all( [
-				loadPlugins().then( initPlugins )
-			] );
+			return loadPlugins()
+				.then( initPlugins )
+				.then( findCreators )
+				.then( fireCreator );
 
 			function loadPlugins() {
 				return that.plugins.load( config.plugins );
@@ -103,6 +121,48 @@ CKEDITOR.define( [
 					};
 				}
 			}
+
+			function findCreators() {
+				that.plugins.forEach( function( plugin, name ) {
+					if ( plugin instanceof Creator ) {
+						that._creators[ name ] = plugin;
+					}
+				} );
+			}
+
+			function fireCreator() {
+				// Take the name of the creator to use (config or any of the registered ones).
+				var creatorName = config.creator ? ( 'creator-' + config.creator ) : Object.keys( that._creators )[ 0 ];
+				var creator;
+
+				if ( creatorName ) {
+					// Take the registered class for the given creator name.
+					creator = that._creators[ creatorName ];
+				}
+
+				if ( !creator ) {
+					/**
+					 * The creator has not been found.
+					 *
+					 * * If `creatorName` is defined it means that `config.creator` was configured, but such
+					 * plugin does not exist or it does not implement a creator.
+					 * * If `creatorName` is undefined it means that `config.creator` was not configured and
+					 * that none of the loaded plugins implement a creator.
+					 *
+					 * @error editor-creator-404
+					 * @param {String} creatorName
+					 */
+					throw new CKEditorError(
+						'editor-creator-404: The creator has not been found.',
+						{ creatorName: creatorName }
+					);
+				}
+
+				that._creator = creator;
+
+				// Finally fire the creator. It may be asynchronous, returning a promise.
+				return creator.create();
+			}
 		}
 
 		/**
@@ -110,11 +170,18 @@ CKEDITOR.define( [
 		 * element will be recovered.
 		 *
 		 * @fires destroy
+		 * @returns {Promise} A promise that resolves once the editor instance is fully destroyed.
 		 */
 		destroy() {
+			var that = this;
+
 			this.fire( 'destroy' );
 
 			delete this.element;
+
+			return Promise.resolve().then( function() {
+				return that._creator && that._creator.destroy();
+			} );
 		}
 	}
 

+ 13 - 0
packages/ckeditor5-engine/src/plugincollection.js

@@ -166,6 +166,19 @@ CKEDITOR.define( [
 
 			return this._names[ name ];
 		}
+
+		/**
+		 * Executes the callback for each model in the collection.
+		 *
+		 * @param {Function} callback
+		 * @param {Model} callback.item
+		 * @param {String} callback.name
+		 */
+		forEach( callback ) {
+			for ( var name in this._names ) {
+				callback( this._names[ name ], name );
+			}
+		}
 	}
 
 	return PluginCollection;

+ 54 - 0
packages/ckeditor5-engine/tests/_tools/tools.js

@@ -0,0 +1,54 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals bender, sinon */
+
+'use strict';
+
+( function() {
+	bender.tools.core = {
+		/**
+		 * Defines CKEditor plugin which is a mock of an editor creator.
+		 *
+		 * If `proto` is not set or it does not define `create()` and `destroy()` methods,
+		 * then they will be set to Sinon spies. Therefore the shortest usage is:
+		 *
+		 *	  bender.tools.defineEditorCreatorMock( 'test1' );
+		 *
+		 * The mocked creator is available under:
+		 *
+		 *	  editor.plugins.get( 'creator-thename' );
+		 *
+		 * @param {String} creatorName Name of the creator.
+		 * @param {Object} [proto] Prototype of the creator. Properties from the proto param will
+		 * be copied to the prototype of the creator.
+		 */
+		defineEditorCreatorMock: function( creatorName, proto ) {
+			CKEDITOR.define( 'plugin!creator-' + creatorName, [ 'creator' ], function( Creator ) {
+				return mockCreator( Creator );
+			} );
+
+			function mockCreator( Creator ) {
+				class TestCreator extends Creator {}
+
+				if ( proto ) {
+					for ( var propName in proto ) {
+						TestCreator.prototype[ propName ] = proto[ propName ];
+					}
+				}
+
+				if ( !TestCreator.prototype.create ) {
+					TestCreator.prototype.create = sinon.spy().named( creatorName + '-create' );
+				}
+
+				if ( !TestCreator.prototype.destroy ) {
+					TestCreator.prototype.destroy = sinon.spy().named( creatorName + '-destroy' );
+				}
+
+				return TestCreator;
+			}
+		}
+	};
+} )();

+ 60 - 0
packages/ckeditor5-engine/tests/bender/tools.js

@@ -0,0 +1,60 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-include: ../_tools/tools.js */
+
+'use strict';
+
+var createFn3 = function() {};
+var destroyFn3 = function() {};
+
+bender.tools.core.defineEditorCreatorMock( 'test1' );
+bender.tools.core.defineEditorCreatorMock( 'test2', {
+	foo: 1,
+	bar: 2
+} );
+bender.tools.core.defineEditorCreatorMock( 'test3', {
+	create: createFn3,
+	destroy: destroyFn3
+} );
+
+var modules = bender.amd.require( 'creator', 'plugin!creator-test1', 'plugin!creator-test2', 'plugin!creator-test3' );
+
+///////////////////
+
+describe( 'bender.tools.core.defineEditorCreatorMock()', function() {
+	it( 'should register all creators', function() {
+		var Creator = modules.creator;
+		var TestCreator1 = modules[ 'plugin!creator-test1' ];
+		var TestCreator2 = modules[ 'plugin!creator-test2' ];
+		var TestCreator3 = modules[ 'plugin!creator-test3' ];
+
+		expect( TestCreator1.prototype ).to.be.instanceof( Creator );
+		expect( TestCreator2.prototype ).to.be.instanceof( Creator );
+		expect( TestCreator3.prototype ).to.be.instanceof( Creator );
+	} );
+
+	it( 'should copy properties from the second argument', function() {
+		var TestCreator = modules[ 'plugin!creator-test2' ];
+
+		expect( TestCreator.prototype ).to.have.property( 'foo', 1 );
+		expect( TestCreator.prototype ).to.have.property( 'bar', 2 );
+	} );
+
+	it( 'should create spies for create() and destroy() if not defined', function() {
+		var TestCreator1 = modules[ 'plugin!creator-test1' ];
+		var TestCreator2 = modules[ 'plugin!creator-test2' ];
+		var TestCreator3 = modules[ 'plugin!creator-test3' ];
+
+		expect( TestCreator1.prototype.create ).to.have.property( 'called', false, 'test1.create' );
+		expect( TestCreator1.prototype.destroy ).to.have.property( 'called', false, 'test1.destroy' );
+		expect( TestCreator2.prototype.create ).to.have.property( 'called', false, 'test2.create' );
+		expect( TestCreator2.prototype.destroy ).to.have.property( 'called', false, 'test2.destroy' );
+
+		// Not spies:
+		expect( TestCreator3.prototype ).to.have.property( 'create', createFn3 );
+		expect( TestCreator3.prototype ).to.have.property( 'destroy', destroyFn3 );
+	} );
+} );

+ 13 - 7
packages/ckeditor5-engine/tests/ckeditor/ckeditor.js

@@ -4,12 +4,18 @@
  */
 
 /* globals document */
+/* bender-include: ../_tools/tools.js */
 
 'use strict';
 
 var modules = bender.amd.require( 'ckeditor', 'editor', 'promise', 'config' );
 
 var content = document.getElementById( 'content' );
+var editorConfig = { plugins: 'creator-test' };
+
+before( function() {
+	bender.tools.core.defineEditorCreatorMock( 'test' );
+} );
 
 beforeEach( function() {
 	var CKEDITOR = modules.ckeditor;
@@ -25,14 +31,14 @@ describe( 'create', function() {
 		var CKEDITOR = modules.ckeditor;
 		var Promise = modules.promise;
 
-		expect( CKEDITOR.create( content ) ).to.be.instanceof( Promise );
+		expect( CKEDITOR.create( content, editorConfig ) ).to.be.instanceof( Promise );
 	} );
 
 	it( 'should create a new editor instance', function() {
 		var CKEDITOR = modules.ckeditor;
 		var Editor = modules.editor;
 
-		return CKEDITOR.create( content ).then( function( editor ) {
+		return CKEDITOR.create( content, editorConfig ).then( function( editor ) {
 			expect( editor ).to.be.instanceof( Editor );
 			expect( editor.element ).to.equal( content );
 		} );
@@ -42,7 +48,7 @@ describe( 'create', function() {
 		var CKEDITOR = modules.ckeditor;
 		var Editor = modules.editor;
 
-		return CKEDITOR.create( '.editor' ).then( function( editor ) {
+		return CKEDITOR.create( '.editor', editorConfig ).then( function( editor ) {
 			expect( editor ).to.be.instanceof( Editor );
 			expect( editor.element ).to.equal( document.querySelector( '.editor' ) );
 		} );
@@ -51,7 +57,7 @@ describe( 'create', function() {
 	it( 'should set configurations on the new editor', function() {
 		var CKEDITOR = modules.ckeditor;
 
-		return CKEDITOR.create( content, { test: 1 } ).then( function( editor ) {
+		return CKEDITOR.create( content, { test: 1, plugins: 'creator-test' } ).then( function( editor ) {
 			expect( editor.config.test ).to.equal( 1 );
 		} );
 	} );
@@ -59,7 +65,7 @@ describe( 'create', function() {
 	it( 'should add the editor to the `instances` collection', function() {
 		var CKEDITOR = modules.ckeditor;
 
-		return CKEDITOR.create( content ).then( function( editor ) {
+		return CKEDITOR.create( content, editorConfig ).then( function( editor ) {
 			expect( CKEDITOR.instances ).to.have.length( 1 );
 			expect( CKEDITOR.instances.get( 0 ) ).to.equal( editor );
 		} );
@@ -70,11 +76,11 @@ describe( 'create', function() {
 		var editor1, editor2;
 
 		// Create the first editor.
-		return CKEDITOR.create( content ).then( function( editor ) {
+		return CKEDITOR.create( content, editorConfig ).then( function( editor ) {
 			editor1 = editor;
 
 			// Create the second editor.
-			return CKEDITOR.create( '.editor' ).then( function( editor ) {
+			return CKEDITOR.create( '.editor', editorConfig ).then( function( editor ) {
 				editor2 = editor;
 
 				// It should have 2 editors.

+ 136 - 0
packages/ckeditor5-engine/tests/editor/creator.js

@@ -0,0 +1,136 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+/* globals document */
+/* bender-include: ../_tools/tools.js */
+
+var modules = bender.amd.require( 'editor', 'plugin', 'promise', 'creator', 'ckeditorerror' );
+var editor, element;
+
+function initEditor( config ) {
+	var Editor = modules.editor;
+
+	element = document.createElement( 'div' );
+	document.body.appendChild( element );
+
+	editor = new Editor( element, config );
+
+	return editor.init();
+}
+
+bender.tools.createSinonSandbox();
+
+before( function() {
+	bender.tools.core.defineEditorCreatorMock( 'test1' );
+
+	bender.tools.core.defineEditorCreatorMock( 'test-any1' );
+	bender.tools.core.defineEditorCreatorMock( 'test-any2' );
+
+	bender.tools.core.defineEditorCreatorMock( 'test-config1' );
+	bender.tools.core.defineEditorCreatorMock( 'test-config2' );
+
+	CKEDITOR.define( 'plugin!test3', [ 'plugin' ], function( Plugin ) {
+		return class extends Plugin {};
+	} );
+} );
+
+afterEach( function() {
+	editor = null; // To make sure we're using the freshly inited editor.
+} );
+
+///////////////////
+
+describe( 'init', function() {
+	it( 'should instantiate the creator and call create()', function() {
+		var Creator = modules.creator;
+
+		return initEditor( {
+				plugins: 'creator-test1'
+			} )
+			.then( function() {
+				var creator = editor.plugins.get( 'creator-test1' );
+
+				expect( creator ).to.be.instanceof( Creator );
+
+				// The create method has been called.
+				sinon.assert.calledOnce( creator.create );
+			} );
+	} );
+
+	it( 'should instantiate any creator when more than one is available', function() {
+		return initEditor( {
+				plugins: 'creator-test-any1,creator-test-any2'
+			} )
+			.then( function() {
+				var creator1 = editor.plugins.get( 'creator-test-any1' );
+				var creator2 = editor.plugins.get( 'creator-test-any2' );
+
+				expect( creator1.create.called + creator2.create.called ).to.be.equal( 1, 'only one of the creators should be used' );
+			} );
+	} );
+
+	it( 'should use the creator specified in config.creator', function() {
+		return initEditor( {
+				creator: 'test-config2',
+				plugins: 'creator-test-config1,creator-test-config2',
+			} )
+			.then( function() {
+				var creator1 = editor.plugins.get( 'creator-test-config1' );
+				var creator2 = editor.plugins.get( 'creator-test-config2' );
+
+				sinon.assert.calledOnce( creator2.create );
+				sinon.assert.notCalled( creator1.create );
+			} );
+	} );
+
+	it( 'should throw an error if the creator doesn\'t exist', function() {
+		var CKEditorError = modules.ckeditorerror;
+
+		return initEditor( {
+				creator: 'bad',
+				plugins: 'creator-test1'
+			} )
+			.then( function() {
+				throw new Error( 'This should not be executed.' );
+			} )
+			.catch( function( err ) {
+				expect( err ).to.be.instanceof( CKEditorError );
+				expect( err.message ).to.match( /^editor-creator-404:/ );
+			} );
+	} );
+
+	it( 'should throw an error no creators are defined', function() {
+		var CKEditorError = modules.ckeditorerror;
+
+		return initEditor( {} )
+			.then( function() {
+				throw new Error( 'This should not be executed.' );
+			} )
+			.catch( function( err ) {
+				expect( err ).to.be.instanceof( CKEditorError );
+				expect( err.message ).to.match( /^editor-creator-404:/ );
+			} );
+	} );
+} );
+
+describe( 'destroy', function() {
+	it( 'should call "destroy" on the creator', function() {
+		var creator1;
+
+		return initEditor( {
+				plugins: 'creator-test1'
+			} )
+			.then( function() {
+				creator1 = editor.plugins.get( 'creator-test1' );
+
+				return editor.destroy();
+			} )
+			.then( function() {
+				sinon.assert.calledOnce( creator1.destroy );
+			} );
+	} );
+} );

+ 25 - 12
packages/ckeditor5-engine/tests/editor/editor.js

@@ -4,6 +4,7 @@
  */
 
 /* globals document */
+/* bender-include: ../_tools/tools.js */
 
 'use strict';
 
@@ -24,6 +25,9 @@ beforeEach( function() {
 
 before( function() {
 	// Define fake plugins to be used in tests.
+	bender.tools.core.defineEditorCreatorMock( 'test', {
+		init: sinon.spy().named( 'creator-test' )
+	} );
 
 	CKEDITOR.define( 'plugin!A', [ 'plugin' ], pluginDefinition( 'A' ) );
 
@@ -84,6 +88,11 @@ describe( 'config', function() {
 describe( 'init', function() {
 	it( 'should return a promise that resolves properly', function() {
 		var Promise = modules.promise;
+		var Editor = modules.editor;
+
+		editor = new Editor( element, {
+			plugins: 'creator-test'
+		} );
 
 		var promise = editor.init();
 
@@ -97,16 +106,17 @@ describe( 'init', function() {
 		var Plugin = modules.plugin;
 
 		editor = new Editor( element, {
-			plugins: 'A,B'
+			plugins: 'A,B,creator-test'
 		} );
 
 		expect( editor.plugins.length ).to.equal( 0 );
 
 		return editor.init().then( function() {
-			expect( editor.plugins.length ).to.equal( 2 );
+			expect( editor.plugins.length ).to.equal( 3 );
 
 			expect( editor.plugins.get( 'A' ) ).to.be.an.instanceof( Plugin );
 			expect( editor.plugins.get( 'B' ) ).to.be.an.instanceof( Plugin );
+			expect( editor.plugins.get( 'creator-test' ) ).to.be.an.instanceof( Plugin );
 		} );
 	} );
 
@@ -114,11 +124,12 @@ describe( 'init', function() {
 		var Editor = modules.editor;
 
 		editor = new Editor( element, {
-			plugins: 'A,D'
+			plugins: 'creator-test,A,D'
 		} );
 
 		return editor.init().then( function() {
 			sinon.assert.callOrder(
+				editor.plugins.get( 'creator-test' ).init,
 				editor.plugins.get( 'A' ).init,
 				editor.plugins.get( 'B' ).init,
 				editor.plugins.get( 'C' ).init,
@@ -131,14 +142,16 @@ describe( 'init', function() {
 		var Editor = modules.editor;
 
 		editor = new Editor( element, {
-			plugins: 'A,F'
+			plugins: 'creator-test,A,F'
 		} );
 
 		return editor.init().then( function() {
 			sinon.assert.callOrder(
+				editor.plugins.get( 'creator-test' ).init,
 				editor.plugins.get( 'A' ).init,
 				editor.plugins.get( 'async' ).init,
-				asyncSpy,	// This one is called with delay by the async init
+				// This one is called with delay by the async init
+				asyncSpy,
 				editor.plugins.get( 'F' ).init
 			);
 		} );
@@ -148,7 +161,7 @@ describe( 'init', function() {
 		var Editor = modules.editor;
 
 		editor = new Editor( element, {
-			plugins: 'E'
+			plugins: 'E,creator-test'
 		} );
 
 		return editor.init();
@@ -167,14 +180,14 @@ describe( 'destroy', function() {
 
 		editor.on( 'destroy', spy );
 
-		editor.destroy();
-
-		sinon.assert.called( spy );
+		return editor.destroy().then( function() {
+			sinon.assert.called( spy );
+		} );
 	} );
 
 	it( 'should delete the "element" property', function() {
-		editor.destroy();
-
-		expect( editor ).to.not.have.property( 'element' );
+		return editor.destroy().then( function() {
+			expect( editor ).to.not.have.property( 'element' );
+		} );
 	} );
 } );