UNPKG

ckeditor5-image-upload-base64

Version:

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

328 lines (282 loc) 8.02 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/editorwatchdog */ /* globals console */ import { throttle, cloneDeepWith, isElement } from 'lodash-es'; import areConnectedThroughProperties from './utils/areconnectedthroughproperties'; import Watchdog from './watchdog'; /** * A watchdog for CKEditor 5 editors. * * 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 EditorWatchdog extends Watchdog { /** * @param {*} Editor The editor class. * @param {module:watchdog/watchdog~WatchdogConfig} [watchdogConfig] The watchdog plugin configuration. */ constructor( Editor, watchdogConfig = {} ) { super( watchdogConfig ); /** * The current editor instance. * * @private * @type {module:core/editor/editor~Editor} */ this._editor = null; /** * Throttled save method. The `save()` method is called the specified `saveInterval` after `throttledSave()` is called, * unless a new action happens in the meantime. * * @private * @type {Function} */ this._throttledSave = throttle( this._save.bind( this ), typeof watchdogConfig.saveInterval === 'number' ? watchdogConfig.saveInterval : 5000 ); /** * The latest saved editor data represented as a root name -> root data object. * * @private * @member {Object.<String,String>} #_data */ /** * The last document version. * * @private * @member {Number} #_lastDocumentVersion */ /** * The editor source element or data. * * @private * @member {HTMLElement|String|Object.<String|String>} #_elementOrData */ /** * The editor configuration. * * @private * @member {Object|undefined} #_config */ // Set default creator and destructor functions: this._creator = ( ( elementOrData, config ) => Editor.create( elementOrData, config ) ); this._destructor = editor => editor.destroy(); } /** * The current editor instance. * * @readonly * @type {module:core/editor/editor~Editor} */ get editor() { return this._editor; } /** * @inheritDoc */ get _item() { return this._editor; } /** * Sets the function that is responsible for the editor creation. * It expects a function that should return a promise. * * watchdog.setCreator( ( element, config ) => ClassicEditor.create( element, config ) ); * * @method #setCreator * @param {Function} creator */ /** * Sets the function that is responsible for the editor destruction. * Overrides the default destruction function, which destroys only the editor instance. * It expects a function that should return a promise or `undefined`. * * watchdog.setDestructor( editor => { * // Do something before the editor is destroyed. * * return editor * .destroy() * .then( () => { * // Do something after the editor is destroyed. * } ); * } ); * * @method #setDestructor * @param {Function} destructor */ /** * Restarts the editor instance. This method is called whenever an editor error occurs. It fires the `restart` event and changes * the state to `initializing`. * * @protected * @fires restart * @returns {Promise} */ _restart() { return Promise.resolve() .then( () => { this.state = 'initializing'; this._fire( 'stateChange' ); return this._destroy(); } ) .catch( err => { console.error( 'An error happened during the editor destroying.', err ); } ) .then( () => { if ( typeof this._elementOrData === 'string' ) { return this.create( this._data, this._config, this._config.context ); } else { const updatedConfig = Object.assign( {}, this._config, { initialData: this._data } ); return this.create( this._elementOrData, updatedConfig, updatedConfig.context ); } } ) .then( () => { this._fire( 'restart' ); } ); } /** * Creates the editor instance and keeps it running, using the defined creator and destructor. * * @param {HTMLElement|String|Object.<String|String>} [elementOrData] The editor source element or the editor data. * @param {module:core/editor/editorconfig~EditorConfig} [config] The editor configuration. * @param {Object} [context] A context for the editor. * * @returns {Promise} */ create( elementOrData = this._elementOrData, config = this._config, context ) { return Promise.resolve() .then( () => { super._startErrorHandling(); this._elementOrData = elementOrData; // Clone configuration because it might be shared within multiple watchdog instances. Otherwise, // when an error occurs in one of these editors, the watchdog will restart all of them. this._config = this._cloneEditorConfiguration( config ) || {}; this._config.context = context; return this._creator( elementOrData, this._config ); } ) .then( editor => { this._editor = editor; editor.model.document.on( 'change:data', this._throttledSave ); this._lastDocumentVersion = editor.model.document.version; this._data = this._getData(); this.state = 'ready'; this._fire( 'stateChange' ); } ); } /** * Destroys the watchdog and the current editor instance. It fires the callback * registered in {@link #setDestructor `setDestructor()`} and uses it to destroy the editor instance. * It also sets the state to `destroyed`. * * @returns {Promise} */ destroy() { return Promise.resolve() .then( () => { this.state = 'destroyed'; this._fire( 'stateChange' ); super.destroy(); return this._destroy(); } ); } /** * @private * @returns {Promise} */ _destroy() { return Promise.resolve() .then( () => { this._stopErrorHandling(); // Save data if there is a remaining editor data change. this._throttledSave.flush(); const editor = this._editor; this._editor = null; return this._destructor( editor ); } ); } /** * Saves the editor data, so it can be restored after the crash even if the data cannot be fetched at * the moment of the crash. * * @private */ _save() { const version = this._editor.model.document.version; // Operation may not result in a model change, so the document's version can be the same. if ( version === this._lastDocumentVersion ) { return; } try { this._data = this._getData(); this._lastDocumentVersion = version; } catch ( err ) { console.error( err, 'An error happened during restoring editor data. ' + 'Editor will be restored from the previously saved data.' ); } } /** * @protected * @param {Set} props */ _setExcludedProperties( props ) { this._excludedProps = props; } /** * Returns the editor data. * * @private * @returns {Object<String,String>} */ _getData() { const data = {}; for ( const rootName of this._editor.model.document.getRootNames() ) { data[ rootName ] = this._editor.data.get( { rootName } ); } return data; } /** * Traverses the error context and the current editor to find out whether these structures are connected * to each other via properties. * * @protected * @param {module:utils/ckeditorerror~CKEditorError} error */ _isErrorComingFromThisItem( error ) { return areConnectedThroughProperties( this._editor, error.context, this._excludedProps ); } /** * Clones the editor configuration. * * @private * @param {Object} config */ _cloneEditorConfiguration( config ) { return cloneDeepWith( config, ( value, key ) => { // Leave DOM references. if ( isElement( value ) ) { return value; } if ( key === 'context' ) { return value; } } ); } /** * Fired after the watchdog restarts the error in case of a crash. * * @event restart */ }