| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327 |
- /**
- * @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
- */
- }
|