Skip to content
This repository has been archived by the owner on Jun 26, 2020. It is now read-only.

Commit

Permalink
Merge pull request #204 from ckeditor/context
Browse files Browse the repository at this point in the history
Feature: Introduced the concept of editor contexts and context plugins. Contexts provide a common, higher-level environment for solutions which use multiple editors and/or plugins that work outside an editor. Closes ckeditor/ckeditor5#5891.
  • Loading branch information
Reinmar authored Jan 16, 2020
2 parents 8376e9c + e96aeed commit 672e55e
Show file tree
Hide file tree
Showing 12 changed files with 971 additions and 81 deletions.
287 changes: 287 additions & 0 deletions src/context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
/**
* @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/context
*/

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';

/**
* Provides a common, higher level environment for solutions which use multiple {@link module:core/editor/editor~Editor editors}
* or/and plugins that work outside of an editor. Use it instead of {@link module:core/editor/editor~Editor.create `Editor.create()`}
* in advanced application integrations.
*
* All configuration options passed to a `Context` will be used as default options for editor instances initialized in that context.
*
* {@link module:core/contextplugin~ContextPlugin `ContextPlugin`s} passed to a `Context` instance will be shared among all
* editor instances initialized in that context. These will be the same plugin instances for all the editors.
*
* **Note:** `Context` can only be initialized with {@link module:core/contextplugin~ContextPlugin `ContextPlugin`s}
* (e.g. [comments](https://ckeditor.com/collaboration/comments/)). Regular {@link module:core/plugin~Plugin `Plugin`s} require an
* editor instance to work and cannot be added to a `Context`.
*
* **Note:** You can add `ContextPlugin` to an editor instance, though.
*
* If you are using multiple editor instances on one page and use any `ContextPlugin`s, create `Context` to share configuration and plugins
* among those editors. Some plugins will use the information about all existing editors to better integrate between them.
*
* If you are using plugins that do not require an editor to work (e.g. [comments](https://ckeditor.com/collaboration/comments/))
* enable and configure them using `Context`.
*
* If you are using only a single editor on each page use {@link module:core/editor/editor~Editor.create `Editor.create()`} instead.
* In such case, `Context` instance will be created by the editor instance in a transparent way.
*
* See {@link module:core/context~Context.create `Context.create()`} for usage examples.
*/
export default class Context {
/**
* Creates a context instance with a given configuration.
*
* Usually, not to be used directly. See the static {@link module:core/context~Context.create `create()`} method.
*
* @param {Object} [config={}] The context config.
*/
constructor( config ) {
/**
* Holds all configurations specific to this context instance.
*
* @readonly
* @type {module:utils/config~Config}
*/
this.config = new Config( config );

/**
* The plugins loaded and in use by this context instance.
*
* @readonly
* @type {module:core/plugincollection~PluginCollection}
*/
this.plugins = new PluginCollection( this );

const languageConfig = this.config.get( 'language' ) || {};

/**
* @readonly
* @type {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;

/**
* List of editors to which this context instance is injected.
*
* @private
* @type {Set.<module:core/editor/editor~Editor>}
*/
this._editors = new Set();

/**
* Reference to the editor which created the context.
* Null when the context was created outside of the editor.
*
* It is used to destroy the context when removing the editor that has created the context.
*
* @private
* @type {module:core/editor/editor~Editor|null}
*/
this._contextOwner = null;
}

/**
* Loads and initializes plugins specified in the config.
*
* @returns {Promise.<module:core/plugin~LoadedPlugins>} A promise which resolves
* once the initialization is completed providing an array of loaded plugins.
*/
initPlugins() {
const plugins = this.config.get( 'plugins' ) || [];

for ( const Plugin of plugins ) {
if ( typeof Plugin != 'function' ) {
/**
* Only constructor is allowed as a {@link module:core/contextplugin~ContextPlugin}.
*
* @error context-initplugins-constructor-only
*/
throw new CKEditorError(
'context-initplugins-constructor-only: Only constructor is allowed as a Context plugin.',
null,
{ Plugin }
);
}

if ( Plugin.isContextPlugin !== true ) {
/**
* Only plugin marked as a {@link module:core/contextplugin~ContextPlugin} is allowed to be used with a context.
*
* @error context-initplugins-invalid-plugin
*/
throw new CKEditorError(
'context-initplugins-invalid-plugin: Only plugin marked as a ContextPlugin is allowed.',
null,
{ Plugin }
);
}
}

return this.plugins.init( plugins );
}

/**
* Destroys the context instance, and all editors used with the context.
* Releasing all resources used by the context.
*
* @returns {Promise} A promise that resolves once the context instance is fully destroyed.
*/
destroy() {
return Promise.all( Array.from( this._editors, editor => editor.destroy() ) )
.then( () => this.plugins.destroy() );
}

/**
* Adds a reference to the editor which is used with this context.
*
* When the given editor has created the context then the reference to this editor will be stored
* as a {@link ~Context#_contextOwner}.
*
* This method should be used only by the editor.
*
* @protected
* @param {module:core/editor/editor~Editor} editor
* @param {Boolean} isContextOwner Stores the given editor as a context owner.
*/
_addEditor( editor, isContextOwner ) {
if ( this._contextOwner ) {
/**
* Cannot add multiple editors to the context which is created by the editor.
*
* @error context-addEditor-private-context
*/
throw new CKEditorError(
'context-addEditor-private-context: Cannot add multiple editors to the context which is created by the editor.'
);
}

this._editors.add( editor );

if ( isContextOwner ) {
this._contextOwner = editor;
}
}

/**
* Removes a reference to the editor which was used with this context.
* When the context was created by the given editor then the context will be destroyed.
*
* This method should be used only by the editor.
*
* @protected
* @param {module:core/editor/editor~Editor} editor
* @return {Promise} A promise that resolves once the editor is removed from the context or when the context has been destroyed.
*/
_removeEditor( editor ) {
this._editors.delete( editor );

if ( this._contextOwner === editor ) {
return this.destroy();
}

return Promise.resolve();
}

/**
* Returns context configuration which will be copied to editors created using this context.
*
* The configuration returned by this method has removed plugins configuration - plugins are shared with all editors
* through another mechanism.
*
* This method should be used only by the editor.
*
* @protected
* @returns {Object} Configuration as a plain object.
*/
_getEditorConfig() {
const result = {};

for ( const name of this.config.names() ) {
if ( ![ 'plugins', 'removePlugins', 'extraPlugins' ].includes( name ) ) {
result[ name ] = this.config.get( name );
}
}

return result;
}

/**
* Creates and initializes a new context instance.
*
* const commonConfig = { ... }; // Configuration for all the plugins and editors.
* const editorPlugins = [ ... ]; // Regular `Plugin`s here.
*
* Context
* .create( {
* // Only `ContextPlugin`s here.
* plugins: [ ... ],
*
* // Configure language for all the editors (it cannot be overwritten).
* language: { ... },
*
* // Configuration for context plugins.
* comments: { ... },
* ...
*
* // Default configuration for editor plugins.
* toolbar: { ... },
* image: { ... },
* ...
* } )
* .then( context => {
* const promises = [];
*
* promises.push( ClassicEditor.create(
* document.getElementById( 'editor1' ),
* {
* editorPlugins,
* context
* }
* ) );
*
* promises.push( ClassicEditor.create(
* document.getElementById( 'editor2' ),
* {
* editorPlugins,
* context,
* toolbar: { ... } // You can overwrite context's configuration.
* }
* ) );
*
* return Promise.all( promises );
* } );
*
* @param {Object} [config] The context config.
* @returns {Promise} A promise resolved once the context is ready. The promise resolves with the created context instance.
*/
static create( config ) {
return new Promise( resolve => {
const context = new this( config );

resolve( context.initPlugins().then( () => context ) );
} );
}
}
61 changes: 61 additions & 0 deletions src/contextplugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* @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/contextplugin
*/

import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
import mix from '@ckeditor/ckeditor5-utils/src/mix';

/**
* The base class for {@link module:core/context~Context} plugin classes.
*
* A context plugin can either be initialized for an {@link module:core/editor/editor~Editor editor} or for
* a {@link module:core/context~Context context}. In other words, it can either
* work within one editor instance or with one or more editor instances that use a single context.
* It is the context plugin's role to implement handling for both modes.
*
* A couple of rules for interaction between editor plugins and context plugins:
*
* * a context plugin can require another context plugin,
* * an {@link module:core/plugin~Plugin editor plugin} can require a context plugin,
* * a context plugin MUST NOT require an {@link module:core/plugin~Plugin editor plugin}.
*
* @implements module:core/plugin~PluginInterface
* @mixes module:utils/observablemixin~ObservableMixin
*/
export default class ContextPlugin {
/**
* Creates a new plugin instance.
*
* @param {module:core/context~Context|module:core/editor/editor~Editor} context
*/
constructor( context ) {
/**
* The context instance.
*
* @readonly
* @type {module:core/context~Context|module:core/editor/editor~Editor}
*/
this.context = context;
}

/**
* @inheritDoc
*/
destroy() {
this.stopListening();
}

/**
* @inheritDoc
*/
static get isContextPlugin() {
return true;
}
}

mix( ContextPlugin, ObservableMixin );
Loading

0 comments on commit 672e55e

Please sign in to comment.