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

Commit

Permalink
Merge eeab88a into 53690ec
Browse files Browse the repository at this point in the history
  • Loading branch information
ma2ciek committed Jun 15, 2018
2 parents 53690ec + eeab88a commit 05160ca
Show file tree
Hide file tree
Showing 8 changed files with 1,055 additions and 0 deletions.
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,13 @@
"ckeditor5-feature"
],
"dependencies": {
"@ckeditor/ckeditor5-utils": "^10.0.0",
"@ckeditor/ckeditor5-core": "^10.0.0"
},
"devDependencies": {
"@ckeditor/ckeditor5-engine": "^10.0.0",
"@ckeditor/ckeditor5-paragraph": "^10.0.0",
"@ckeditor/ckeditor5-editor-classic": "^10.0.0",
"eslint": "^4.15.0",
"eslint-config-ckeditor5": "^1.0.7",
"husky": "^0.14.3",
Expand Down
256 changes: 256 additions & 0 deletions src/autosave.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
/**
* @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/

/**
* @module autosave/autosave
*/

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';

/* globals window */

/**
* Autosave plugin provides an easy-to-use API to save the editor's content.
* It watches {@link module:engine/model/document~Document#event:change:data change:data}
* and `window#beforeunload` events and calls the {@link module:autosave/autosave~Adapter#save} method.
*
* ClassicEditor
* .create( document.querySelector( '#editor' ), {
* plugins: [ ArticlePluginSet, Autosave ],
* toolbar: [ 'heading', '|', 'bold', 'italic', 'link', 'bulletedList', 'numberedList', 'blockQuote', 'undo', 'redo' ],
* image: {
* toolbar: [ 'imageStyle:full', 'imageStyle:side', '|', 'imageTextAlternative' ],
* },
* autosave: {
* save() {
* // Note: saveEditorsContentToDatabase function should return a promise
* // which should be resolved when the saving action is complete.
* return saveEditorsContentToDatabase( data );
* }
* }
* } );
*/
export default class Autosave extends Plugin {
/**
* @inheritDoc
*/
static get pluginName() {
return 'Autosave';
}

/**
* @inheritDoc
*/
static get requires() {
return [ PendingActions ];
}

/**
* @inheritDoc
*/
constructor( editor ) {
super( editor );

/**
* The adapter is an object with the `save()` method. That method will be called whenever
* the model's data changes. It might be called some time after the change,
* since the event is throttled for performance reasons.
*
* @type {module:autosave/autosave~Adapter}
*/
this.adapter = undefined;

/**
* Throttled save method.
*
* @protected
* @type {Function}
*/
this._throttledSave = throttle( this._save.bind( this ), 500 );

/**
* Last document version.
*
* @protected
* @type {Number}
*/
this._lastDocumentVersion = editor.model.document.version;

/**
* DOM emitter.
*
* @private
* @type {DomEmitterMixin}
*/
this._domEmitter = Object.create( DomEmitterMixin );

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

/**
* An action that will be added to pending action manager for actions happening in that plugin.
*
* @private
* @type {Object|null}
*/
this._action = null;

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

/**
* Editor's pending actions manager.
*
* @private
* @member {@module:core/pendingactions~PendingActions} #_pendingActions
*/
}

/**
* @inheritDoc
*/
init() {
const editor = this.editor;
const doc = editor.model.document;

this._pendingActions = editor.plugins.get( PendingActions );

this.listenTo( doc, 'change:data', () => {
this._incrementCounter();

const willOriginalFunctionBeCalled = this._throttledSave();

if ( !willOriginalFunctionBeCalled ) {
this._decrementCounter();
}
} );

// Flush on the editor's destroy listener with the highest priority to ensure that
// `editor.getData()` will be called before plugins are destroyed.
this.listenTo( editor, 'destroy', () => this._flush(), { priority: 'highest' } );

// It's not possible to easy test it because karma uses `beforeunload` event
// to warn before full page reload and this event cannot be dispatched manually.
/* istanbul ignore next */
this._domEmitter.listenTo( window, 'beforeunload', ( evtInfo, domEvt ) => {
if ( this._pendingActions.isPending ) {
domEvt.returnValue = this._pendingActions.first.message;
}
} );
}

/**
* @inheritDoc
*/
destroy() {
// There's no need for canceling or flushing the throttled save, as
// it's done on the editor's destroy event with the highest priority.

this._domEmitter.stopListening();
super.destroy();
}

/**
* Invokes remaining `_save` method call.
*
* @protected
*/
_flush() {
this._throttledSave.flush();
}

/**
* If the adapter is set and new document version exists,
* `_save()` method creates a pending action and calls `adapter.save()` method.
* It waits for the result and then removes the created pending action.
*
* @private
*/
_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 );
}

// Marker's 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._decrementCounter();

return;
}

this._lastDocumentVersion = version;

Promise.all( saveCallbacks.map( cb => cb() ) )
.then( () => {
this._decrementCounter();
} );
}

/**
* Increments counter and adds pending action if it not exists.
*
* @private
*/
_incrementCounter() {
this._saveActionCounter++;

if ( !this._action ) {
this._action = this._pendingActions.add( 'Saving in progress.' );
}
}

/**
* 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;
}
}
}

/**
* An interface that requires the `save()` method.
*
* Used by {module:autosave/autosave~Autosave#adapter}
*
* @interface module:autosave/autosave~Adapter
*/

/**
* Method that will be called when the data model changes. It should return a promise (e.g. in case of saving content to the database),
* so the `Autosave` plugin will wait for that action before removing it from pending actions.
*
* @method #save
* @returns {Promise.<*>|undefined}
*/
68 changes: 68 additions & 0 deletions src/throttle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/

/**
* @module autosave/throttle
*/

/* globals window */

/**
* Throttle function - a helper that provides ability to specify minimum time gap between calling the original function.
* Comparing to the lodash implementation, this provides an information if calling the throttled function will result in
* calling the original function.
*
* @param {Function} fn Original function that will be called.
* @param {Number} wait Minimum amount of time between original function calls.
*/
export default function throttle( fn, wait ) {
// Time in ms of the last call.
let lastCallTime = 0;

// Timeout id that enables stopping scheduled call.
let timeoutId = null;

// @returns {Boolean} `true` if the original function was or will be called.
function throttledFn() {
const now = Date.now();

// Cancel call, as the next call is scheduled.
if ( timeoutId ) {
return false;
}

// Call instantly, as the fn wasn't called within the `time` period.
if ( now > lastCallTime + wait ) {
call();
return true;
}

// Set timeout, so the fn will be called `time` ms after the last call.
timeoutId = window.setTimeout( call, lastCallTime + wait - now );

return true;
}

throttledFn.flush = flush;

function flush() {
if ( timeoutId ) {
window.clearTimeout( timeoutId );
call();
}

lastCallTime = 0;
}

// Calls the original function and updates internals.
function call() {
lastCallTime = Date.now();
timeoutId = null;

fn();
}

return throttledFn;
}
Loading

0 comments on commit 05160ca

Please sign in to comment.