@ckeditor/ckeditor5-autosave 36.0.1 → 37.0.0-alpha.0
Sign up to get free protection for your applications and to get access to all the features.
- package/build/autosave.js +1 -1
- package/package.json +16 -11
- package/src/autosave.d.ts +233 -0
- package/src/autosave.js +205 -421
- package/src/index.d.ts +8 -0
- package/src/index.js +0 -2
package/build/autosave.js
CHANGED
@@ -2,4 +2,4 @@
|
|
2
2
|
/*!
|
3
3
|
* @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
|
4
4
|
* For licensing, see LICENSE.md.
|
5
|
-
*/(()=>{var t={704:(t,e,i)=>{t.exports=i(79)("./src/core.js")},209:(t,e,i)=>{t.exports=i(79)("./src/utils.js")},79:t=>{"use strict";t.exports=CKEditor5.dll}},e={};function i(n){var s=e[n];if(void 0!==s)return s.exports;var o=e[n]={exports:{}};return t[n](o,o.exports,i),o.exports}i.d=(t,e)=>{for(var n in e)i.o(e,n)&&!i.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:e[n]})},i.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),i.r=t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})};var n={};(()=>{"use strict";i.r(n),i.d(n,{Autosave:()=>L});var t=i(704),e=i(209);const s=function(t){var e=typeof t;return null!=t&&("object"==e||"function"==e)};const o="object"==typeof global&&global&&global.Object===Object&&global;var r="object"==typeof self&&self&&self.Object===Object&&self;const a=o||r||Function("return this")();const c=function(){return a.Date.now()};var u=/\s/;const
|
5
|
+
*/(()=>{var t={704:(t,e,i)=>{t.exports=i(79)("./src/core.js")},209:(t,e,i)=>{t.exports=i(79)("./src/utils.js")},79:t=>{"use strict";t.exports=CKEditor5.dll}},e={};function i(n){var s=e[n];if(void 0!==s)return s.exports;var o=e[n]={exports:{}};return t[n](o,o.exports,i),o.exports}i.d=(t,e)=>{for(var n in e)i.o(e,n)&&!i.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:e[n]})},i.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),i.r=t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})};var n={};(()=>{"use strict";i.r(n),i.d(n,{Autosave:()=>L});var t=i(704),e=i(209);const s=function(t){var e=typeof t;return null!=t&&("object"==e||"function"==e)};const o="object"==typeof global&&global&&global.Object===Object&&global;var r="object"==typeof self&&self&&self.Object===Object&&self;const a=o||r||Function("return this")();const c=function(){return a.Date.now()};var u=/\s/;const l=function(t){for(var e=t.length;e--&&u.test(t.charAt(e)););return e};var d=/^\s+/;const h=function(t){return t?t.slice(0,l(t)+1).replace(d,""):t};const v=a.Symbol;var f=Object.prototype,m=f.hasOwnProperty,g=f.toString,_=v?v.toStringTag:void 0;const p=function(t){var e=m.call(t,_),i=t[_];try{t[_]=void 0;var n=!0}catch(t){}var s=g.call(t);return n&&(e?t[_]=i:delete t[_]),s};var b=Object.prototype.toString;const y=function(t){return b.call(t)};var S="[object Null]",T="[object Undefined]",j=v?v.toStringTag:void 0;const w=function(t){return null==t?void 0===t?T:S:j&&j in Object(t)?p(t):y(t)};const O=function(t){return null!=t&&"object"==typeof t};var P="[object Symbol]";const A=function(t){return"symbol"==typeof t||O(t)&&w(t)==P};var x=NaN,E=/^[-+]0x[0-9a-f]+$/i,I=/^0b[01]+$/i,C=/^0o[0-7]+$/i,D=parseInt;const N=function(t){if("number"==typeof t)return t;if(A(t))return x;if(s(t)){var e="function"==typeof t.valueOf?t.valueOf():t;t=s(e)?e+"":e}if("string"!=typeof t)return 0===t?t:+t;t=h(t);var i=I.test(t);return i||C.test(t)?D(t.slice(2),i?2:8):E.test(t)?x:+t};var k="Expected a function",K=Math.max,M=Math.min;const V=function(t,e,i){var n,o,r,a,u,l,d=0,h=!1,v=!1,f=!0;if("function"!=typeof t)throw new TypeError(k);function m(e){var i=n,s=o;return n=o=void 0,d=e,a=t.apply(s,i)}function g(t){var i=t-l;return void 0===l||i>=e||i<0||v&&t-d>=r}function _(){var t=c();if(g(t))return p(t);u=setTimeout(_,function(t){var i=e-(t-l);return v?M(i,r-(t-d)):i}(t))}function p(t){return u=void 0,f&&n?m(t):(n=o=void 0,a)}function b(){var t=c(),i=g(t);if(n=arguments,o=this,l=t,i){if(void 0===u)return function(t){return d=t,u=setTimeout(_,e),h?m(t):a}(l);if(v)return clearTimeout(u),u=setTimeout(_,e),m(l)}return void 0===u&&(u=setTimeout(_,e)),a}return e=N(e)||0,s(i)&&(h=!!i.leading,r=(v="maxWait"in i)?K(N(i.maxWait)||0,e):r,f="trailing"in i?!!i.trailing:f),b.cancel=function(){void 0!==u&&clearTimeout(u),d=0,n=l=o=u=void 0},b.flush=function(){return void 0===u?a:p(c())},b};class L extends t.Plugin{static get pluginName(){return"Autosave"}static get requires(){return[t.PendingActions]}constructor(i){super(i),this._action=null;const n=i.config.get("autosave")||{},s=n.waitingTime||1e3;this.set("state","synchronized"),this._debouncedSave=V(this._save.bind(this),s),this._lastDocumentVersion=i.model.document.version,this._savePromise=null,this._domEmitter=new((0,e.DomEmitterMixin)()),this._config=n,this._pendingActions=i.plugins.get(t.PendingActions),this._makeImmediateSave=!1}init(){const t=this.editor,e=t.model.document;this.listenTo(t,"ready",(()=>{this.listenTo(e,"change:data",((t,e)=>{this._saveCallbacks.length&&e.isLocal&&("synchronized"===this.state&&(this.state="waiting",this._setPendingAction()),"waiting"===this.state&&this._debouncedSave())}))})),this.listenTo(t,"destroy",(()=>this._flush()),{priority:"highest"}),this._domEmitter.listenTo(window,"beforeunload",((t,e)=>{this._pendingActions.hasAny&&(e.returnValue=this._pendingActions.first.message)}))}destroy(){this._domEmitter.stopListening(),super.destroy()}save(){return this._debouncedSave.cancel(),this._save()}_flush(){this._debouncedSave.flush()}_save(){return this._savePromise?(this._makeImmediateSave=this.editor.model.document.version>this._lastDocumentVersion,this._savePromise):(this._setPendingAction(),this.state="saving",this._lastDocumentVersion=this.editor.model.document.version,this._savePromise=Promise.resolve().then((()=>Promise.all(this._saveCallbacks.map((t=>t(this.editor)))))).finally((()=>{this._savePromise=null})).then((()=>{if(this._makeImmediateSave)return this._makeImmediateSave=!1,this._save();this.editor.model.document.version>this._lastDocumentVersion?(this.state="waiting",this._debouncedSave()):(this.state="synchronized",this._pendingActions.remove(this._action),this._action=null)})).catch((t=>{throw this.state="error",this.state="saving",this._debouncedSave(),t})),this._savePromise)}_setPendingAction(){const t=this.editor.t;this._action||(this._action=this._pendingActions.add(t("Saving changes")))}get _saveCallbacks(){const t=[];return this.adapter&&this.adapter.save&&t.push(this.adapter.save),this._config.save&&t.push(this._config.save),t}}})(),(window.CKEditor5=window.CKEditor5||{}).autosave=n})();
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@ckeditor/ckeditor5-autosave",
|
3
|
-
"version": "
|
3
|
+
"version": "37.0.0-alpha.0",
|
4
4
|
"description": "Autosave feature for CKEditor 5.",
|
5
5
|
"keywords": [
|
6
6
|
"ckeditor",
|
@@ -12,16 +12,17 @@
|
|
12
12
|
],
|
13
13
|
"main": "src/index.js",
|
14
14
|
"dependencies": {
|
15
|
-
"ckeditor5": "^
|
15
|
+
"ckeditor5": "^37.0.0-alpha.0",
|
16
16
|
"lodash-es": "^4.17.15"
|
17
17
|
},
|
18
18
|
"devDependencies": {
|
19
|
-
"@ckeditor/ckeditor5-core": "^
|
20
|
-
"@ckeditor/ckeditor5-dev-utils": "^
|
21
|
-
"@ckeditor/ckeditor5-editor-classic": "^
|
22
|
-
"@ckeditor/ckeditor5-paragraph": "^
|
23
|
-
"@ckeditor/ckeditor5-source-editing": "^
|
24
|
-
"@ckeditor/ckeditor5-theme-lark": "^
|
19
|
+
"@ckeditor/ckeditor5-core": "^37.0.0-alpha.0",
|
20
|
+
"@ckeditor/ckeditor5-dev-utils": "^34.0.0",
|
21
|
+
"@ckeditor/ckeditor5-editor-classic": "^37.0.0-alpha.0",
|
22
|
+
"@ckeditor/ckeditor5-paragraph": "^37.0.0-alpha.0",
|
23
|
+
"@ckeditor/ckeditor5-source-editing": "^37.0.0-alpha.0",
|
24
|
+
"@ckeditor/ckeditor5-theme-lark": "^37.0.0-alpha.0",
|
25
|
+
"typescript": "^4.8.4",
|
25
26
|
"webpack": "^5.58.1",
|
26
27
|
"webpack-cli": "^4.9.0"
|
27
28
|
},
|
@@ -40,13 +41,17 @@
|
|
40
41
|
},
|
41
42
|
"files": [
|
42
43
|
"lang",
|
43
|
-
"src",
|
44
|
+
"src/**/*.js",
|
45
|
+
"src/**/*.d.ts",
|
44
46
|
"theme",
|
45
47
|
"build",
|
46
48
|
"ckeditor5-metadata.json",
|
47
49
|
"CHANGELOG.md"
|
48
50
|
],
|
49
51
|
"scripts": {
|
50
|
-
"dll:build": "webpack"
|
51
|
-
|
52
|
+
"dll:build": "webpack",
|
53
|
+
"build": "tsc -p ./tsconfig.release.json",
|
54
|
+
"postversion": "npm run build"
|
55
|
+
},
|
56
|
+
"types": "src/index.d.ts"
|
52
57
|
}
|
@@ -0,0 +1,233 @@
|
|
1
|
+
/**
|
2
|
+
* @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
|
3
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
4
|
+
*/
|
5
|
+
/**
|
6
|
+
* @module autosave/autosave
|
7
|
+
*/
|
8
|
+
import { Plugin, type PluginDependencies, type Editor } from 'ckeditor5/src/core';
|
9
|
+
/**
|
10
|
+
* The {@link module:autosave/autosave~Autosave} plugin allows you to automatically save the data (e.g. send it to the server)
|
11
|
+
* when needed (when the user changed the content).
|
12
|
+
*
|
13
|
+
* It listens to the {@link module:engine/model/document~Document#event:change:data `editor.model.document#change:data`}
|
14
|
+
* and `window#beforeunload` events and calls the
|
15
|
+
* {@link module:autosave/autosave~AutosaveAdapter#save `config.autosave.save()`} function.
|
16
|
+
*
|
17
|
+
* ```ts
|
18
|
+
* ClassicEditor
|
19
|
+
* .create( document.querySelector( '#editor' ), {
|
20
|
+
* plugins: [ ArticlePluginSet, Autosave ],
|
21
|
+
* toolbar: [ 'heading', '|', 'bold', 'italic', 'link', 'bulletedList', 'numberedList', 'blockQuote', 'undo', 'redo' ],
|
22
|
+
* image: {
|
23
|
+
* toolbar: [ 'imageStyle:block', 'imageStyle:side', '|', 'toggleImageCaption', 'imageTextAlternative' ],
|
24
|
+
* },
|
25
|
+
* autosave: {
|
26
|
+
* save( editor: Editor ) {
|
27
|
+
* // The saveData() function must return a promise
|
28
|
+
* // which should be resolved when the data is successfully saved.
|
29
|
+
* return saveData( editor.getData() );
|
30
|
+
* }
|
31
|
+
* }
|
32
|
+
* } );
|
33
|
+
* ```
|
34
|
+
*
|
35
|
+
* Read more about this feature in the {@glink installation/getting-started/getting-and-setting-data#autosave-feature Autosave feature}
|
36
|
+
* section of the {@glink installation/getting-started/getting-and-setting-data Saving and getting data}.
|
37
|
+
*/
|
38
|
+
export default class Autosave extends Plugin {
|
39
|
+
/**
|
40
|
+
* The adapter is an object with a `save()` method. That method will be called whenever
|
41
|
+
* the data changes. It might be called some time after the change,
|
42
|
+
* since the event is throttled for performance reasons.
|
43
|
+
*/
|
44
|
+
adapter?: AutosaveAdapter;
|
45
|
+
/**
|
46
|
+
* The state of this plugin.
|
47
|
+
*
|
48
|
+
* The plugin can be in the following states:
|
49
|
+
*
|
50
|
+
* * synchronized – When all changes are saved.
|
51
|
+
* * waiting – When the plugin is waiting for other changes before calling `adapter#save()` and `config.autosave.save()`.
|
52
|
+
* * saving – When the provided save method is called and the plugin waits for the response.
|
53
|
+
* * error &ndash When the provided save method will throw an error. This state immediately changes to the `saving` state and
|
54
|
+
* the save method will be called again in the short period of time.
|
55
|
+
*
|
56
|
+
* @observable
|
57
|
+
* @readonly
|
58
|
+
*/
|
59
|
+
state: 'synchronized' | 'waiting' | 'saving' | 'error';
|
60
|
+
/**
|
61
|
+
* Debounced save method. The `save()` method is called the specified `waitingTime` after `debouncedSave()` is called,
|
62
|
+
* unless a new action happens in the meantime.
|
63
|
+
*/
|
64
|
+
private _debouncedSave;
|
65
|
+
/**
|
66
|
+
* The last saved document version.
|
67
|
+
*/
|
68
|
+
private _lastDocumentVersion;
|
69
|
+
/**
|
70
|
+
* Promise used for asynchronous save calls.
|
71
|
+
*
|
72
|
+
* Created to handle the autosave call to an external data source. It resolves when that call is finished. It is re-used if
|
73
|
+
* save is called before the promise has been resolved. It is set to `null` if there is no call in progress.
|
74
|
+
*/
|
75
|
+
private _savePromise;
|
76
|
+
/**
|
77
|
+
* DOM emitter.
|
78
|
+
*/
|
79
|
+
private _domEmitter;
|
80
|
+
/**
|
81
|
+
* The configuration of this plugins.
|
82
|
+
*/
|
83
|
+
private _config;
|
84
|
+
/**
|
85
|
+
* Editor's pending actions manager.
|
86
|
+
*/
|
87
|
+
private _pendingActions;
|
88
|
+
/**
|
89
|
+
* Informs whether there should be another autosave callback performed, immediately after current autosave callback finishes.
|
90
|
+
*
|
91
|
+
* This is set to `true` when there is a save request while autosave callback is already being processed
|
92
|
+
* and the model has changed since the last save.
|
93
|
+
*/
|
94
|
+
private _makeImmediateSave;
|
95
|
+
/**
|
96
|
+
* An action that will be added to the pending action manager for actions happening in that plugin.
|
97
|
+
*/
|
98
|
+
private _action;
|
99
|
+
/**
|
100
|
+
* @inheritDoc
|
101
|
+
*/
|
102
|
+
static get pluginName(): 'Autosave';
|
103
|
+
/**
|
104
|
+
* @inheritDoc
|
105
|
+
*/
|
106
|
+
static get requires(): PluginDependencies;
|
107
|
+
/**
|
108
|
+
* @inheritDoc
|
109
|
+
*/
|
110
|
+
constructor(editor: Editor);
|
111
|
+
/**
|
112
|
+
* @inheritDoc
|
113
|
+
*/
|
114
|
+
init(): void;
|
115
|
+
/**
|
116
|
+
* @inheritDoc
|
117
|
+
*/
|
118
|
+
destroy(): void;
|
119
|
+
/**
|
120
|
+
* Immediately calls autosave callback. All previously queued (debounced) callbacks are cleared. If there is already an autosave
|
121
|
+
* callback in progress, then the requested save will be performed immediately after the current callback finishes.
|
122
|
+
*
|
123
|
+
* @returns A promise that will be resolved when the autosave callback is finished.
|
124
|
+
*/
|
125
|
+
save(): Promise<void>;
|
126
|
+
/**
|
127
|
+
* Invokes the remaining `_save()` method call.
|
128
|
+
*/
|
129
|
+
private _flush;
|
130
|
+
/**
|
131
|
+
* If the adapter is set and a new document version exists,
|
132
|
+
* the `_save()` method creates a pending action and calls the `adapter.save()` method.
|
133
|
+
* It waits for the result and then removes the created pending action.
|
134
|
+
*
|
135
|
+
* @returns A promise that will be resolved when the autosave callback is finished.
|
136
|
+
*/
|
137
|
+
private _save;
|
138
|
+
/**
|
139
|
+
* Creates a pending action if it is not set already.
|
140
|
+
*/
|
141
|
+
private _setPendingAction;
|
142
|
+
/**
|
143
|
+
* Saves callbacks.
|
144
|
+
*/
|
145
|
+
private get _saveCallbacks();
|
146
|
+
}
|
147
|
+
/**
|
148
|
+
* An interface that requires the `save()` method.
|
149
|
+
*
|
150
|
+
* Used by {@link module:autosave/autosave~Autosave#adapter}.
|
151
|
+
*/
|
152
|
+
export interface AutosaveAdapter {
|
153
|
+
/**
|
154
|
+
* The method that will be called when the data changes. It should return a promise (e.g. in case of saving content to the database),
|
155
|
+
* so the autosave plugin will wait for that action before removing it from pending actions.
|
156
|
+
*/
|
157
|
+
save(editor: Editor): Promise<unknown>;
|
158
|
+
}
|
159
|
+
/**
|
160
|
+
* The configuration of the {@link module:autosave/autosave~Autosave autosave feature}.
|
161
|
+
*
|
162
|
+
* ```ts
|
163
|
+
* ClassicEditor
|
164
|
+
* .create( editorElement, {
|
165
|
+
* autosave: {
|
166
|
+
* save( editor: Editor ) {
|
167
|
+
* // The saveData() function must return a promise
|
168
|
+
* // which should be resolved when the data is successfully saved.
|
169
|
+
* return saveData( editor.getData() );
|
170
|
+
* }
|
171
|
+
* }
|
172
|
+
* } );
|
173
|
+
* .then( ... )
|
174
|
+
* .catch( ... );
|
175
|
+
* ```
|
176
|
+
*
|
177
|
+
* See {@link module:core/editor/editorconfig~EditorConfig all editor configuration options}.
|
178
|
+
*
|
179
|
+
* See also the demo of the {@glink installation/getting-started/getting-and-setting-data#autosave-feature autosave feature}.
|
180
|
+
*/
|
181
|
+
export interface AutosaveConfig {
|
182
|
+
/**
|
183
|
+
* The callback to be executed when the data needs to be saved.
|
184
|
+
*
|
185
|
+
* This function must return a promise which should be resolved when the data is successfully saved.
|
186
|
+
*
|
187
|
+
* ```ts
|
188
|
+
* ClassicEditor
|
189
|
+
* .create( editorElement, {
|
190
|
+
* autosave: {
|
191
|
+
* save( editor: Editor ) {
|
192
|
+
* return saveData( editor.getData() );
|
193
|
+
* }
|
194
|
+
* }
|
195
|
+
* } );
|
196
|
+
* .then( ... )
|
197
|
+
* .catch( ... );
|
198
|
+
* ```
|
199
|
+
*/
|
200
|
+
save?: (editor: Editor) => Promise<unknown>;
|
201
|
+
/**
|
202
|
+
* The minimum amount of time that needs to pass after the last action to call the provided callback.
|
203
|
+
* By default it is 1000 ms.
|
204
|
+
*
|
205
|
+
* ```ts
|
206
|
+
* ClassicEditor
|
207
|
+
* .create( editorElement, {
|
208
|
+
* autosave: {
|
209
|
+
* save( editor: Editor ) {
|
210
|
+
* return saveData( editor.getData() );
|
211
|
+
* },
|
212
|
+
* waitingTime: 2000
|
213
|
+
* }
|
214
|
+
* } );
|
215
|
+
* .then( ... )
|
216
|
+
* .catch( ... );
|
217
|
+
* ```
|
218
|
+
*/
|
219
|
+
waitingTime?: number;
|
220
|
+
}
|
221
|
+
declare module '@ckeditor/ckeditor5-core' {
|
222
|
+
interface PluginsMap {
|
223
|
+
[Autosave.pluginName]: Autosave;
|
224
|
+
}
|
225
|
+
interface EditorConfig {
|
226
|
+
/**
|
227
|
+
* The configuration of the {@link module:autosave/autosave~Autosave autosave feature}.
|
228
|
+
*
|
229
|
+
* Read more in {@link module:autosave/autosave~AutosaveConfig}.
|
230
|
+
*/
|
231
|
+
autosave?: AutosaveConfig;
|
232
|
+
}
|
233
|
+
}
|
package/src/autosave.js
CHANGED
@@ -2,17 +2,13 @@
|
|
2
2
|
* @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
|
3
3
|
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
4
4
|
*/
|
5
|
-
|
6
5
|
/**
|
7
6
|
* @module autosave/autosave
|
8
7
|
*/
|
9
|
-
|
10
8
|
import { Plugin, PendingActions } from 'ckeditor5/src/core';
|
11
|
-
import { DomEmitterMixin
|
9
|
+
import { DomEmitterMixin } from 'ckeditor5/src/utils';
|
12
10
|
import { debounce } from 'lodash-es';
|
13
|
-
|
14
11
|
/* globals window */
|
15
|
-
|
16
12
|
/**
|
17
13
|
* The {@link module:autosave/autosave~Autosave} plugin allows you to automatically save the data (e.g. send it to the server)
|
18
14
|
* when needed (when the user changed the content).
|
@@ -21,425 +17,213 @@ import { debounce } from 'lodash-es';
|
|
21
17
|
* and `window#beforeunload` events and calls the
|
22
18
|
* {@link module:autosave/autosave~AutosaveAdapter#save `config.autosave.save()`} function.
|
23
19
|
*
|
24
|
-
*
|
25
|
-
*
|
26
|
-
*
|
27
|
-
*
|
28
|
-
*
|
29
|
-
*
|
30
|
-
*
|
31
|
-
*
|
32
|
-
*
|
33
|
-
*
|
34
|
-
*
|
35
|
-
*
|
36
|
-
*
|
37
|
-
*
|
38
|
-
*
|
20
|
+
* ```ts
|
21
|
+
* ClassicEditor
|
22
|
+
* .create( document.querySelector( '#editor' ), {
|
23
|
+
* plugins: [ ArticlePluginSet, Autosave ],
|
24
|
+
* toolbar: [ 'heading', '|', 'bold', 'italic', 'link', 'bulletedList', 'numberedList', 'blockQuote', 'undo', 'redo' ],
|
25
|
+
* image: {
|
26
|
+
* toolbar: [ 'imageStyle:block', 'imageStyle:side', '|', 'toggleImageCaption', 'imageTextAlternative' ],
|
27
|
+
* },
|
28
|
+
* autosave: {
|
29
|
+
* save( editor: Editor ) {
|
30
|
+
* // The saveData() function must return a promise
|
31
|
+
* // which should be resolved when the data is successfully saved.
|
32
|
+
* return saveData( editor.getData() );
|
33
|
+
* }
|
34
|
+
* }
|
35
|
+
* } );
|
36
|
+
* ```
|
39
37
|
*
|
40
38
|
* Read more about this feature in the {@glink installation/getting-started/getting-and-setting-data#autosave-feature Autosave feature}
|
41
39
|
* section of the {@glink installation/getting-started/getting-and-setting-data Saving and getting data}.
|
42
|
-
*
|
43
|
-
* @extends module:core/plugin~Plugin
|
44
40
|
*/
|
45
41
|
export default class Autosave extends Plugin {
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
this._debouncedSave.cancel();
|
234
|
-
|
235
|
-
return this._save();
|
236
|
-
}
|
237
|
-
|
238
|
-
/**
|
239
|
-
* Invokes the remaining `_save()` method call.
|
240
|
-
*
|
241
|
-
* @protected
|
242
|
-
*/
|
243
|
-
_flush() {
|
244
|
-
this._debouncedSave.flush();
|
245
|
-
}
|
246
|
-
|
247
|
-
/**
|
248
|
-
* If the adapter is set and a new document version exists,
|
249
|
-
* the `_save()` method creates a pending action and calls the `adapter.save()` method.
|
250
|
-
* It waits for the result and then removes the created pending action.
|
251
|
-
*
|
252
|
-
* @private
|
253
|
-
* @returns {Promise} A promise that will be resolved when the autosave callback is finished.
|
254
|
-
*/
|
255
|
-
_save() {
|
256
|
-
if ( this._savePromise ) {
|
257
|
-
this._makeImmediateSave = this.editor.model.document.version > this._lastDocumentVersion;
|
258
|
-
|
259
|
-
return this._savePromise;
|
260
|
-
}
|
261
|
-
|
262
|
-
// Make sure there is a pending action (in case if `_save()` was called through manual `save()` call).
|
263
|
-
this._setPendingAction();
|
264
|
-
|
265
|
-
this.state = 'saving';
|
266
|
-
this._lastDocumentVersion = this.editor.model.document.version;
|
267
|
-
|
268
|
-
// Wait one promise cycle to be sure that save callbacks are not called inside a conversion or when the editor's state changes.
|
269
|
-
this._savePromise = Promise.resolve()
|
270
|
-
// Make autosave callback.
|
271
|
-
.then( () => Promise.all(
|
272
|
-
this._saveCallbacks.map( cb => cb( this.editor ) )
|
273
|
-
) )
|
274
|
-
// When the autosave callback is finished, always clear `this._savePromise`, no matter if it was successful or not.
|
275
|
-
.finally( () => {
|
276
|
-
this._savePromise = null;
|
277
|
-
} )
|
278
|
-
// If the save was successful, we have three scenarios:
|
279
|
-
//
|
280
|
-
// 1. If a save was requested when an autosave callback was already processed, we need to immediately call
|
281
|
-
// another autosave callback. In this case, `this._savePromise` will not be resolved until the next callback is done.
|
282
|
-
// 2. Otherwise, if changes happened to the model, make a delayed autosave callback (like the change just happened).
|
283
|
-
// 3. If no changes happened to the model, return to the `synchronized` state.
|
284
|
-
.then( () => {
|
285
|
-
if ( this._makeImmediateSave ) {
|
286
|
-
this._makeImmediateSave = false;
|
287
|
-
|
288
|
-
// Start another autosave callback. Return a promise that will be resolved after the new autosave callback.
|
289
|
-
// This way promises returned by `_save()` will not be resolved until all changes are saved.
|
290
|
-
//
|
291
|
-
// If `save()` was called when another (most often automatic) autosave callback was already processed,
|
292
|
-
// the promise returned by `save()` call will be resolved only after new changes have been saved.
|
293
|
-
//
|
294
|
-
// Note that it would not work correctly if `this._savePromise` is not cleared.
|
295
|
-
return this._save();
|
296
|
-
} else {
|
297
|
-
if ( this.editor.model.document.version > this._lastDocumentVersion ) {
|
298
|
-
this.state = 'waiting';
|
299
|
-
this._debouncedSave();
|
300
|
-
} else {
|
301
|
-
this.state = 'synchronized';
|
302
|
-
this._pendingActions.remove( this._action );
|
303
|
-
this._action = null;
|
304
|
-
}
|
305
|
-
}
|
306
|
-
} )
|
307
|
-
// In case of an error, retry the autosave callback after a delay (and also throw the original error).
|
308
|
-
.catch( err => {
|
309
|
-
// Change state to `error` so that listeners handling autosave error can be called.
|
310
|
-
this.state = 'error';
|
311
|
-
// Then, immediately change to the `saving` state as described above.
|
312
|
-
// Being in the `saving` state ensures that the autosave callback won't be delayed further by the `change:data` listener.
|
313
|
-
this.state = 'saving';
|
314
|
-
|
315
|
-
this._debouncedSave();
|
316
|
-
|
317
|
-
throw err;
|
318
|
-
} );
|
319
|
-
|
320
|
-
return this._savePromise;
|
321
|
-
}
|
322
|
-
|
323
|
-
/**
|
324
|
-
* Creates a pending action if it is not set already.
|
325
|
-
*
|
326
|
-
* @private
|
327
|
-
*/
|
328
|
-
_setPendingAction() {
|
329
|
-
const t = this.editor.t;
|
330
|
-
|
331
|
-
if ( !this._action ) {
|
332
|
-
this._action = this._pendingActions.add( t( 'Saving changes' ) );
|
333
|
-
}
|
334
|
-
}
|
335
|
-
|
336
|
-
/**
|
337
|
-
* Saves callbacks.
|
338
|
-
*
|
339
|
-
* @private
|
340
|
-
* @type {Array.<Function>}
|
341
|
-
*/
|
342
|
-
get _saveCallbacks() {
|
343
|
-
const saveCallbacks = [];
|
344
|
-
|
345
|
-
if ( this.adapter && this.adapter.save ) {
|
346
|
-
saveCallbacks.push( this.adapter.save );
|
347
|
-
}
|
348
|
-
|
349
|
-
if ( this._config.save ) {
|
350
|
-
saveCallbacks.push( this._config.save );
|
351
|
-
}
|
352
|
-
|
353
|
-
return saveCallbacks;
|
354
|
-
}
|
42
|
+
/**
|
43
|
+
* @inheritDoc
|
44
|
+
*/
|
45
|
+
static get pluginName() {
|
46
|
+
return 'Autosave';
|
47
|
+
}
|
48
|
+
/**
|
49
|
+
* @inheritDoc
|
50
|
+
*/
|
51
|
+
static get requires() {
|
52
|
+
return [PendingActions];
|
53
|
+
}
|
54
|
+
/**
|
55
|
+
* @inheritDoc
|
56
|
+
*/
|
57
|
+
constructor(editor) {
|
58
|
+
super(editor);
|
59
|
+
/**
|
60
|
+
* An action that will be added to the pending action manager for actions happening in that plugin.
|
61
|
+
*/
|
62
|
+
this._action = null;
|
63
|
+
const config = editor.config.get('autosave') || {};
|
64
|
+
// A minimum amount of time that needs to pass after the last action.
|
65
|
+
// After that time the provided save callbacks are being called.
|
66
|
+
const waitingTime = config.waitingTime || 1000;
|
67
|
+
this.set('state', 'synchronized');
|
68
|
+
this._debouncedSave = debounce(this._save.bind(this), waitingTime);
|
69
|
+
this._lastDocumentVersion = editor.model.document.version;
|
70
|
+
this._savePromise = null;
|
71
|
+
this._domEmitter = new (DomEmitterMixin())();
|
72
|
+
this._config = config;
|
73
|
+
this._pendingActions = editor.plugins.get(PendingActions);
|
74
|
+
this._makeImmediateSave = false;
|
75
|
+
}
|
76
|
+
/**
|
77
|
+
* @inheritDoc
|
78
|
+
*/
|
79
|
+
init() {
|
80
|
+
const editor = this.editor;
|
81
|
+
const doc = editor.model.document;
|
82
|
+
// Add the listener only after the editor is initialized to prevent firing save callback on data init.
|
83
|
+
this.listenTo(editor, 'ready', () => {
|
84
|
+
this.listenTo(doc, 'change:data', (evt, batch) => {
|
85
|
+
if (!this._saveCallbacks.length) {
|
86
|
+
return;
|
87
|
+
}
|
88
|
+
if (!batch.isLocal) {
|
89
|
+
return;
|
90
|
+
}
|
91
|
+
if (this.state === 'synchronized') {
|
92
|
+
this.state = 'waiting';
|
93
|
+
// Set pending action already when we are waiting for the autosave callback.
|
94
|
+
this._setPendingAction();
|
95
|
+
}
|
96
|
+
if (this.state === 'waiting') {
|
97
|
+
this._debouncedSave();
|
98
|
+
}
|
99
|
+
// If the plugin is in `saving` state, it will change its state later basing on the `document.version`.
|
100
|
+
// If the `document.version` will be higher than stored `#_lastDocumentVersion`, then it means, that some `change:data`
|
101
|
+
// event has fired in the meantime.
|
102
|
+
});
|
103
|
+
});
|
104
|
+
// Flush on the editor's destroy listener with the highest priority to ensure that
|
105
|
+
// `editor.getData()` will be called before plugins are destroyed.
|
106
|
+
this.listenTo(editor, 'destroy', () => this._flush(), { priority: 'highest' });
|
107
|
+
// It's not possible to easy test it because karma uses `beforeunload` event
|
108
|
+
// to warn before full page reload and this event cannot be dispatched manually.
|
109
|
+
/* istanbul ignore next */
|
110
|
+
this._domEmitter.listenTo(window, 'beforeunload', (evtInfo, domEvt) => {
|
111
|
+
if (this._pendingActions.hasAny) {
|
112
|
+
domEvt.returnValue = this._pendingActions.first.message;
|
113
|
+
}
|
114
|
+
});
|
115
|
+
}
|
116
|
+
/**
|
117
|
+
* @inheritDoc
|
118
|
+
*/
|
119
|
+
destroy() {
|
120
|
+
// There's no need for canceling or flushing the throttled save, as
|
121
|
+
// it's done on the editor's destroy event with the highest priority.
|
122
|
+
this._domEmitter.stopListening();
|
123
|
+
super.destroy();
|
124
|
+
}
|
125
|
+
/**
|
126
|
+
* Immediately calls autosave callback. All previously queued (debounced) callbacks are cleared. If there is already an autosave
|
127
|
+
* callback in progress, then the requested save will be performed immediately after the current callback finishes.
|
128
|
+
*
|
129
|
+
* @returns A promise that will be resolved when the autosave callback is finished.
|
130
|
+
*/
|
131
|
+
save() {
|
132
|
+
this._debouncedSave.cancel();
|
133
|
+
return this._save();
|
134
|
+
}
|
135
|
+
/**
|
136
|
+
* Invokes the remaining `_save()` method call.
|
137
|
+
*/
|
138
|
+
_flush() {
|
139
|
+
this._debouncedSave.flush();
|
140
|
+
}
|
141
|
+
/**
|
142
|
+
* If the adapter is set and a new document version exists,
|
143
|
+
* the `_save()` method creates a pending action and calls the `adapter.save()` method.
|
144
|
+
* It waits for the result and then removes the created pending action.
|
145
|
+
*
|
146
|
+
* @returns A promise that will be resolved when the autosave callback is finished.
|
147
|
+
*/
|
148
|
+
_save() {
|
149
|
+
if (this._savePromise) {
|
150
|
+
this._makeImmediateSave = this.editor.model.document.version > this._lastDocumentVersion;
|
151
|
+
return this._savePromise;
|
152
|
+
}
|
153
|
+
// Make sure there is a pending action (in case if `_save()` was called through manual `save()` call).
|
154
|
+
this._setPendingAction();
|
155
|
+
this.state = 'saving';
|
156
|
+
this._lastDocumentVersion = this.editor.model.document.version;
|
157
|
+
// Wait one promise cycle to be sure that save callbacks are not called inside a conversion or when the editor's state changes.
|
158
|
+
this._savePromise = Promise.resolve()
|
159
|
+
// Make autosave callback.
|
160
|
+
.then(() => Promise.all(this._saveCallbacks.map(cb => cb(this.editor))))
|
161
|
+
// When the autosave callback is finished, always clear `this._savePromise`, no matter if it was successful or not.
|
162
|
+
.finally(() => {
|
163
|
+
this._savePromise = null;
|
164
|
+
})
|
165
|
+
// If the save was successful, we have three scenarios:
|
166
|
+
//
|
167
|
+
// 1. If a save was requested when an autosave callback was already processed, we need to immediately call
|
168
|
+
// another autosave callback. In this case, `this._savePromise` will not be resolved until the next callback is done.
|
169
|
+
// 2. Otherwise, if changes happened to the model, make a delayed autosave callback (like the change just happened).
|
170
|
+
// 3. If no changes happened to the model, return to the `synchronized` state.
|
171
|
+
.then(() => {
|
172
|
+
if (this._makeImmediateSave) {
|
173
|
+
this._makeImmediateSave = false;
|
174
|
+
// Start another autosave callback. Return a promise that will be resolved after the new autosave callback.
|
175
|
+
// This way promises returned by `_save()` will not be resolved until all changes are saved.
|
176
|
+
//
|
177
|
+
// If `save()` was called when another (most often automatic) autosave callback was already processed,
|
178
|
+
// the promise returned by `save()` call will be resolved only after new changes have been saved.
|
179
|
+
//
|
180
|
+
// Note that it would not work correctly if `this._savePromise` is not cleared.
|
181
|
+
return this._save();
|
182
|
+
}
|
183
|
+
else {
|
184
|
+
if (this.editor.model.document.version > this._lastDocumentVersion) {
|
185
|
+
this.state = 'waiting';
|
186
|
+
this._debouncedSave();
|
187
|
+
}
|
188
|
+
else {
|
189
|
+
this.state = 'synchronized';
|
190
|
+
this._pendingActions.remove(this._action);
|
191
|
+
this._action = null;
|
192
|
+
}
|
193
|
+
}
|
194
|
+
})
|
195
|
+
// In case of an error, retry the autosave callback after a delay (and also throw the original error).
|
196
|
+
.catch(err => {
|
197
|
+
// Change state to `error` so that listeners handling autosave error can be called.
|
198
|
+
this.state = 'error';
|
199
|
+
// Then, immediately change to the `saving` state as described above.
|
200
|
+
// Being in the `saving` state ensures that the autosave callback won't be delayed further by the `change:data` listener.
|
201
|
+
this.state = 'saving';
|
202
|
+
this._debouncedSave();
|
203
|
+
throw err;
|
204
|
+
});
|
205
|
+
return this._savePromise;
|
206
|
+
}
|
207
|
+
/**
|
208
|
+
* Creates a pending action if it is not set already.
|
209
|
+
*/
|
210
|
+
_setPendingAction() {
|
211
|
+
const t = this.editor.t;
|
212
|
+
if (!this._action) {
|
213
|
+
this._action = this._pendingActions.add(t('Saving changes'));
|
214
|
+
}
|
215
|
+
}
|
216
|
+
/**
|
217
|
+
* Saves callbacks.
|
218
|
+
*/
|
219
|
+
get _saveCallbacks() {
|
220
|
+
const saveCallbacks = [];
|
221
|
+
if (this.adapter && this.adapter.save) {
|
222
|
+
saveCallbacks.push(this.adapter.save);
|
223
|
+
}
|
224
|
+
if (this._config.save) {
|
225
|
+
saveCallbacks.push(this._config.save);
|
226
|
+
}
|
227
|
+
return saveCallbacks;
|
228
|
+
}
|
355
229
|
}
|
356
|
-
|
357
|
-
mix( Autosave, ObservableMixin );
|
358
|
-
|
359
|
-
/**
|
360
|
-
* An interface that requires the `save()` method.
|
361
|
-
*
|
362
|
-
* Used by {@link module:autosave/autosave~Autosave#adapter}.
|
363
|
-
*
|
364
|
-
* @interface module:autosave/autosave~AutosaveAdapter
|
365
|
-
*/
|
366
|
-
|
367
|
-
/**
|
368
|
-
* The method that will be called when the data changes. It should return a promise (e.g. in case of saving content to the database),
|
369
|
-
* so the autosave plugin will wait for that action before removing it from pending actions.
|
370
|
-
*
|
371
|
-
* @method #save
|
372
|
-
* @param {module:core/editor/editor~Editor} editor The editor instance.
|
373
|
-
* @returns {Promise.<*>}
|
374
|
-
*/
|
375
|
-
|
376
|
-
/**
|
377
|
-
* The configuration of the {@link module:autosave/autosave~Autosave autosave feature}.
|
378
|
-
*
|
379
|
-
* Read more in {@link module:autosave/autosave~AutosaveConfig}.
|
380
|
-
*
|
381
|
-
* @member {module:autosave/autosave~AutosaveConfig} module:core/editor/editorconfig~EditorConfig#autosave
|
382
|
-
*/
|
383
|
-
|
384
|
-
/**
|
385
|
-
* The configuration of the {@link module:autosave/autosave~Autosave autosave feature}.
|
386
|
-
*
|
387
|
-
* ClassicEditor
|
388
|
-
* .create( editorElement, {
|
389
|
-
* autosave: {
|
390
|
-
* save( editor ) {
|
391
|
-
* // The saveData() function must return a promise
|
392
|
-
* // which should be resolved when the data is successfully saved.
|
393
|
-
* return saveData( editor.getData() );
|
394
|
-
* }
|
395
|
-
* }
|
396
|
-
* } );
|
397
|
-
* .then( ... )
|
398
|
-
* .catch( ... );
|
399
|
-
*
|
400
|
-
* See {@link module:core/editor/editorconfig~EditorConfig all editor configuration options}.
|
401
|
-
*
|
402
|
-
* See also the demo of the {@glink installation/getting-started/getting-and-setting-data#autosave-feature autosave feature}.
|
403
|
-
*
|
404
|
-
* @interface AutosaveConfig
|
405
|
-
*/
|
406
|
-
|
407
|
-
/**
|
408
|
-
* The callback to be executed when the data needs to be saved.
|
409
|
-
*
|
410
|
-
* This function must return a promise which should be resolved when the data is successfully saved.
|
411
|
-
*
|
412
|
-
* ClassicEditor
|
413
|
-
* .create( editorElement, {
|
414
|
-
* autosave: {
|
415
|
-
* save( editor ) {
|
416
|
-
* return saveData( editor.getData() );
|
417
|
-
* }
|
418
|
-
* }
|
419
|
-
* } );
|
420
|
-
* .then( ... )
|
421
|
-
* .catch( ... );
|
422
|
-
*
|
423
|
-
* @method module:autosave/autosave~AutosaveConfig#save
|
424
|
-
* @param {module:core/editor/editor~Editor} editor The editor instance.
|
425
|
-
* @returns {Promise.<*>}
|
426
|
-
*/
|
427
|
-
|
428
|
-
/**
|
429
|
-
* The minimum amount of time that needs to pass after the last action to call the provided callback.
|
430
|
-
* By default it is 1000 ms.
|
431
|
-
*
|
432
|
-
* ClassicEditor
|
433
|
-
* .create( editorElement, {
|
434
|
-
* autosave: {
|
435
|
-
* save( editor ) {
|
436
|
-
* return saveData( editor.getData() );
|
437
|
-
* },
|
438
|
-
* waitingTime: 2000
|
439
|
-
* }
|
440
|
-
* } );
|
441
|
-
* .then( ... )
|
442
|
-
* .catch( ... );
|
443
|
-
*
|
444
|
-
* @member {Number} module:autosave/autosave~AutosaveConfig#waitingTime
|
445
|
-
*/
|
package/src/index.d.ts
ADDED
package/src/index.js
CHANGED