@ckeditor/ckeditor5-watchdog 40.0.0 → 40.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,423 +1,423 @@
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
- import areConnectedThroughProperties from './utils/areconnectedthroughproperties';
6
- import Watchdog from './watchdog';
7
- import { throttle, cloneDeepWith, isElement } from 'lodash-es';
8
- /**
9
- * A watchdog for CKEditor 5 editors.
10
- *
11
- * See the {@glink features/watchdog Watchdog feature guide} to learn the rationale behind it and
12
- * how to use it.
13
- */
14
- export default class EditorWatchdog extends Watchdog {
15
- /**
16
- * @param Editor The editor class.
17
- * @param watchdogConfig The watchdog plugin configuration.
18
- */
19
- constructor(Editor, watchdogConfig = {}) {
20
- super(watchdogConfig);
21
- /**
22
- * The current editor instance.
23
- */
24
- this._editor = null;
25
- /**
26
- * Specifies whether the editor was initialized using document data (`true`) or HTML elements (`false`).
27
- */
28
- this._initUsingData = true;
29
- /**
30
- * The latest record of the editor editable elements. Used to restart the editor.
31
- */
32
- this._editables = {};
33
- // this._editorClass = Editor;
34
- this._throttledSave = throttle(this._save.bind(this), typeof watchdogConfig.saveInterval === 'number' ? watchdogConfig.saveInterval : 5000);
35
- // Set default creator and destructor functions:
36
- if (Editor) {
37
- this._creator = ((elementOrData, config) => Editor.create(elementOrData, config));
38
- }
39
- this._destructor = editor => editor.destroy();
40
- }
41
- /**
42
- * The current editor instance.
43
- */
44
- get editor() {
45
- return this._editor;
46
- }
47
- /**
48
- * @internal
49
- */
50
- get _item() {
51
- return this._editor;
52
- }
53
- /**
54
- * Sets the function that is responsible for the editor creation.
55
- * It expects a function that should return a promise.
56
- *
57
- * ```ts
58
- * watchdog.setCreator( ( element, config ) => ClassicEditor.create( element, config ) );
59
- * ```
60
- */
61
- setCreator(creator) {
62
- this._creator = creator;
63
- }
64
- /**
65
- * Sets the function that is responsible for the editor destruction.
66
- * Overrides the default destruction function, which destroys only the editor instance.
67
- * It expects a function that should return a promise or `undefined`.
68
- *
69
- * ```ts
70
- * watchdog.setDestructor( editor => {
71
- * // Do something before the editor is destroyed.
72
- *
73
- * return editor
74
- * .destroy()
75
- * .then( () => {
76
- * // Do something after the editor is destroyed.
77
- * } );
78
- * } );
79
- * ```
80
- */
81
- setDestructor(destructor) {
82
- this._destructor = destructor;
83
- }
84
- /**
85
- * Restarts the editor instance. This method is called whenever an editor error occurs. It fires the `restart` event and changes
86
- * the state to `initializing`.
87
- *
88
- * @fires restart
89
- */
90
- _restart() {
91
- return Promise.resolve()
92
- .then(() => {
93
- this.state = 'initializing';
94
- this._fire('stateChange');
95
- return this._destroy();
96
- })
97
- .catch(err => {
98
- console.error('An error happened during the editor destroying.', err);
99
- })
100
- .then(() => {
101
- // Pre-process some data from the original editor config.
102
- // Our goal here is to make sure that the restarted editor will be reinitialized with correct set of roots.
103
- // We are not interested in any data set in config or in `.create()` first parameter. It will be replaced anyway.
104
- // But we need to set them correctly to make sure that proper roots are created.
105
- //
106
- // Since a different set of roots will be created, `lazyRoots` and `rootsAttributes` properties must be managed too.
107
- // Keys are root names, values are ''. Used when the editor was initialized by setting the first parameter to document data.
108
- const existingRoots = {};
109
- // Keeps lazy roots. They may be different when compared to initial config if some of the roots were loaded.
110
- const lazyRoots = [];
111
- // Roots attributes from the old config. Will be referred when setting new attributes.
112
- const oldRootsAttributes = this._config.rootsAttributes || {};
113
- // New attributes to be set. Is filled only for roots that still exist in the document.
114
- const rootsAttributes = {};
115
- // Traverse through the roots saved when the editor crashed and set up the discussed values.
116
- for (const [rootName, rootData] of Object.entries(this._data.roots)) {
117
- if (rootData.isLoaded) {
118
- existingRoots[rootName] = '';
119
- rootsAttributes[rootName] = oldRootsAttributes[rootName] || {};
120
- }
121
- else {
122
- lazyRoots.push(rootName);
123
- }
124
- }
125
- const updatedConfig = {
126
- ...this._config,
127
- extraPlugins: this._config.extraPlugins || [],
128
- lazyRoots,
129
- rootsAttributes,
130
- _watchdogInitialData: this._data
131
- };
132
- // Delete `initialData` as it is not needed. Data will be set by the watchdog based on `_watchdogInitialData`.
133
- // First parameter of the editor `.create()` will be used to set up initial roots.
134
- delete updatedConfig.initialData;
135
- updatedConfig.extraPlugins.push(EditorWatchdogInitPlugin);
136
- if (this._initUsingData) {
137
- return this.create(existingRoots, updatedConfig, updatedConfig.context);
138
- }
139
- else {
140
- // Set correct editables to make sure that proper roots are created and linked with DOM elements.
141
- // No need to set initial data, as it would be discarded anyway.
142
- //
143
- // If one element was initially set in `elementOrData`, then use that original element to restart the editor.
144
- // This is for compatibility purposes with single-root editor types.
145
- if (isElement(this._elementOrData)) {
146
- return this.create(this._elementOrData, updatedConfig, updatedConfig.context);
147
- }
148
- else {
149
- return this.create(this._editables, updatedConfig, updatedConfig.context);
150
- }
151
- }
152
- })
153
- .then(() => {
154
- this._fire('restart');
155
- });
156
- }
157
- /**
158
- * Creates the editor instance and keeps it running, using the defined creator and destructor.
159
- *
160
- * @param elementOrData The editor source element or the editor data.
161
- * @param config The editor configuration.
162
- * @param context A context for the editor.
163
- */
164
- create(elementOrData = this._elementOrData, config = this._config, context) {
165
- return Promise.resolve()
166
- .then(() => {
167
- super._startErrorHandling();
168
- this._elementOrData = elementOrData;
169
- // Use document data in the first parameter of the editor `.create()` call only if it was used like this originally.
170
- // Use document data if a string or object with strings was passed.
171
- this._initUsingData = typeof elementOrData == 'string' ||
172
- (Object.keys(elementOrData).length > 0 && typeof Object.values(elementOrData)[0] == 'string');
173
- // Clone configuration because it might be shared within multiple watchdog instances. Otherwise,
174
- // when an error occurs in one of these editors, the watchdog will restart all of them.
175
- this._config = this._cloneEditorConfiguration(config) || {};
176
- this._config.context = context;
177
- return this._creator(elementOrData, this._config);
178
- })
179
- .then(editor => {
180
- this._editor = editor;
181
- editor.model.document.on('change:data', this._throttledSave);
182
- this._lastDocumentVersion = editor.model.document.version;
183
- this._data = this._getData();
184
- if (!this._initUsingData) {
185
- this._editables = this._getEditables();
186
- }
187
- this.state = 'ready';
188
- this._fire('stateChange');
189
- });
190
- }
191
- /**
192
- * Destroys the watchdog and the current editor instance. It fires the callback
193
- * registered in {@link #setDestructor `setDestructor()`} and uses it to destroy the editor instance.
194
- * It also sets the state to `destroyed`.
195
- */
196
- destroy() {
197
- return Promise.resolve()
198
- .then(() => {
199
- this.state = 'destroyed';
200
- this._fire('stateChange');
201
- super.destroy();
202
- return this._destroy();
203
- });
204
- }
205
- _destroy() {
206
- return Promise.resolve()
207
- .then(() => {
208
- this._stopErrorHandling();
209
- this._throttledSave.cancel();
210
- const editor = this._editor;
211
- this._editor = null;
212
- // Remove the `change:data` listener before destroying the editor.
213
- // Incorrectly written plugins may trigger firing `change:data` events during the editor destruction phase
214
- // causing the watchdog to call `editor.getData()` when some parts of editor are already destroyed.
215
- editor.model.document.off('change:data', this._throttledSave);
216
- return this._destructor(editor);
217
- });
218
- }
219
- /**
220
- * Saves the editor data, so it can be restored after the crash even if the data cannot be fetched at
221
- * the moment of the crash.
222
- */
223
- _save() {
224
- const version = this._editor.model.document.version;
225
- try {
226
- this._data = this._getData();
227
- if (!this._initUsingData) {
228
- this._editables = this._getEditables();
229
- }
230
- this._lastDocumentVersion = version;
231
- }
232
- catch (err) {
233
- console.error(err, 'An error happened during restoring editor data. ' +
234
- 'Editor will be restored from the previously saved data.');
235
- }
236
- }
237
- /**
238
- * @internal
239
- */
240
- _setExcludedProperties(props) {
241
- this._excludedProps = props;
242
- }
243
- /**
244
- * Gets all data that is required to reinitialize editor instance.
245
- */
246
- _getData() {
247
- const editor = this._editor;
248
- const roots = editor.model.document.roots.filter(root => root.isAttached() && root.rootName != '$graveyard');
249
- const { plugins } = editor;
250
- // `as any` to avoid linking from external private repo.
251
- const commentsRepository = plugins.has('CommentsRepository') && plugins.get('CommentsRepository');
252
- const trackChanges = plugins.has('TrackChanges') && plugins.get('TrackChanges');
253
- const data = {
254
- roots: {},
255
- markers: {},
256
- commentThreads: JSON.stringify([]),
257
- suggestions: JSON.stringify([])
258
- };
259
- roots.forEach(root => {
260
- data.roots[root.rootName] = {
261
- content: JSON.stringify(Array.from(root.getChildren())),
262
- attributes: JSON.stringify(Array.from(root.getAttributes())),
263
- isLoaded: root._isLoaded
264
- };
265
- });
266
- for (const marker of editor.model.markers) {
267
- if (!marker._affectsData) {
268
- continue;
269
- }
270
- data.markers[marker.name] = {
271
- rangeJSON: marker.getRange().toJSON(),
272
- usingOperation: marker._managedUsingOperations,
273
- affectsData: marker._affectsData
274
- };
275
- }
276
- if (commentsRepository) {
277
- data.commentThreads = JSON.stringify(commentsRepository.getCommentThreads({ toJSON: true, skipNotAttached: true }));
278
- }
279
- if (trackChanges) {
280
- data.suggestions = JSON.stringify(trackChanges.getSuggestions({ toJSON: true, skipNotAttached: true }));
281
- }
282
- return data;
283
- }
284
- /**
285
- * For each attached model root, returns its HTML editable element (if available).
286
- */
287
- _getEditables() {
288
- const editables = {};
289
- for (const rootName of this.editor.model.document.getRootNames()) {
290
- const editable = this.editor.ui.getEditableElement(rootName);
291
- if (editable) {
292
- editables[rootName] = editable;
293
- }
294
- }
295
- return editables;
296
- }
297
- /**
298
- * Traverses the error context and the current editor to find out whether these structures are connected
299
- * to each other via properties.
300
- *
301
- * @internal
302
- */
303
- _isErrorComingFromThisItem(error) {
304
- return areConnectedThroughProperties(this._editor, error.context, this._excludedProps);
305
- }
306
- /**
307
- * Clones the editor configuration.
308
- */
309
- _cloneEditorConfiguration(config) {
310
- return cloneDeepWith(config, (value, key) => {
311
- // Leave DOM references.
312
- if (isElement(value)) {
313
- return value;
314
- }
315
- if (key === 'context') {
316
- return value;
317
- }
318
- });
319
- }
320
- }
321
- /**
322
- * Internal plugin that is used to stop the default editor initialization and restoring the editor state
323
- * based on the `editor.config._watchdogInitialData` data.
324
- */
325
- class EditorWatchdogInitPlugin {
326
- constructor(editor) {
327
- this.editor = editor;
328
- this._data = editor.config.get('_watchdogInitialData');
329
- }
330
- /**
331
- * @inheritDoc
332
- */
333
- init() {
334
- // Stops the default editor initialization and use the saved data to restore the editor state.
335
- // Some of data could not be initialize as a config properties. It is important to keep the data
336
- // in the same form as it was before the restarting.
337
- this.editor.data.on('init', evt => {
338
- evt.stop();
339
- this.editor.model.enqueueChange({ isUndoable: false }, writer => {
340
- this._restoreCollaborationData();
341
- this._restoreEditorData(writer);
342
- });
343
- this.editor.data.fire('ready');
344
- // Keep priority `'high' - 1` to be sure that RTC initialization will be first.
345
- }, { priority: 1000 - 1 });
346
- }
347
- /**
348
- * Creates a model node (element or text) based on provided JSON.
349
- */
350
- _createNode(writer, jsonNode) {
351
- if ('name' in jsonNode) {
352
- // If child has name property, it is an Element.
353
- const element = writer.createElement(jsonNode.name, jsonNode.attributes);
354
- if (jsonNode.children) {
355
- for (const child of jsonNode.children) {
356
- element._appendChild(this._createNode(writer, child));
357
- }
358
- }
359
- return element;
360
- }
361
- else {
362
- // Otherwise, it is a Text node.
363
- return writer.createText(jsonNode.data, jsonNode.attributes);
364
- }
365
- }
366
- /**
367
- * Restores the editor by setting the document data, roots attributes and markers.
368
- */
369
- _restoreEditorData(writer) {
370
- const editor = this.editor;
371
- Object.entries(this._data.roots).forEach(([rootName, { content, attributes }]) => {
372
- const parsedNodes = JSON.parse(content);
373
- const parsedAttributes = JSON.parse(attributes);
374
- const rootElement = editor.model.document.getRoot(rootName);
375
- for (const [key, value] of parsedAttributes) {
376
- writer.setAttribute(key, value, rootElement);
377
- }
378
- for (const child of parsedNodes) {
379
- const node = this._createNode(writer, child);
380
- writer.insert(node, rootElement, 'end');
381
- }
382
- });
383
- Object.entries(this._data.markers).forEach(([markerName, markerOptions]) => {
384
- const { document } = editor.model;
385
- const { rangeJSON: { start, end }, ...options } = markerOptions;
386
- const root = document.getRoot(start.root);
387
- const startPosition = writer.createPositionFromPath(root, start.path, start.stickiness);
388
- const endPosition = writer.createPositionFromPath(root, end.path, end.stickiness);
389
- const range = writer.createRange(startPosition, endPosition);
390
- writer.addMarker(markerName, {
391
- range,
392
- ...options
393
- });
394
- });
395
- }
396
- /**
397
- * Restores the editor collaboration data - comment threads and suggestions.
398
- */
399
- _restoreCollaborationData() {
400
- // `as any` to avoid linking from external private repo.
401
- const parsedCommentThreads = JSON.parse(this._data.commentThreads);
402
- const parsedSuggestions = JSON.parse(this._data.suggestions);
403
- parsedCommentThreads.forEach(commentThreadData => {
404
- const channelId = this.editor.config.get('collaboration.channelId');
405
- const commentsRepository = this.editor.plugins.get('CommentsRepository');
406
- if (commentsRepository.hasCommentThread(commentThreadData.threadId)) {
407
- const commentThread = commentsRepository.getCommentThread(commentThreadData.threadId);
408
- commentThread.remove();
409
- }
410
- commentsRepository.addCommentThread({ channelId, ...commentThreadData });
411
- });
412
- parsedSuggestions.forEach(suggestionData => {
413
- const trackChangesEditing = this.editor.plugins.get('TrackChangesEditing');
414
- if (trackChangesEditing.hasSuggestion(suggestionData.id)) {
415
- const suggestion = trackChangesEditing.getSuggestion(suggestionData.id);
416
- suggestion.attributes = suggestionData.attributes;
417
- }
418
- else {
419
- trackChangesEditing.addSuggestionData(suggestionData);
420
- }
421
- });
422
- }
423
- }
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
+ import areConnectedThroughProperties from './utils/areconnectedthroughproperties';
6
+ import Watchdog from './watchdog';
7
+ import { throttle, cloneDeepWith, isElement } from 'lodash-es';
8
+ /**
9
+ * A watchdog for CKEditor 5 editors.
10
+ *
11
+ * See the {@glink features/watchdog Watchdog feature guide} to learn the rationale behind it and
12
+ * how to use it.
13
+ */
14
+ export default class EditorWatchdog extends Watchdog {
15
+ /**
16
+ * @param Editor The editor class.
17
+ * @param watchdogConfig The watchdog plugin configuration.
18
+ */
19
+ constructor(Editor, watchdogConfig = {}) {
20
+ super(watchdogConfig);
21
+ /**
22
+ * The current editor instance.
23
+ */
24
+ this._editor = null;
25
+ /**
26
+ * Specifies whether the editor was initialized using document data (`true`) or HTML elements (`false`).
27
+ */
28
+ this._initUsingData = true;
29
+ /**
30
+ * The latest record of the editor editable elements. Used to restart the editor.
31
+ */
32
+ this._editables = {};
33
+ // this._editorClass = Editor;
34
+ this._throttledSave = throttle(this._save.bind(this), typeof watchdogConfig.saveInterval === 'number' ? watchdogConfig.saveInterval : 5000);
35
+ // Set default creator and destructor functions:
36
+ if (Editor) {
37
+ this._creator = ((elementOrData, config) => Editor.create(elementOrData, config));
38
+ }
39
+ this._destructor = editor => editor.destroy();
40
+ }
41
+ /**
42
+ * The current editor instance.
43
+ */
44
+ get editor() {
45
+ return this._editor;
46
+ }
47
+ /**
48
+ * @internal
49
+ */
50
+ get _item() {
51
+ return this._editor;
52
+ }
53
+ /**
54
+ * Sets the function that is responsible for the editor creation.
55
+ * It expects a function that should return a promise.
56
+ *
57
+ * ```ts
58
+ * watchdog.setCreator( ( element, config ) => ClassicEditor.create( element, config ) );
59
+ * ```
60
+ */
61
+ setCreator(creator) {
62
+ this._creator = creator;
63
+ }
64
+ /**
65
+ * Sets the function that is responsible for the editor destruction.
66
+ * Overrides the default destruction function, which destroys only the editor instance.
67
+ * It expects a function that should return a promise or `undefined`.
68
+ *
69
+ * ```ts
70
+ * watchdog.setDestructor( editor => {
71
+ * // Do something before the editor is destroyed.
72
+ *
73
+ * return editor
74
+ * .destroy()
75
+ * .then( () => {
76
+ * // Do something after the editor is destroyed.
77
+ * } );
78
+ * } );
79
+ * ```
80
+ */
81
+ setDestructor(destructor) {
82
+ this._destructor = destructor;
83
+ }
84
+ /**
85
+ * Restarts the editor instance. This method is called whenever an editor error occurs. It fires the `restart` event and changes
86
+ * the state to `initializing`.
87
+ *
88
+ * @fires restart
89
+ */
90
+ _restart() {
91
+ return Promise.resolve()
92
+ .then(() => {
93
+ this.state = 'initializing';
94
+ this._fire('stateChange');
95
+ return this._destroy();
96
+ })
97
+ .catch(err => {
98
+ console.error('An error happened during the editor destroying.', err);
99
+ })
100
+ .then(() => {
101
+ // Pre-process some data from the original editor config.
102
+ // Our goal here is to make sure that the restarted editor will be reinitialized with correct set of roots.
103
+ // We are not interested in any data set in config or in `.create()` first parameter. It will be replaced anyway.
104
+ // But we need to set them correctly to make sure that proper roots are created.
105
+ //
106
+ // Since a different set of roots will be created, `lazyRoots` and `rootsAttributes` properties must be managed too.
107
+ // Keys are root names, values are ''. Used when the editor was initialized by setting the first parameter to document data.
108
+ const existingRoots = {};
109
+ // Keeps lazy roots. They may be different when compared to initial config if some of the roots were loaded.
110
+ const lazyRoots = [];
111
+ // Roots attributes from the old config. Will be referred when setting new attributes.
112
+ const oldRootsAttributes = this._config.rootsAttributes || {};
113
+ // New attributes to be set. Is filled only for roots that still exist in the document.
114
+ const rootsAttributes = {};
115
+ // Traverse through the roots saved when the editor crashed and set up the discussed values.
116
+ for (const [rootName, rootData] of Object.entries(this._data.roots)) {
117
+ if (rootData.isLoaded) {
118
+ existingRoots[rootName] = '';
119
+ rootsAttributes[rootName] = oldRootsAttributes[rootName] || {};
120
+ }
121
+ else {
122
+ lazyRoots.push(rootName);
123
+ }
124
+ }
125
+ const updatedConfig = {
126
+ ...this._config,
127
+ extraPlugins: this._config.extraPlugins || [],
128
+ lazyRoots,
129
+ rootsAttributes,
130
+ _watchdogInitialData: this._data
131
+ };
132
+ // Delete `initialData` as it is not needed. Data will be set by the watchdog based on `_watchdogInitialData`.
133
+ // First parameter of the editor `.create()` will be used to set up initial roots.
134
+ delete updatedConfig.initialData;
135
+ updatedConfig.extraPlugins.push(EditorWatchdogInitPlugin);
136
+ if (this._initUsingData) {
137
+ return this.create(existingRoots, updatedConfig, updatedConfig.context);
138
+ }
139
+ else {
140
+ // Set correct editables to make sure that proper roots are created and linked with DOM elements.
141
+ // No need to set initial data, as it would be discarded anyway.
142
+ //
143
+ // If one element was initially set in `elementOrData`, then use that original element to restart the editor.
144
+ // This is for compatibility purposes with single-root editor types.
145
+ if (isElement(this._elementOrData)) {
146
+ return this.create(this._elementOrData, updatedConfig, updatedConfig.context);
147
+ }
148
+ else {
149
+ return this.create(this._editables, updatedConfig, updatedConfig.context);
150
+ }
151
+ }
152
+ })
153
+ .then(() => {
154
+ this._fire('restart');
155
+ });
156
+ }
157
+ /**
158
+ * Creates the editor instance and keeps it running, using the defined creator and destructor.
159
+ *
160
+ * @param elementOrData The editor source element or the editor data.
161
+ * @param config The editor configuration.
162
+ * @param context A context for the editor.
163
+ */
164
+ create(elementOrData = this._elementOrData, config = this._config, context) {
165
+ return Promise.resolve()
166
+ .then(() => {
167
+ super._startErrorHandling();
168
+ this._elementOrData = elementOrData;
169
+ // Use document data in the first parameter of the editor `.create()` call only if it was used like this originally.
170
+ // Use document data if a string or object with strings was passed.
171
+ this._initUsingData = typeof elementOrData == 'string' ||
172
+ (Object.keys(elementOrData).length > 0 && typeof Object.values(elementOrData)[0] == 'string');
173
+ // Clone configuration because it might be shared within multiple watchdog instances. Otherwise,
174
+ // when an error occurs in one of these editors, the watchdog will restart all of them.
175
+ this._config = this._cloneEditorConfiguration(config) || {};
176
+ this._config.context = context;
177
+ return this._creator(elementOrData, this._config);
178
+ })
179
+ .then(editor => {
180
+ this._editor = editor;
181
+ editor.model.document.on('change:data', this._throttledSave);
182
+ this._lastDocumentVersion = editor.model.document.version;
183
+ this._data = this._getData();
184
+ if (!this._initUsingData) {
185
+ this._editables = this._getEditables();
186
+ }
187
+ this.state = 'ready';
188
+ this._fire('stateChange');
189
+ });
190
+ }
191
+ /**
192
+ * Destroys the watchdog and the current editor instance. It fires the callback
193
+ * registered in {@link #setDestructor `setDestructor()`} and uses it to destroy the editor instance.
194
+ * It also sets the state to `destroyed`.
195
+ */
196
+ destroy() {
197
+ return Promise.resolve()
198
+ .then(() => {
199
+ this.state = 'destroyed';
200
+ this._fire('stateChange');
201
+ super.destroy();
202
+ return this._destroy();
203
+ });
204
+ }
205
+ _destroy() {
206
+ return Promise.resolve()
207
+ .then(() => {
208
+ this._stopErrorHandling();
209
+ this._throttledSave.cancel();
210
+ const editor = this._editor;
211
+ this._editor = null;
212
+ // Remove the `change:data` listener before destroying the editor.
213
+ // Incorrectly written plugins may trigger firing `change:data` events during the editor destruction phase
214
+ // causing the watchdog to call `editor.getData()` when some parts of editor are already destroyed.
215
+ editor.model.document.off('change:data', this._throttledSave);
216
+ return this._destructor(editor);
217
+ });
218
+ }
219
+ /**
220
+ * Saves the editor data, so it can be restored after the crash even if the data cannot be fetched at
221
+ * the moment of the crash.
222
+ */
223
+ _save() {
224
+ const version = this._editor.model.document.version;
225
+ try {
226
+ this._data = this._getData();
227
+ if (!this._initUsingData) {
228
+ this._editables = this._getEditables();
229
+ }
230
+ this._lastDocumentVersion = version;
231
+ }
232
+ catch (err) {
233
+ console.error(err, 'An error happened during restoring editor data. ' +
234
+ 'Editor will be restored from the previously saved data.');
235
+ }
236
+ }
237
+ /**
238
+ * @internal
239
+ */
240
+ _setExcludedProperties(props) {
241
+ this._excludedProps = props;
242
+ }
243
+ /**
244
+ * Gets all data that is required to reinitialize editor instance.
245
+ */
246
+ _getData() {
247
+ const editor = this._editor;
248
+ const roots = editor.model.document.roots.filter(root => root.isAttached() && root.rootName != '$graveyard');
249
+ const { plugins } = editor;
250
+ // `as any` to avoid linking from external private repo.
251
+ const commentsRepository = plugins.has('CommentsRepository') && plugins.get('CommentsRepository');
252
+ const trackChanges = plugins.has('TrackChanges') && plugins.get('TrackChanges');
253
+ const data = {
254
+ roots: {},
255
+ markers: {},
256
+ commentThreads: JSON.stringify([]),
257
+ suggestions: JSON.stringify([])
258
+ };
259
+ roots.forEach(root => {
260
+ data.roots[root.rootName] = {
261
+ content: JSON.stringify(Array.from(root.getChildren())),
262
+ attributes: JSON.stringify(Array.from(root.getAttributes())),
263
+ isLoaded: root._isLoaded
264
+ };
265
+ });
266
+ for (const marker of editor.model.markers) {
267
+ if (!marker._affectsData) {
268
+ continue;
269
+ }
270
+ data.markers[marker.name] = {
271
+ rangeJSON: marker.getRange().toJSON(),
272
+ usingOperation: marker._managedUsingOperations,
273
+ affectsData: marker._affectsData
274
+ };
275
+ }
276
+ if (commentsRepository) {
277
+ data.commentThreads = JSON.stringify(commentsRepository.getCommentThreads({ toJSON: true, skipNotAttached: true }));
278
+ }
279
+ if (trackChanges) {
280
+ data.suggestions = JSON.stringify(trackChanges.getSuggestions({ toJSON: true, skipNotAttached: true }));
281
+ }
282
+ return data;
283
+ }
284
+ /**
285
+ * For each attached model root, returns its HTML editable element (if available).
286
+ */
287
+ _getEditables() {
288
+ const editables = {};
289
+ for (const rootName of this.editor.model.document.getRootNames()) {
290
+ const editable = this.editor.ui.getEditableElement(rootName);
291
+ if (editable) {
292
+ editables[rootName] = editable;
293
+ }
294
+ }
295
+ return editables;
296
+ }
297
+ /**
298
+ * Traverses the error context and the current editor to find out whether these structures are connected
299
+ * to each other via properties.
300
+ *
301
+ * @internal
302
+ */
303
+ _isErrorComingFromThisItem(error) {
304
+ return areConnectedThroughProperties(this._editor, error.context, this._excludedProps);
305
+ }
306
+ /**
307
+ * Clones the editor configuration.
308
+ */
309
+ _cloneEditorConfiguration(config) {
310
+ return cloneDeepWith(config, (value, key) => {
311
+ // Leave DOM references.
312
+ if (isElement(value)) {
313
+ return value;
314
+ }
315
+ if (key === 'context') {
316
+ return value;
317
+ }
318
+ });
319
+ }
320
+ }
321
+ /**
322
+ * Internal plugin that is used to stop the default editor initialization and restoring the editor state
323
+ * based on the `editor.config._watchdogInitialData` data.
324
+ */
325
+ class EditorWatchdogInitPlugin {
326
+ constructor(editor) {
327
+ this.editor = editor;
328
+ this._data = editor.config.get('_watchdogInitialData');
329
+ }
330
+ /**
331
+ * @inheritDoc
332
+ */
333
+ init() {
334
+ // Stops the default editor initialization and use the saved data to restore the editor state.
335
+ // Some of data could not be initialize as a config properties. It is important to keep the data
336
+ // in the same form as it was before the restarting.
337
+ this.editor.data.on('init', evt => {
338
+ evt.stop();
339
+ this.editor.model.enqueueChange({ isUndoable: false }, writer => {
340
+ this._restoreCollaborationData();
341
+ this._restoreEditorData(writer);
342
+ });
343
+ this.editor.data.fire('ready');
344
+ // Keep priority `'high' - 1` to be sure that RTC initialization will be first.
345
+ }, { priority: 1000 - 1 });
346
+ }
347
+ /**
348
+ * Creates a model node (element or text) based on provided JSON.
349
+ */
350
+ _createNode(writer, jsonNode) {
351
+ if ('name' in jsonNode) {
352
+ // If child has name property, it is an Element.
353
+ const element = writer.createElement(jsonNode.name, jsonNode.attributes);
354
+ if (jsonNode.children) {
355
+ for (const child of jsonNode.children) {
356
+ element._appendChild(this._createNode(writer, child));
357
+ }
358
+ }
359
+ return element;
360
+ }
361
+ else {
362
+ // Otherwise, it is a Text node.
363
+ return writer.createText(jsonNode.data, jsonNode.attributes);
364
+ }
365
+ }
366
+ /**
367
+ * Restores the editor by setting the document data, roots attributes and markers.
368
+ */
369
+ _restoreEditorData(writer) {
370
+ const editor = this.editor;
371
+ Object.entries(this._data.roots).forEach(([rootName, { content, attributes }]) => {
372
+ const parsedNodes = JSON.parse(content);
373
+ const parsedAttributes = JSON.parse(attributes);
374
+ const rootElement = editor.model.document.getRoot(rootName);
375
+ for (const [key, value] of parsedAttributes) {
376
+ writer.setAttribute(key, value, rootElement);
377
+ }
378
+ for (const child of parsedNodes) {
379
+ const node = this._createNode(writer, child);
380
+ writer.insert(node, rootElement, 'end');
381
+ }
382
+ });
383
+ Object.entries(this._data.markers).forEach(([markerName, markerOptions]) => {
384
+ const { document } = editor.model;
385
+ const { rangeJSON: { start, end }, ...options } = markerOptions;
386
+ const root = document.getRoot(start.root);
387
+ const startPosition = writer.createPositionFromPath(root, start.path, start.stickiness);
388
+ const endPosition = writer.createPositionFromPath(root, end.path, end.stickiness);
389
+ const range = writer.createRange(startPosition, endPosition);
390
+ writer.addMarker(markerName, {
391
+ range,
392
+ ...options
393
+ });
394
+ });
395
+ }
396
+ /**
397
+ * Restores the editor collaboration data - comment threads and suggestions.
398
+ */
399
+ _restoreCollaborationData() {
400
+ // `as any` to avoid linking from external private repo.
401
+ const parsedCommentThreads = JSON.parse(this._data.commentThreads);
402
+ const parsedSuggestions = JSON.parse(this._data.suggestions);
403
+ parsedCommentThreads.forEach(commentThreadData => {
404
+ const channelId = this.editor.config.get('collaboration.channelId');
405
+ const commentsRepository = this.editor.plugins.get('CommentsRepository');
406
+ if (commentsRepository.hasCommentThread(commentThreadData.threadId)) {
407
+ const commentThread = commentsRepository.getCommentThread(commentThreadData.threadId);
408
+ commentThread.remove();
409
+ }
410
+ commentsRepository.addCommentThread({ channelId, ...commentThreadData });
411
+ });
412
+ parsedSuggestions.forEach(suggestionData => {
413
+ const trackChangesEditing = this.editor.plugins.get('TrackChangesEditing');
414
+ if (trackChangesEditing.hasSuggestion(suggestionData.id)) {
415
+ const suggestion = trackChangesEditing.getSuggestion(suggestionData.id);
416
+ suggestion.attributes = suggestionData.attributes;
417
+ }
418
+ else {
419
+ trackChangesEditing.addSuggestionData(suggestionData);
420
+ }
421
+ });
422
+ }
423
+ }