This repository has been archived by the owner on Jun 26, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
1,055 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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} | ||
*/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
Oops, something went wrong.