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

Commit

Permalink
Merge 3ae597c into d246a4d
Browse files Browse the repository at this point in the history
  • Loading branch information
ma2ciek committed Aug 13, 2018
2 parents d246a4d + 3ae597c commit 342f4a8
Show file tree
Hide file tree
Showing 6 changed files with 360 additions and 443 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
],
"dependencies": {
"@ckeditor/ckeditor5-utils": "^10.2.0",
"@ckeditor/ckeditor5-core": "^11.0.0"
"@ckeditor/ckeditor5-core": "^11.0.0",
"lodash-es": "^4.17.10"
},
"devDependencies": {
"@ckeditor/ckeditor5-engine": "^10.2.0",
Expand Down
148 changes: 89 additions & 59 deletions src/autosave.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
import PendingActions from '@ckeditor/ckeditor5-core/src/pendingactions';
import DomEmitterMixin from '@ckeditor/ckeditor5-utils/src/dom/emittermixin';
import throttle from './throttle';
import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
import mix from '@ckeditor/ckeditor5-utils/src/mix';
import { debounce } from 'lodash-es';

/* globals window */

Expand Down Expand Up @@ -62,6 +64,12 @@ export default class Autosave extends Plugin {
constructor( editor ) {
super( editor );

const config = editor.config.get( 'autosave' ) || {};

// A minimum amount of time that needs to pass after the last action.
// After that time the provided save callbacks are being called.
const waitingTime = config.waitingTime || 2000;

/**
* The adapter is an object with a `save()` method. That method will be called whenever
* the data changes. It might be called some time after the change,
Expand All @@ -71,17 +79,31 @@ export default class Autosave extends Plugin {
*/

/**
* Throttled save method.
* The state of this plugin.
*
* The plugin can be in the following states:
*
* * synchronized - when all changes are saved
* * waiting - when the plugin is waiting for other changes before calling `adapter#save()` and `config.autosave.save()`
* * saving - when the provided save method is called and the plugin waits for the response
*
* @member {'synchronized'|'waiting'|'saving'} #state
*/
this.set( 'state', 'synchronized' );

/**
* Debounced save method. The `save` method is called the specified `waitingTime` after the `debouncedSave` is called,
* unless new action happens in the meantime.
*
* @protected
* @private
* @type {Function}
*/
this._throttledSave = throttle( this._save.bind( this ), 1000 );
this._debouncedSave = debounce( this._save.bind( this ), waitingTime );

/**
* Last document version.
*
* @protected
* @private
* @type {Number}
*/
this._lastDocumentVersion = editor.model.document.version;
Expand All @@ -95,12 +117,12 @@ export default class Autosave extends Plugin {
this._domEmitter = Object.create( DomEmitterMixin );

/**
* Save action counter monitors number of actions.
* The config of this plugins.
*
* @private
* @type {Number}
* @type {Object}
*/
this._saveActionCounter = 0;
this._config = config;

/**
* An action that will be added to pending action manager for actions happening in that plugin.
Expand All @@ -109,14 +131,6 @@ export default class Autosave extends Plugin {
* @member {Object} #_action
*/

/**
* Plugins' config.
*
* @private
* @type {Object}
*/
this._config = editor.config.get( 'autosave' ) || {};

/**
* Editor's pending actions manager.
*
Expand All @@ -135,13 +149,22 @@ export default class Autosave extends Plugin {
this._pendingActions = editor.plugins.get( PendingActions );

this.listenTo( doc, 'change:data', () => {
this._incrementCounter();
if ( !this._saveCallbacks.length ) {
return;
}

if ( this.state == 'synchronized' ) {
this._action = this._pendingActions.add( this.editor.t( 'Saving changes' ) );
this.state = 'waiting';

const willOriginalFunctionBeCalled = this._throttledSave();
this._debouncedSave();
}

if ( !willOriginalFunctionBeCalled ) {
this._decrementCounter();
else if ( this.state == 'waiting' ) {
this._debouncedSave();
}

// Do nothing if the plugin is in `external-saving` state.
} );

// Flush on the editor's destroy listener with the highest priority to ensure that
Expand Down Expand Up @@ -175,7 +198,7 @@ export default class Autosave extends Plugin {
* @protected
*/
_flush() {
this._throttledSave.flush();
this._debouncedSave.flush();
}

/**
Expand All @@ -188,74 +211,62 @@ export default class Autosave extends Plugin {
_save() {
const version = this.editor.model.document.version;

const saveCallbacks = [];

if ( this.adapter && this.adapter.save ) {
saveCallbacks.push( this.adapter.save );
}

if ( this._config.save ) {
saveCallbacks.push( this._config.save );
}

// Change may not produce an operation, so the document's version
// can be the same after that change.
if (
version < this._lastDocumentVersion ||
!saveCallbacks.length ||
this.editor.state === 'initializing'
) {
this._throttledSave.flush();
this._decrementCounter();
this._debouncedSave.cancel();

return;
}

this._lastDocumentVersion = version;

// Wait one promise cycle to be sure that:
// 1. The save method is always asynchronous.
// 2. Save callbacks are not called inside conversions or while editor's state changes.
this.state = 'saving';

// Wait one promise cycle to be sure that save callbacks are not called
// inside a conversion or when the editor's state changes.
Promise.resolve()
.then( () => Promise.all(
saveCallbacks.map( cb => cb( this.editor ) )
this._saveCallbacks.map( cb => cb( this.editor ) )
) )
.then( () => {
this._decrementCounter();
if ( this.editor.model.document.version > this._lastDocumentVersion ) {
this.state = 'waiting';
this._debouncedSave();
} else {
this.state = 'synchronized';
this._pendingActions.remove( this._action );
this._action = null;
}
} );
}

/**
* Increments counter and adds pending action if it not exists.
* Save callbacks.
*
* @private
* @type {Array.<Function>}
*/
_incrementCounter() {
const t = this.editor.t;

this._saveActionCounter++;
get _saveCallbacks() {
const saveCallbacks = [];

if ( !this._action ) {
this._action = this._pendingActions.add( t( 'Saving changes' ) );
if ( this.adapter && this.adapter.save ) {
saveCallbacks.push( this.adapter.save );
}
}

/**
* Decrements counter and removes pending action if counter is empty,
* which means, that no new save action occurred.
*
* @private
*/
_decrementCounter() {
this._saveActionCounter--;

if ( this._saveActionCounter === 0 ) {
this._pendingActions.remove( this._action );
this._action = null;
if ( this._config.save ) {
saveCallbacks.push( this._config.save );
}

return saveCallbacks;
}
}

mix( Autosave, ObservableMixin );

/**
* An interface that requires the `save()` method.
*
Expand Down Expand Up @@ -324,3 +335,22 @@ export default class Autosave extends Plugin {
* @param {module:core/editor/editor~Editor} editor The editor instance.
* @returns {Promise.<*>}
*/

/**
* The minimum amount of time that need to pass after last action to call the provided callback.
*
* ClassicEditor
* .create( editorElement, {
* autosave: {
* save( editor ) {
* return saveData( editor.getData() );
* },
* waitingTime: 1000
* }
* } );
* .then( ... )
* .catch( ... );
*
* @property module:autosave/autosave~AutosaveConfig#waitingTime
* @type {Number}
*/
68 changes: 0 additions & 68 deletions src/throttle.js

This file was deleted.

0 comments on commit 342f4a8

Please sign in to comment.