Parcourir la source

Initial Scope implementation.

Oskar Wróbel il y a 6 ans
Parent
commit
5c2710cc6c

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

@@ -7,11 +7,11 @@
  * @module core/editor/editor
  */
 
+import Scope from '@ckeditor/ckeditor5-core/src/scope';
 import Config from '@ckeditor/ckeditor5-utils/src/config';
 import EditingController from '@ckeditor/ckeditor5-engine/src/controller/editingcontroller';
 import PluginCollection from '../plugincollection';
 import CommandCollection from '../commandcollection';
-import Locale from '@ckeditor/ckeditor5-utils/src/locale';
 import DataController from '@ckeditor/ckeditor5-engine/src/controller/datacontroller';
 import Conversion from '@ckeditor/ckeditor5-engine/src/conversion/conversion';
 import Model from '@ckeditor/ckeditor5-engine/src/model/model';
@@ -48,9 +48,16 @@ export default class Editor {
 	 *
 	 * Usually, not to be used directly. See the static {@link module:core/editor/editor~Editor.create `create()`} method.
 	 *
-	 * @param {Object} [config] The editor config.
+	 * @param {Object} [config={}] The editor config.
 	 */
-	constructor( config ) {
+	constructor( config = {} ) {
+		/**
+		 * @readonly
+		 * @type {module:core/scope~Scope}
+		 */
+		this.scope = config.scope || new Scope( config );
+		this.scope.addEditor( this );
+
 		const availablePlugins = this.constructor.builtinPlugins;
 
 		/**
@@ -63,7 +70,6 @@ export default class Editor {
 		 * @member {module:utils/config~Config}
 		 */
 		this.config = new Config( config, this.constructor.defaultConfig );
-
 		this.config.define( 'plugins', availablePlugins );
 
 		/**
@@ -74,7 +80,7 @@ export default class Editor {
 		 * @readonly
 		 * @member {module:core/plugincollection~PluginCollection}
 		 */
-		this.plugins = new PluginCollection( this, availablePlugins );
+		this.plugins = new PluginCollection( this, availablePlugins, this.scope.plugins );
 
 		/**
 		 * Commands registered to the editor.
@@ -92,25 +98,6 @@ export default class Editor {
 		 */
 		this.commands = new CommandCollection();
 
-		const languageConfig = this.config.get( 'language' ) || {};
-
-		/**
-		 * @readonly
-		 * @member {module:utils/locale~Locale}
-		 */
-		this.locale = new Locale( {
-			uiLanguage: typeof languageConfig === 'string' ? languageConfig : languageConfig.ui,
-			contentLanguage: this.config.get( 'language.content' )
-		} );
-
-		/**
-		 * Shorthand for {@link module:utils/locale~Locale#t}.
-		 *
-		 * @see module:utils/locale~Locale#t
-		 * @method #t
-		 */
-		this.t = this.locale.t;
-
 		/**
 		 * Indicates the editor life-cycle state.
 		 *
@@ -214,6 +201,24 @@ export default class Editor {
 		this.keystrokes.listenTo( this.editing.view.document );
 	}
 
+	/**
+	 * @readonly
+	 * @type {module:utils/locale~Locale}
+	 */
+	get locale() {
+		return this.scope.locale;
+	}
+
+	/**
+	 * Shorthand for {@link module:utils/locale~Locale#t}.
+	 *
+	 * @see module:utils/locale~Locale#t
+	 * @method #t
+	 */
+	get t() {
+		return this.locale.t;
+	}
+
 	/**
 	 * Loads and initializes plugins specified in the config.
 	 *
@@ -239,6 +244,16 @@ export default class Editor {
 	 * @returns {Promise} A promise that resolves once the editor instance is fully destroyed.
 	 */
 	destroy() {
+		// If editor scope is created by this editor
+		// it is enough to destroy the scope, editor will be destroyed along with it.
+		if ( this.scope ) {
+			if ( this.config.scope ) {
+				return this.scope.destroy();
+			}
+
+			this.scope.removeEditor( this );
+		}
+
 		let readyPromise = Promise.resolve();
 
 		if ( this.state == 'initializing' ) {

+ 36 - 13
packages/ckeditor5-core/src/plugincollection.js

@@ -23,33 +23,36 @@ export default class PluginCollection {
 	/**
 	 * Creates an instance of the PluginCollection class.
 	 * Allows loading and initializing plugins and their dependencies.
+	 * Allows to provide a list of already loaded plugins, these plugins won't be destroyed along with this collection.
 	 *
 	 * @param {module:core/editor/editor~Editor} editor
 	 * @param {Array.<Function>} [availablePlugins] Plugins (constructors) which the collection will be able to use
 	 * when {@link module:core/plugincollection~PluginCollection#init} is used with plugin names (strings, instead of constructors).
 	 * Usually, the editor will pass its built-in plugins to the collection so they can later be
 	 * used in `config.plugins` or `config.removePlugins` by names.
+	 * @param {Iterable.<Array>} externalPlugins List of already initialized plugins represented by a
+	 * `[ PluginConstructor, pluginInstance ]` pair.
 	 */
-	constructor( editor, availablePlugins = [] ) {
+	constructor( editor, availablePlugins = [], externalPlugins = [] ) {
 		/**
 		 * @protected
-		 * @member {module:core/editor/editor~Editor} module:core/plugin~PluginCollection#_editor
+		 * @type {module:core/editor/editor~Editor}
 		 */
 		this._editor = editor;
 
 		/**
-		 * Map of plugin constructors which can be retrieved by their names.
-		 *
 		 * @protected
-		 * @member {Map.<String|Function,Function>} module:core/plugin~PluginCollection#_availablePlugins
+		 * @type {Map}
 		 */
-		this._availablePlugins = new Map();
+		this._plugins = new Map();
 
 		/**
+		 * Map of plugin constructors which can be retrieved by their names.
+		 *
 		 * @protected
-		 * @member {Map} module:core/plugin~PluginCollection#_plugins
+		 * @type {Map.<String|Function,Function>}
 		 */
-		this._plugins = new Map();
+		this._availablePlugins = new Map();
 
 		for ( const PluginConstructor of availablePlugins ) {
 			this._availablePlugins.set( PluginConstructor, PluginConstructor );
@@ -58,6 +61,19 @@ export default class PluginCollection {
 				this._availablePlugins.set( PluginConstructor.pluginName, PluginConstructor );
 			}
 		}
+
+		/**
+		 * List of constructors of externally loaded plugins.
+		 *
+		 * @private
+		 * @type {Map<Function,Function>}
+		 */
+		this._externalPlugins = new Map();
+
+		for ( const [ PluginConstructor, pluginInstance ] of externalPlugins ) {
+			this._externalPlugins.set( PluginConstructor, pluginInstance );
+			this._externalPlugins.set( pluginInstance, PluginConstructor );
+		}
 	}
 
 	/**
@@ -246,6 +262,10 @@ export default class PluginCollection {
 					return promise;
 				}
 
+				if ( that._externalPlugins.has( plugin ) ) {
+					return promise;
+				}
+
 				return promise.then( plugin[ method ].bind( plugin ) );
 			}, Promise.resolve() );
 		}
@@ -278,7 +298,7 @@ export default class PluginCollection {
 					} );
 				}
 
-				const plugin = new PluginConstructor( editor );
+				const plugin = that._externalPlugins.get( PluginConstructor ) || new PluginConstructor( editor );
 				that._add( PluginConstructor, plugin );
 				loaded.push( plugin );
 
@@ -319,10 +339,13 @@ export default class PluginCollection {
 	 * @returns {Promise}
 	 */
 	destroy() {
-		const promises = Array.from( this )
-			.map( ( [ , pluginInstance ] ) => pluginInstance )
-			.filter( pluginInstance => typeof pluginInstance.destroy == 'function' )
-			.map( pluginInstance => pluginInstance.destroy() );
+		const promises = [];
+
+		for ( const [ , pluginInstance ] of this ) {
+			if ( typeof pluginInstance.destroy == 'function' && !this._externalPlugins.has( pluginInstance ) ) {
+				promises.push( pluginInstance.destroy() );
+			}
+		}
 
 		return Promise.all( promises );
 	}

+ 103 - 0
packages/ckeditor5-core/src/scope.js

@@ -0,0 +1,103 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module core/scope
+ */
+
+import Config from '@ckeditor/ckeditor5-utils/src/config';
+import PluginCollection from './plugincollection';
+import Locale from '@ckeditor/ckeditor5-utils/src/locale';
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+
+export default class Scope {
+	constructor( config ) {
+		/**
+		 * Holds all configurations specific to this scope instance.
+		 *
+		 * @readonly
+		 * @type {module:utils/config~Config}
+		 */
+		this.config = new Config( config );
+
+		/**
+		 * The plugins loaded and in use by this scope instance.
+		 *
+		 * @readonly
+		 * @type {module:core/plugincollection~PluginCollection}
+		 */
+		this.plugins = new PluginCollection( this, this.config.get( 'plugins' ) );
+
+		const languageConfig = this.config.get( 'language' ) || {};
+
+		/**
+		 * @readonly
+		 * @member {module:utils/locale~Locale}
+		 */
+		this.locale = new Locale( {
+			uiLanguage: typeof languageConfig === 'string' ? languageConfig : languageConfig.ui,
+			contentLanguage: this.config.get( 'language.content' )
+		} );
+
+		/**
+		 * List of editors used with this scope.
+		 *
+		 * @private
+		 * @type {Set<module:core/editor/editor~Editor>}
+		 */
+		this._editors = new Set();
+	}
+
+	/**
+	 * @param {module:core/editor/editor~Editor} editor
+	 */
+	addEditor( editor ) {
+		this._editors.add( editor );
+	}
+
+	/**
+	 * @param {module:core/editor/editor~Editor} editor
+	 */
+	removeEditor( editor ) {
+		return this._editors.delete( editor );
+	}
+
+	initPlugins() {
+		const plugins = this.config.get( 'plugins' );
+
+		for ( const plugin of plugins ) {
+			if ( typeof plugin != 'function' ) {
+				throw new CKEditorError( 'scope-initplugins: Only constructor is allowed as a Scope plugin' );
+			}
+		}
+
+		return this.plugins.init( plugins );
+	}
+
+	/**
+	 * Destroys the scope instance, releasing all resources used by it.
+	 *
+	 * @returns {Promise} A promise that resolves once the scope instance is fully destroyed.
+	 */
+	destroy() {
+		const promises = [];
+
+		for ( const editor of Array.from( this._editors ) ) {
+			editor.scope = null;
+			promises.push( editor.destroy() );
+		}
+
+		return Promise.all( promises )
+			.then( () => this.plugins.destroy() );
+	}
+
+	static create( config ) {
+		return new Promise( resolve => {
+			const scope = new this( config );
+
+			resolve( scope.initPlugins().then( () => scope ) );
+		} );
+	}
+}

+ 115 - 0
packages/ckeditor5-core/tests/scope.js

@@ -0,0 +1,115 @@
+/**
+ * @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 Scope from '../src/scope';
+import Plugin from '../src/plugin';
+import VirtualTestEditor from './_utils/virtualtesteditor';
+
+describe( 'Scope', () => {
+	it( 'should share the same instance of plugin within editors using scope', async () => {
+		class ScopePluginA extends Plugin {}
+		class ScopePluginB extends Plugin {}
+		class EditorPluginA extends Plugin {}
+
+		const scope = await Scope.create( { plugins: [ ScopePluginA, ScopePluginB ] } );
+		const editorA = await VirtualTestEditor.create( { scope, plugins: [ ScopePluginA, EditorPluginA ] } );
+		const editorB = await VirtualTestEditor.create( { scope, plugins: [ ScopePluginB, EditorPluginA ] } );
+
+		expect( editorA.plugins.get( ScopePluginA ) ).to.equal( scope.plugins.get( ScopePluginA ) );
+		expect( editorA.plugins.has( ScopePluginB ) ).to.equal( false );
+		expect( editorB.plugins.get( ScopePluginB ) ).to.equal( scope.plugins.get( ScopePluginB ) );
+		expect( editorB.plugins.has( ScopePluginA ) ).to.equal( false );
+
+		expect( scope.plugins.has( EditorPluginA ) ).to.equal( false );
+		expect( editorA.plugins.get( EditorPluginA ) ).to.not.equal( editorB.plugins.get( EditorPluginA ) );
+
+		await scope.destroy();
+	} );
+
+	it( 'should share the same instance of plugin within editors using scope (deeps)', async () => {
+		class ScopePluginA extends Plugin {}
+		class ScopePluginB extends Plugin {}
+		class EditorPluginA extends Plugin {
+			static get requires() {
+				return [ ScopePluginA ];
+			}
+		}
+		class EditorPluginB extends Plugin {
+			static get requires() {
+				return [ ScopePluginB ];
+			}
+		}
+
+		const scope = await Scope.create( { plugins: [ ScopePluginA, ScopePluginB ] } );
+		const editorA = await VirtualTestEditor.create( { scope, plugins: [ EditorPluginA ] } );
+		const editorB = await VirtualTestEditor.create( { scope, plugins: [ EditorPluginB ] } );
+
+		expect( scope.plugins.get( ScopePluginA ) ).to.equal( editorA.plugins.get( ScopePluginA ) );
+		expect( scope.plugins.get( ScopePluginB ) ).to.equal( editorB.plugins.get( ScopePluginB ) );
+
+		await scope.destroy();
+	} );
+
+	it( 'should not initialize twice plugin added to the scope and the editor', async () => {
+		const initSpy = sinon.spy();
+		const afterInitSpy = sinon.spy();
+
+		class ScopePluginA extends Plugin {
+			init() {
+				initSpy();
+			}
+
+			afterInit() {
+				afterInitSpy();
+			}
+		}
+
+		const scope = await Scope.create( { plugins: [ ScopePluginA ] } );
+		const editor = await VirtualTestEditor.create( { scope, plugins: [ ScopePluginA ] } );
+
+		expect( scope.plugins.get( ScopePluginA ) ).to.equal( editor.plugins.get( ScopePluginA ) );
+		sinon.assert.calledOnce( initSpy );
+		sinon.assert.calledOnce( afterInitSpy );
+
+		await scope.destroy();
+	} );
+
+	it( 'should not destroy scope plugin when only the editor is destroyed', async () => {
+		const scopePluginDestroySpy = sinon.spy();
+		const editorPluginDestroySpy = sinon.spy();
+
+		class ScopePlugin extends Plugin {
+			destroy() {
+				scopePluginDestroySpy();
+			}
+		}
+
+		class EditorPlugin extends Plugin {
+			destroy() {
+				editorPluginDestroySpy();
+			}
+		}
+
+		const scope = await Scope.create( { plugins: [ ScopePlugin ] } );
+		const editor = await VirtualTestEditor.create( { scope, plugins: [ ScopePlugin, EditorPlugin ] } );
+
+		await editor.destroy();
+
+		sinon.assert.calledOnce( editorPluginDestroySpy );
+		sinon.assert.notCalled( scopePluginDestroySpy );
+	} );
+
+	it( 'should not destroy scope along with editor when scope is injected to the editor', () => {
+
+	} );
+
+	it( 'should destroy all editors along with the scope when scope is injected to them', () => {
+
+	} );
+
+	it( 'should destroy scope along with editor when scope is created by the editor', () => {
+
+	} );
+} );