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

Commit 672e55e

Browse files
authored
Merge pull request #204 from ckeditor/context
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.
2 parents 8376e9c + e96aeed commit 672e55e

File tree

12 files changed

+971
-81
lines changed

12 files changed

+971
-81
lines changed

src/context.js

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
/**
2+
* @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
3+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4+
*/
5+
6+
/**
7+
* @module core/context
8+
*/
9+
10+
import Config from '@ckeditor/ckeditor5-utils/src/config';
11+
import PluginCollection from './plugincollection';
12+
import Locale from '@ckeditor/ckeditor5-utils/src/locale';
13+
import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
14+
15+
/**
16+
* Provides a common, higher level environment for solutions which use multiple {@link module:core/editor/editor~Editor editors}
17+
* or/and plugins that work outside of an editor. Use it instead of {@link module:core/editor/editor~Editor.create `Editor.create()`}
18+
* in advanced application integrations.
19+
*
20+
* All configuration options passed to a `Context` will be used as default options for editor instances initialized in that context.
21+
*
22+
* {@link module:core/contextplugin~ContextPlugin `ContextPlugin`s} passed to a `Context` instance will be shared among all
23+
* editor instances initialized in that context. These will be the same plugin instances for all the editors.
24+
*
25+
* **Note:** `Context` can only be initialized with {@link module:core/contextplugin~ContextPlugin `ContextPlugin`s}
26+
* (e.g. [comments](https://ckeditor.com/collaboration/comments/)). Regular {@link module:core/plugin~Plugin `Plugin`s} require an
27+
* editor instance to work and cannot be added to a `Context`.
28+
*
29+
* **Note:** You can add `ContextPlugin` to an editor instance, though.
30+
*
31+
* If you are using multiple editor instances on one page and use any `ContextPlugin`s, create `Context` to share configuration and plugins
32+
* among those editors. Some plugins will use the information about all existing editors to better integrate between them.
33+
*
34+
* If you are using plugins that do not require an editor to work (e.g. [comments](https://ckeditor.com/collaboration/comments/))
35+
* enable and configure them using `Context`.
36+
*
37+
* If you are using only a single editor on each page use {@link module:core/editor/editor~Editor.create `Editor.create()`} instead.
38+
* In such case, `Context` instance will be created by the editor instance in a transparent way.
39+
*
40+
* See {@link module:core/context~Context.create `Context.create()`} for usage examples.
41+
*/
42+
export default class Context {
43+
/**
44+
* Creates a context instance with a given configuration.
45+
*
46+
* Usually, not to be used directly. See the static {@link module:core/context~Context.create `create()`} method.
47+
*
48+
* @param {Object} [config={}] The context config.
49+
*/
50+
constructor( config ) {
51+
/**
52+
* Holds all configurations specific to this context instance.
53+
*
54+
* @readonly
55+
* @type {module:utils/config~Config}
56+
*/
57+
this.config = new Config( config );
58+
59+
/**
60+
* The plugins loaded and in use by this context instance.
61+
*
62+
* @readonly
63+
* @type {module:core/plugincollection~PluginCollection}
64+
*/
65+
this.plugins = new PluginCollection( this );
66+
67+
const languageConfig = this.config.get( 'language' ) || {};
68+
69+
/**
70+
* @readonly
71+
* @type {module:utils/locale~Locale}
72+
*/
73+
this.locale = new Locale( {
74+
uiLanguage: typeof languageConfig === 'string' ? languageConfig : languageConfig.ui,
75+
contentLanguage: this.config.get( 'language.content' )
76+
} );
77+
78+
/**
79+
* Shorthand for {@link module:utils/locale~Locale#t}.
80+
*
81+
* @see module:utils/locale~Locale#t
82+
* @method #t
83+
*/
84+
this.t = this.locale.t;
85+
86+
/**
87+
* List of editors to which this context instance is injected.
88+
*
89+
* @private
90+
* @type {Set.<module:core/editor/editor~Editor>}
91+
*/
92+
this._editors = new Set();
93+
94+
/**
95+
* Reference to the editor which created the context.
96+
* Null when the context was created outside of the editor.
97+
*
98+
* It is used to destroy the context when removing the editor that has created the context.
99+
*
100+
* @private
101+
* @type {module:core/editor/editor~Editor|null}
102+
*/
103+
this._contextOwner = null;
104+
}
105+
106+
/**
107+
* Loads and initializes plugins specified in the config.
108+
*
109+
* @returns {Promise.<module:core/plugin~LoadedPlugins>} A promise which resolves
110+
* once the initialization is completed providing an array of loaded plugins.
111+
*/
112+
initPlugins() {
113+
const plugins = this.config.get( 'plugins' ) || [];
114+
115+
for ( const Plugin of plugins ) {
116+
if ( typeof Plugin != 'function' ) {
117+
/**
118+
* Only constructor is allowed as a {@link module:core/contextplugin~ContextPlugin}.
119+
*
120+
* @error context-initplugins-constructor-only
121+
*/
122+
throw new CKEditorError(
123+
'context-initplugins-constructor-only: Only constructor is allowed as a Context plugin.',
124+
null,
125+
{ Plugin }
126+
);
127+
}
128+
129+
if ( Plugin.isContextPlugin !== true ) {
130+
/**
131+
* Only plugin marked as a {@link module:core/contextplugin~ContextPlugin} is allowed to be used with a context.
132+
*
133+
* @error context-initplugins-invalid-plugin
134+
*/
135+
throw new CKEditorError(
136+
'context-initplugins-invalid-plugin: Only plugin marked as a ContextPlugin is allowed.',
137+
null,
138+
{ Plugin }
139+
);
140+
}
141+
}
142+
143+
return this.plugins.init( plugins );
144+
}
145+
146+
/**
147+
* Destroys the context instance, and all editors used with the context.
148+
* Releasing all resources used by the context.
149+
*
150+
* @returns {Promise} A promise that resolves once the context instance is fully destroyed.
151+
*/
152+
destroy() {
153+
return Promise.all( Array.from( this._editors, editor => editor.destroy() ) )
154+
.then( () => this.plugins.destroy() );
155+
}
156+
157+
/**
158+
* Adds a reference to the editor which is used with this context.
159+
*
160+
* When the given editor has created the context then the reference to this editor will be stored
161+
* as a {@link ~Context#_contextOwner}.
162+
*
163+
* This method should be used only by the editor.
164+
*
165+
* @protected
166+
* @param {module:core/editor/editor~Editor} editor
167+
* @param {Boolean} isContextOwner Stores the given editor as a context owner.
168+
*/
169+
_addEditor( editor, isContextOwner ) {
170+
if ( this._contextOwner ) {
171+
/**
172+
* Cannot add multiple editors to the context which is created by the editor.
173+
*
174+
* @error context-addEditor-private-context
175+
*/
176+
throw new CKEditorError(
177+
'context-addEditor-private-context: Cannot add multiple editors to the context which is created by the editor.'
178+
);
179+
}
180+
181+
this._editors.add( editor );
182+
183+
if ( isContextOwner ) {
184+
this._contextOwner = editor;
185+
}
186+
}
187+
188+
/**
189+
* Removes a reference to the editor which was used with this context.
190+
* When the context was created by the given editor then the context will be destroyed.
191+
*
192+
* This method should be used only by the editor.
193+
*
194+
* @protected
195+
* @param {module:core/editor/editor~Editor} editor
196+
* @return {Promise} A promise that resolves once the editor is removed from the context or when the context has been destroyed.
197+
*/
198+
_removeEditor( editor ) {
199+
this._editors.delete( editor );
200+
201+
if ( this._contextOwner === editor ) {
202+
return this.destroy();
203+
}
204+
205+
return Promise.resolve();
206+
}
207+
208+
/**
209+
* Returns context configuration which will be copied to editors created using this context.
210+
*
211+
* The configuration returned by this method has removed plugins configuration - plugins are shared with all editors
212+
* through another mechanism.
213+
*
214+
* This method should be used only by the editor.
215+
*
216+
* @protected
217+
* @returns {Object} Configuration as a plain object.
218+
*/
219+
_getEditorConfig() {
220+
const result = {};
221+
222+
for ( const name of this.config.names() ) {
223+
if ( ![ 'plugins', 'removePlugins', 'extraPlugins' ].includes( name ) ) {
224+
result[ name ] = this.config.get( name );
225+
}
226+
}
227+
228+
return result;
229+
}
230+
231+
/**
232+
* Creates and initializes a new context instance.
233+
*
234+
* const commonConfig = { ... }; // Configuration for all the plugins and editors.
235+
* const editorPlugins = [ ... ]; // Regular `Plugin`s here.
236+
*
237+
* Context
238+
* .create( {
239+
* // Only `ContextPlugin`s here.
240+
* plugins: [ ... ],
241+
*
242+
* // Configure language for all the editors (it cannot be overwritten).
243+
* language: { ... },
244+
*
245+
* // Configuration for context plugins.
246+
* comments: { ... },
247+
* ...
248+
*
249+
* // Default configuration for editor plugins.
250+
* toolbar: { ... },
251+
* image: { ... },
252+
* ...
253+
* } )
254+
* .then( context => {
255+
* const promises = [];
256+
*
257+
* promises.push( ClassicEditor.create(
258+
* document.getElementById( 'editor1' ),
259+
* {
260+
* editorPlugins,
261+
* context
262+
* }
263+
* ) );
264+
*
265+
* promises.push( ClassicEditor.create(
266+
* document.getElementById( 'editor2' ),
267+
* {
268+
* editorPlugins,
269+
* context,
270+
* toolbar: { ... } // You can overwrite context's configuration.
271+
* }
272+
* ) );
273+
*
274+
* return Promise.all( promises );
275+
* } );
276+
*
277+
* @param {Object} [config] The context config.
278+
* @returns {Promise} A promise resolved once the context is ready. The promise resolves with the created context instance.
279+
*/
280+
static create( config ) {
281+
return new Promise( resolve => {
282+
const context = new this( config );
283+
284+
resolve( context.initPlugins().then( () => context ) );
285+
} );
286+
}
287+
}

src/contextplugin.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/**
2+
* @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
3+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4+
*/
5+
6+
/**
7+
* @module core/contextplugin
8+
*/
9+
10+
import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
11+
import mix from '@ckeditor/ckeditor5-utils/src/mix';
12+
13+
/**
14+
* The base class for {@link module:core/context~Context} plugin classes.
15+
*
16+
* A context plugin can either be initialized for an {@link module:core/editor/editor~Editor editor} or for
17+
* a {@link module:core/context~Context context}. In other words, it can either
18+
* work within one editor instance or with one or more editor instances that use a single context.
19+
* It is the context plugin's role to implement handling for both modes.
20+
*
21+
* A couple of rules for interaction between editor plugins and context plugins:
22+
*
23+
* * a context plugin can require another context plugin,
24+
* * an {@link module:core/plugin~Plugin editor plugin} can require a context plugin,
25+
* * a context plugin MUST NOT require an {@link module:core/plugin~Plugin editor plugin}.
26+
*
27+
* @implements module:core/plugin~PluginInterface
28+
* @mixes module:utils/observablemixin~ObservableMixin
29+
*/
30+
export default class ContextPlugin {
31+
/**
32+
* Creates a new plugin instance.
33+
*
34+
* @param {module:core/context~Context|module:core/editor/editor~Editor} context
35+
*/
36+
constructor( context ) {
37+
/**
38+
* The context instance.
39+
*
40+
* @readonly
41+
* @type {module:core/context~Context|module:core/editor/editor~Editor}
42+
*/
43+
this.context = context;
44+
}
45+
46+
/**
47+
* @inheritDoc
48+
*/
49+
destroy() {
50+
this.stopListening();
51+
}
52+
53+
/**
54+
* @inheritDoc
55+
*/
56+
static get isContextPlugin() {
57+
return true;
58+
}
59+
}
60+
61+
mix( ContextPlugin, ObservableMixin );

0 commit comments

Comments
 (0)