UNPKG

ckeditor5-image-upload-base64

Version:

The development environment of CKEditor 5 – the best browser-based rich text editor.

548 lines (476 loc) 15.2 kB
/** * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ /** * @module watchdog/contextwatchdog */ /* globals console */ import Watchdog from './watchdog'; import EditorWatchdog from './editorwatchdog'; import areConnectedThroughProperties from './utils/areconnectedthroughproperties'; import getSubNodes from './utils/getsubnodes'; /** * A watchdog for the {@link module:core/context~Context} class. * * See the {@glink features/watchdog Watchdog feature guide} to learn the rationale behind it and * how to use it. * * @extends {module:watchdog/watchdog~Watchdog} */ export default class ContextWatchdog extends Watchdog { /** * The context watchdog class constructor. * * const watchdog = new ContextWatchdog( Context ); * * await watchdog.create( contextConfiguration ); * * await watchdog.add( item ); * * See the {@glink features/watchdog Watchdog feature guide} to learn more how to use this feature. * * @param {Function} Context The {@link module:core/context~Context} class. * @param {module:watchdog/watchdog~WatchdogConfig} [watchdogConfig] The watchdog configuration. */ constructor( Context, watchdogConfig = {} ) { super( watchdogConfig ); /** * A map of internal watchdogs for added items. * * @protected * @type {Map.<string,module:watchdog/watchdog~EditorWatchdog>} */ this._watchdogs = new Map(); /** * The watchdog configuration. * * @private * @type {module:watchdog/watchdog~WatchdogConfig} */ this._watchdogConfig = watchdogConfig; /** * The current context instance. * * @private * @type {module:core/context~Context|null} */ this._context = null; /** * Context properties (nodes/references) that are gathered during the initial context creation * and are used to distinguish the origin of an error. * * @private * @type {Set.<*>} */ this._contextProps = new Set(); /** * An action queue, which is used to handle async functions queuing. * * @private * @type {ActionQueue} */ this._actionQueue = new ActionQueue(); /** * The configuration for the {@link module:core/context~Context}. * * @private * @member {Object} #_contextConfig */ /** * The context configuration. * * @private * @member {Object|undefined} #_config */ // Default creator and destructor. this._creator = contextConfig => Context.create( contextConfig ); this._destructor = context => context.destroy(); this._actionQueue.onEmpty( () => { if ( this.state === 'initializing' ) { this.state = 'ready'; this._fire( 'stateChange' ); } } ); /** * Sets the function that is responsible for the context creation. * It expects a function that should return a promise (or `undefined`). * * watchdog.setCreator( config => Context.create( config ) ); * * @method #setCreator * @param {Function} creator */ /** * Sets the function that is responsible for the context destruction. * Overrides the default destruction function, which destroys only the context instance. * It expects a function that should return a promise (or `undefined`). * * watchdog.setDestructor( context => { * // Do something before the context is destroyed. * * return context * .destroy() * .then( () => { * // Do something after the context is destroyed. * } ); * } ); * * @method #setDestructor * @param {Function} destructor */ } /** * The context instance. Keep in mind that this property might be changed when the context watchdog restarts, * so do not keep this instance internally. Always operate on the `ContextWatchdog#context` property. * * @type {module:core/context~Context|null} */ get context() { return this._context; } /** * Initializes the context watchdog. Once it is created, the watchdog takes care about * recreating the context and the provided items, and starts the error handling mechanism. * * await watchdog.create( { * plugins: [] * } ); * * @param {Object} [contextConfig] The context configuration. See {@link module:core/context~Context}. * @returns {Promise} */ create( contextConfig = {} ) { return this._actionQueue.enqueue( () => { this._contextConfig = contextConfig; return this._create(); } ); } /** * Returns an item instance with the given `itemId`. * * const editor1 = watchdog.getItem( 'editor1' ); * * @param {String} itemId The item ID. * @returns {*} The item instance or `undefined` if an item with a given ID has not been found. */ getItem( itemId ) { const watchdog = this._getWatchdog( itemId ); return watchdog._item; } /** * Gets the state of the given item. See {@link #state} for a list of available states. * * const editor1State = watchdog.getItemState( 'editor1' ); * * @param {String} itemId Item ID. * @returns {'initializing'|'ready'|'crashed'|'crashedPermanently'|'destroyed'} The state of the item. */ getItemState( itemId ) { const watchdog = this._getWatchdog( itemId ); return watchdog.state; } /** * Adds items to the watchdog. Once created, instances of these items will be available using the {@link #getItem} method. * * Items can be passed together as an array of objects: * * await watchdog.add( [ { * id: 'editor1', * type: 'editor', * sourceElementOrData: document.querySelector( '#editor' ), * config: { * plugins: [ Essentials, Paragraph, Bold, Italic ], * toolbar: [ 'bold', 'italic', 'alignment' ] * }, * creator: ( element, config ) => ClassicEditor.create( element, config ) * } ] ); * * Or one by one as objects: * * await watchdog.add( { * id: 'editor1', * type: 'editor', * sourceElementOrData: document.querySelector( '#editor' ), * config: { * plugins: [ Essentials, Paragraph, Bold, Italic ], * toolbar: [ 'bold', 'italic', 'alignment' ] * }, * creator: ( element, config ) => ClassicEditor.create( element, config ) * ] ); * * Then an instance can be retrieved using the {@link #getItem} method: * * const editor1 = watchdog.getItem( 'editor1' ); * * Note that this method can be called multiple times, but for performance reasons it is better * to pass all items together. * * @param {module:watchdog/contextwatchdog~WatchdogItemConfiguration|Array.<module:watchdog/contextwatchdog~WatchdogItemConfiguration>} * itemConfigurationOrItemConfigurations An item configuration object or an array of item configurations. * @returns {Promise} */ add( itemConfigurationOrItemConfigurations ) { const itemConfigurations = Array.isArray( itemConfigurationOrItemConfigurations ) ? itemConfigurationOrItemConfigurations : [ itemConfigurationOrItemConfigurations ]; return this._actionQueue.enqueue( () => { if ( this.state === 'destroyed' ) { throw new Error( 'Cannot add items to destroyed watchdog.' ); } if ( !this._context ) { throw new Error( 'Context was not created yet. You should call the `ContextWatchdog#create()` method first.' ); } // Create new watchdogs. return Promise.all( itemConfigurations.map( item => { let watchdog; if ( this._watchdogs.has( item.id ) ) { throw new Error( `Item with the given id is already added: '${ item.id }'.` ); } if ( item.type === 'editor' ) { watchdog = new EditorWatchdog( this._watchdogConfig ); watchdog.setCreator( item.creator ); watchdog._setExcludedProperties( this._contextProps ); if ( item.destructor ) { watchdog.setDestructor( item.destructor ); } this._watchdogs.set( item.id, watchdog ); // Enqueue the internal watchdog errors within the main queue. // And propagate the internal `error` events as `itemError` event. watchdog.on( 'error', ( evt, { error, causesRestart } ) => { this._fire( 'itemError', { itemId: item.id, error } ); // Do not enqueue the item restart action if the item will not restart. if ( !causesRestart ) { return; } this._actionQueue.enqueue( () => new Promise( res => { watchdog.on( 'restart', rethrowRestartEventOnce.bind( this ) ); function rethrowRestartEventOnce() { watchdog.off( 'restart', rethrowRestartEventOnce ); this._fire( 'itemRestart', { itemId: item.id } ); res(); } } ) ); } ); return watchdog.create( item.sourceElementOrData, item.config, this._context ); } else { throw new Error( `Not supported item type: '${ item.type }'.` ); } } ) ); } ); } /** * Removes and destroys item(s) with given ID(s). * * await watchdog.remove( 'editor1' ); * * Or * * await watchdog.remove( [ 'editor1', 'editor2' ] ); * * @param {Array.<String>|String} itemIdOrItemIds Item ID or an array of item IDs. * @returns {Promise} */ remove( itemIdOrItemIds ) { const itemIds = Array.isArray( itemIdOrItemIds ) ? itemIdOrItemIds : [ itemIdOrItemIds ]; return this._actionQueue.enqueue( () => { return Promise.all( itemIds.map( itemId => { const watchdog = this._getWatchdog( itemId ); this._watchdogs.delete( itemId ); return watchdog.destroy(); } ) ); } ); } /** * Destroys the context watchdog and all added items. * Once the context watchdog is destroyed, new items cannot be added. * * await watchdog.destroy(); * * @returns {Promise} */ destroy() { return this._actionQueue.enqueue( () => { this.state = 'destroyed'; this._fire( 'stateChange' ); super.destroy(); return this._destroy(); } ); } /** * Restarts the context watchdog. * * @protected * @returns {Promise} */ _restart() { return this._actionQueue.enqueue( () => { this.state = 'initializing'; this._fire( 'stateChange' ); return this._destroy() .catch( err => { console.error( 'An error happened during destroying the context or items.', err ); } ) .then( () => this._create() ) .then( () => this._fire( 'restart' ) ); } ); } /** * @private * @returns {Promise} */ _create() { return Promise.resolve() .then( () => { this._startErrorHandling(); return this._creator( this._contextConfig ); } ) .then( context => { this._context = context; this._contextProps = getSubNodes( this._context ); return Promise.all( Array.from( this._watchdogs.values() ) .map( watchdog => { watchdog._setExcludedProperties( this._contextProps ); return watchdog.create( undefined, undefined, this._context ); } ) ); } ); } /** * Destroys the context instance and all added items. * * @private * @returns {Promise} */ _destroy() { return Promise.resolve() .then( () => { this._stopErrorHandling(); const context = this._context; this._context = null; this._contextProps = new Set(); return Promise.all( Array.from( this._watchdogs.values() ) .map( watchdog => watchdog.destroy() ) ) // Context destructor destroys each editor. .then( () => this._destructor( context ) ); } ); } /** * Returns the watchdog for a given item ID. * * @protected * @param {String} itemId Item ID. * @returns {module:watchdog/watchdog~Watchdog} Watchdog */ _getWatchdog( itemId ) { const watchdog = this._watchdogs.get( itemId ); if ( !watchdog ) { throw new Error( `Item with the given id was not registered: ${ itemId }.` ); } return watchdog; } /** * Checks whether an error comes from the context instance and not from the item instances. * * @protected * @param {Error} error * @returns {Boolean} */ _isErrorComingFromThisItem( error ) { for ( const watchdog of this._watchdogs.values() ) { if ( watchdog._isErrorComingFromThisItem( error ) ) { return false; } } return areConnectedThroughProperties( this._context, error.context ); } /** * Fired after the watchdog restarts the context and the added items because of a crash. * * watchdog.on( 'restart', () => { * console.log( 'The context has been restarted.' ); * } ); * * @event restart */ /** * Fired when a new error occurred in one of the added items. * * watchdog.on( 'itemError', ( evt, { error, itemId, causesRestart } ) => { * console.log( `An error occurred in an item with the '${ itemId }' ID.` ); * } ); * * @event itemError */ /** * Fired after an item has been restarted. * * watchdog.on( 'itemRestart', ( evt, { itemId } ) => { * console.log( 'An item with with the '${ itemId }' ID has been restarted.' ); * } ); * * @event itemRestart */ } // An action queue that allows queuing async functions. class ActionQueue { constructor() { // @type {Promise} this._promiseQueue = Promise.resolve(); // @type {Array.<Function>} this._onEmptyCallbacks = []; } // Used to register callbacks that will be run when the queue becomes empty. // // @param {Function} onEmptyCallback A callback that will be run whenever the queue becomes empty. onEmpty( onEmptyCallback ) { this._onEmptyCallbacks.push( onEmptyCallback ); } // It adds asynchronous actions (functions) to the queue and runs them one by one. // // @param {Function} action A function that should be enqueued. // @returns {Promise} enqueue( action ) { let nonErrorQueue; const queueWithAction = this._promiseQueue .then( action ) .then( () => { if ( this._promiseQueue === nonErrorQueue ) { this._onEmptyCallbacks.forEach( cb => cb() ); } } ); // Catch all errors in the main queue to stack promises even if an error occurred in the past. nonErrorQueue = this._promiseQueue = queueWithAction.catch( () => { } ); return queueWithAction; } } /** * The watchdog item configuration interface. * * @typedef {module:watchdog/contextwatchdog~EditorWatchdogConfiguration} module:watchdog/contextwatchdog~WatchdogItemConfiguration */ /** * The editor watchdog configuration interface specifies how editors should be created and destroyed. * * @typedef {Object} module:watchdog/contextwatchdog~EditorWatchdogConfiguration * * @property {String} id A unique item identificator. * * @property {'editor'} type The type of the item to create. At the moment, only `'editor'` is supported. * * @property {Function} creator A function that initializes the item (the editor). The function takes editor initialization arguments * and should return a promise. For example: `( el, config ) => ClassicEditor.create( el, config )`. * * @property {Function} [destructor] A function that destroys the item instance (the editor). The function * takes an item and should return a promise. For example: `editor => editor.destroy()` * * @property {String|HTMLElement} sourceElementOrData The source element or data that will be passed * as the first argument to the `Editor.create()` method. * * @property {Object} config An editor configuration. */