@ckeditor/ckeditor5-autosave 36.0.0 → 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 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:()=>D});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 d=function(t){for(var e=t.length;e--&&u.test(t.charAt(e)););return e};var l=/^\s+/;const h=function(t){return t?t.slice(0,d(t)+1).replace(l,""):t};const v=a.Symbol;var f=Object.prototype,m=f.hasOwnProperty,g=f.toString,p=v?v.toStringTag:void 0;const _=function(t){var e=m.call(t,p),i=t[p];try{t[p]=void 0;var n=!0}catch(t){}var s=g.call(t);return n&&(e?t[p]=i:delete t[p]),s};var b=Object.prototype.toString;const y=function(t){return b.call(t)};var S=v?v.toStringTag:void 0;const j=function(t){return null==t?void 0===t?"[object Undefined]":"[object Null]":S&&S in Object(t)?_(t):y(t)};const O=function(t){return null!=t&&"object"==typeof t};const T=function(t){return"symbol"==typeof t||O(t)&&"[object Symbol]"==j(t)};var w=/^[-+]0x[0-9a-f]+$/i,P=/^0b[01]+$/i,A=/^0o[0-7]+$/i,x=parseInt;const E=function(t){if("number"==typeof t)return t;if(T(t))return NaN;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=P.test(t);return i||A.test(t)?x(t.slice(2),i?2:8):w.test(t)?NaN:+t};var N=Math.max,I=Math.min;const C=function(t,e,i){var n,o,r,a,u,d,l=0,h=!1,v=!1,f=!0;if("function"!=typeof t)throw new TypeError("Expected a function");function m(e){var i=n,s=o;return n=o=void 0,l=e,a=t.apply(s,i)}function g(t){return l=t,u=setTimeout(_,e),h?m(t):a}function p(t){var i=t-d;return void 0===d||i>=e||i<0||v&&t-l>=r}function _(){var t=c();if(p(t))return b(t);u=setTimeout(_,function(t){var i=e-(t-d);return v?I(i,r-(t-l)):i}(t))}function b(t){return u=void 0,f&&n?m(t):(n=o=void 0,a)}function y(){var t=c(),i=p(t);if(n=arguments,o=this,d=t,i){if(void 0===u)return g(d);if(v)return clearTimeout(u),u=setTimeout(_,e),m(d)}return void 0===u&&(u=setTimeout(_,e)),a}return e=E(e)||0,s(i)&&(h=!!i.leading,r=(v="maxWait"in i)?N(E(i.maxWait)||0,e):r,f="trailing"in i?!!i.trailing:f),y.cancel=function(){void 0!==u&&clearTimeout(u),l=0,n=d=o=u=void 0},y.flush=function(){return void 0===u?a:b(c())},y};class D extends t.Plugin{static get pluginName(){return"Autosave"}static get requires(){return[t.PendingActions]}constructor(i){super(i);const n=i.config.get("autosave")||{},s=n.waitingTime||1e3;this.set("state","synchronized"),this._debouncedSave=C(this._save.bind(this),s),this._lastDocumentVersion=i.model.document.version,this._savePromise=null,this._domEmitter=Object.create(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}}(0,e.mix)(D,e.ObservableMixin)})(),(window.CKEditor5=window.CKEditor5||{}).autosave=n})();
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": "36.0.0",
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": "^36.0.0",
15
+ "ckeditor5": "^37.0.0-alpha.0",
16
16
  "lodash-es": "^4.17.15"
17
17
  },
18
18
  "devDependencies": {
19
- "@ckeditor/ckeditor5-core": "^36.0.0",
20
- "@ckeditor/ckeditor5-dev-utils": "^32.0.0",
21
- "@ckeditor/ckeditor5-editor-classic": "^36.0.0",
22
- "@ckeditor/ckeditor5-paragraph": "^36.0.0",
23
- "@ckeditor/ckeditor5-source-editing": "^36.0.0",
24
- "@ckeditor/ckeditor5-theme-lark": "^36.0.0",
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 &ndash; When all changes are saved.
51
+ * * waiting &ndash; When the plugin is waiting for other changes before calling `adapter#save()` and `config.autosave.save()`.
52
+ * * saving &ndash; 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, ObservableMixin, mix } from 'ckeditor5/src/utils';
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
- * ClassicEditor
25
- * .create( document.querySelector( '#editor' ), {
26
- * plugins: [ ArticlePluginSet, Autosave ],
27
- * toolbar: [ 'heading', '|', 'bold', 'italic', 'link', 'bulletedList', 'numberedList', 'blockQuote', 'undo', 'redo' ],
28
- * image: {
29
- * toolbar: [ 'imageStyle:block', 'imageStyle:side', '|', 'toggleImageCaption', 'imageTextAlternative' ],
30
- * },
31
- * autosave: {
32
- * save( editor ) {
33
- * // The saveData() function must return a promise
34
- * // which should be resolved when the data is successfully saved.
35
- * return saveData( editor.getData() );
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
- * @inheritDoc
48
- */
49
- static get pluginName() {
50
- return 'Autosave';
51
- }
52
-
53
- /**
54
- * @inheritDoc
55
- */
56
- static get requires() {
57
- return [ PendingActions ];
58
- }
59
-
60
- /**
61
- * @inheritDoc
62
- */
63
- constructor( editor ) {
64
- super( editor );
65
-
66
- const config = editor.config.get( 'autosave' ) || {};
67
-
68
- // A minimum amount of time that needs to pass after the last action.
69
- // After that time the provided save callbacks are being called.
70
- const waitingTime = config.waitingTime || 1000;
71
-
72
- /**
73
- * The adapter is an object with a `save()` method. That method will be called whenever
74
- * the data changes. It might be called some time after the change,
75
- * since the event is throttled for performance reasons.
76
- *
77
- * @member {module:autosave/autosave~AutosaveAdapter} #adapter
78
- */
79
-
80
- /**
81
- * The state of this plugin.
82
- *
83
- * The plugin can be in the following states:
84
- *
85
- * * synchronized &ndash; When all changes are saved.
86
- * * waiting &ndash; When the plugin is waiting for other changes before calling `adapter#save()` and `config.autosave.save()`.
87
- * * saving &ndash; When the provided save method is called and the plugin waits for the response.
88
- * * error &ndash When the provided save method will throw an error. This state immediately changes to the `saving` state and
89
- * the save method will be called again in the short period of time.
90
- *
91
- * @readonly
92
- * @member {'synchronized'|'waiting'|'saving'} #state
93
- */
94
- this.set( 'state', 'synchronized' );
95
-
96
- /**
97
- * Debounced save method. The `save()` method is called the specified `waitingTime` after `debouncedSave()` is called,
98
- * unless a new action happens in the meantime.
99
- *
100
- * @private
101
- * @type {Function}
102
- */
103
- this._debouncedSave = debounce( this._save.bind( this ), waitingTime );
104
-
105
- /**
106
- * The last saved document version.
107
- *
108
- * @private
109
- * @type {Number}
110
- */
111
- this._lastDocumentVersion = editor.model.document.version;
112
-
113
- /**
114
- * Promise used for asynchronous save calls.
115
- *
116
- * Created to handle the autosave call to an external data source. It resolves when that call is finished. It is re-used if
117
- * save is called before the promise has been resolved. It is set to `null` if there is no call in progress.
118
- *
119
- * @type {Promise|null}
120
- * @private
121
- */
122
- this._savePromise = null;
123
-
124
- /**
125
- * DOM emitter.
126
- *
127
- * @private
128
- * @type {DomEmitterMixin}
129
- */
130
- this._domEmitter = Object.create( DomEmitterMixin );
131
-
132
- /**
133
- * The configuration of this plugins.
134
- *
135
- * @private
136
- * @type {Object}
137
- */
138
- this._config = config;
139
-
140
- /**
141
- * Editor's pending actions manager.
142
- *
143
- * @private
144
- * @member {module:core/pendingactions~PendingActions} #_pendingActions
145
- */
146
- this._pendingActions = editor.plugins.get( PendingActions );
147
-
148
- /**
149
- * Informs whether there should be another autosave callback performed, immediately after current autosave callback finishes.
150
- *
151
- * This is set to `true` when there is a save request while autosave callback is already being processed
152
- * and the model has changed since the last save.
153
- *
154
- * @private
155
- * @type {Boolean}
156
- */
157
- this._makeImmediateSave = false;
158
-
159
- /**
160
- * An action that will be added to the pending action manager for actions happening in that plugin.
161
- *
162
- * @private
163
- * @member {Object} #_action
164
- */
165
- }
166
-
167
- /**
168
- * @inheritDoc
169
- */
170
- init() {
171
- const editor = this.editor;
172
- const doc = editor.model.document;
173
-
174
- // Add the listener only after the editor is initialized to prevent firing save callback on data init.
175
- this.listenTo( editor, 'ready', () => {
176
- this.listenTo( doc, 'change:data', ( evt, batch ) => {
177
- if ( !this._saveCallbacks.length ) {
178
- return;
179
- }
180
-
181
- if ( !batch.isLocal ) {
182
- return;
183
- }
184
-
185
- if ( this.state === 'synchronized' ) {
186
- this.state = 'waiting';
187
- // Set pending action already when we are waiting for the autosave callback.
188
- this._setPendingAction();
189
- }
190
-
191
- if ( this.state === 'waiting' ) {
192
- this._debouncedSave();
193
- }
194
-
195
- // If the plugin is in `saving` state, it will change its state later basing on the `document.version`.
196
- // If the `document.version` will be higher than stored `#_lastDocumentVersion`, then it means, that some `change:data`
197
- // event has fired in the meantime.
198
- } );
199
- } );
200
-
201
- // Flush on the editor's destroy listener with the highest priority to ensure that
202
- // `editor.getData()` will be called before plugins are destroyed.
203
- this.listenTo( editor, 'destroy', () => this._flush(), { priority: 'highest' } );
204
-
205
- // It's not possible to easy test it because karma uses `beforeunload` event
206
- // to warn before full page reload and this event cannot be dispatched manually.
207
- /* istanbul ignore next */
208
- this._domEmitter.listenTo( window, 'beforeunload', ( evtInfo, domEvt ) => {
209
- if ( this._pendingActions.hasAny ) {
210
- domEvt.returnValue = this._pendingActions.first.message;
211
- }
212
- } );
213
- }
214
-
215
- /**
216
- * @inheritDoc
217
- */
218
- destroy() {
219
- // There's no need for canceling or flushing the throttled save, as
220
- // it's done on the editor's destroy event with the highest priority.
221
-
222
- this._domEmitter.stopListening();
223
- super.destroy();
224
- }
225
-
226
- /**
227
- * Immediately calls autosave callback. All previously queued (debounced) callbacks are cleared. If there is already an autosave
228
- * callback in progress, then the requested save will be performed immediately after the current callback finishes.
229
- *
230
- * @returns {Promise} A promise that will be resolved when the autosave callback is finished.
231
- */
232
- save() {
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
@@ -0,0 +1,8 @@
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
7
+ */
8
+ export { default as Autosave } from './autosave';
package/src/index.js CHANGED
@@ -2,9 +2,7 @@
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
8
7
  */
9
-
10
8
  export { default as Autosave } from './autosave';