@ckeditor/ckeditor5-watchdog 38.2.0-alpha.0 → 39.0.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.
package/README.md CHANGED
@@ -4,7 +4,6 @@ CKEditor 5 watchdog feature
4
4
  [![npm version](https://badge.fury.io/js/%40ckeditor%2Fckeditor5-watchdog.svg)](https://www.npmjs.com/package/@ckeditor/ckeditor5-watchdog)
5
5
  [![Coverage Status](https://coveralls.io/repos/github/ckeditor/ckeditor5/badge.svg?branch=master)](https://coveralls.io/github/ckeditor/ckeditor5?branch=master)
6
6
  [![Build Status](https://travis-ci.com/ckeditor/ckeditor5.svg?branch=master)](https://app.travis-ci.com/github/ckeditor/ckeditor5)
7
- ![Dependency Status](https://img.shields.io/librariesio/release/npm/@ckeditor/ckeditor5-watchdog)
8
7
 
9
8
  This package implements the watchdog feature for CKEditor 5. It keeps a CKEditor 5 rich-text editor instance running.
10
9
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ckeditor/ckeditor5-watchdog",
3
- "version": "38.2.0-alpha.0",
3
+ "version": "39.0.0",
4
4
  "description": "A watchdog feature for CKEditor 5 editors. It keeps a CKEditor 5 editor instance running.",
5
5
  "keywords": [
6
6
  "ckeditor",
@@ -10,9 +10,8 @@
10
10
  "ckeditor5-dll"
11
11
  ],
12
12
  "main": "src/index.js",
13
- "type": "module",
14
13
  "dependencies": {
15
- "lodash-es": "^4.17.15"
14
+ "lodash-es": "4.17.21"
16
15
  },
17
16
  "engines": {
18
17
  "node": ">=16.0.0",
@@ -0,0 +1,15 @@
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 type { EditorData } from './editorwatchdog';
6
+ declare module '@ckeditor/ckeditor5-core' {
7
+ interface EditorConfig {
8
+ /**
9
+ * The temporary property that is used for passing data to the plugin which restores the editor state.
10
+ *
11
+ * @internal
12
+ */
13
+ _watchdogInitialData?: EditorData;
14
+ }
15
+ }
@@ -0,0 +1,5 @@
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
+ export {};
@@ -36,6 +36,14 @@ export default class EditorWatchdog<TEditor extends Editor = Editor> extends Wat
36
36
  * The editor source element or data.
37
37
  */
38
38
  private _elementOrData?;
39
+ /**
40
+ * Specifies whether the editor was initialized using document data (`true`) or HTML elements (`false`).
41
+ */
42
+ private _initUsingData;
43
+ /**
44
+ * The latest record of the editor editable elements. Used to restart the editor.
45
+ */
46
+ private _editables;
39
47
  /**
40
48
  * The editor configuration.
41
49
  */
@@ -109,7 +117,7 @@ export default class EditorWatchdog<TEditor extends Editor = Editor> extends Wat
109
117
  * @param config The editor configuration.
110
118
  * @param context A context for the editor.
111
119
  */
112
- create(elementOrData?: HTMLElement | string | Record<string, string>, config?: EditorConfig, context?: Context): Promise<unknown>;
120
+ create(elementOrData?: HTMLElement | string | Record<string, string> | Record<string, HTMLElement>, config?: EditorConfig, context?: Context): Promise<unknown>;
113
121
  /**
114
122
  * Destroys the watchdog and the current editor instance. It fires the callback
115
123
  * registered in {@link #setDestructor `setDestructor()`} and uses it to destroy the editor instance.
@@ -127,9 +135,13 @@ export default class EditorWatchdog<TEditor extends Editor = Editor> extends Wat
127
135
  */
128
136
  _setExcludedProperties(props: Set<unknown>): void;
129
137
  /**
130
- * Returns the editor data.
138
+ * Gets all data that is required to reinitialize editor instance.
131
139
  */
132
140
  private _getData;
141
+ /**
142
+ * For each attached model root, returns its HTML editable element (if available).
143
+ */
144
+ private _getEditables;
133
145
  /**
134
146
  * Traverses the error context and the current editor to find out whether these structures are connected
135
147
  * to each other via properties.
@@ -142,6 +154,23 @@ export default class EditorWatchdog<TEditor extends Editor = Editor> extends Wat
142
154
  */
143
155
  private _cloneEditorConfiguration;
144
156
  }
157
+ export type EditorData = {
158
+ roots: Record<string, {
159
+ content: string;
160
+ attributes: string;
161
+ isLoaded: boolean;
162
+ }>;
163
+ markers: Record<string, {
164
+ rangeJSON: {
165
+ start: any;
166
+ end: any;
167
+ };
168
+ usingOperation: boolean;
169
+ affectsData: boolean;
170
+ }>;
171
+ commentThreads: string;
172
+ suggestions: string;
173
+ };
145
174
  /**
146
175
  * Fired after the watchdog restarts the error in case of a crash.
147
176
  *
@@ -152,4 +181,4 @@ export type EditorWatchdogRestartEvent = {
152
181
  args: [];
153
182
  return: undefined;
154
183
  };
155
- export type EditorCreatorFunction<TEditor = Editor> = (elementOrData: HTMLElement | string | Record<string, string>, config: EditorConfig) => Promise<TEditor>;
184
+ export type EditorCreatorFunction<TEditor = Editor> = (elementOrData: HTMLElement | string | Record<string, string> | Record<string, HTMLElement>, config: EditorConfig) => Promise<TEditor>;
@@ -22,6 +22,14 @@ export default class EditorWatchdog extends Watchdog {
22
22
  * The current editor instance.
23
23
  */
24
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 = {};
25
33
  // this._editorClass = Editor;
26
34
  this._throttledSave = throttle(this._save.bind(this), typeof watchdogConfig.saveInterval === 'number' ? watchdogConfig.saveInterval : 5000);
27
35
  // Set default creator and destructor functions:
@@ -90,14 +98,56 @@ export default class EditorWatchdog extends Watchdog {
90
98
  console.error('An error happened during the editor destroying.', err);
91
99
  })
92
100
  .then(() => {
93
- if (typeof this._elementOrData === 'string') {
94
- return this.create(this._data, this._config, this._config.context);
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);
95
138
  }
96
139
  else {
97
- const updatedConfig = Object.assign({}, this._config, {
98
- initialData: this._data
99
- });
100
- return this.create(this._elementOrData, updatedConfig, updatedConfig.context);
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
+ }
101
151
  }
102
152
  })
103
153
  .then(() => {
@@ -116,6 +166,10 @@ export default class EditorWatchdog extends Watchdog {
116
166
  .then(() => {
117
167
  super._startErrorHandling();
118
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');
119
173
  // Clone configuration because it might be shared within multiple watchdog instances. Otherwise,
120
174
  // when an error occurs in one of these editors, the watchdog will restart all of them.
121
175
  this._config = this._cloneEditorConfiguration(config) || {};
@@ -127,6 +181,9 @@ export default class EditorWatchdog extends Watchdog {
127
181
  editor.model.document.on('change:data', this._throttledSave);
128
182
  this._lastDocumentVersion = editor.model.document.version;
129
183
  this._data = this._getData();
184
+ if (!this._initUsingData) {
185
+ this._editables = this._getEditables();
186
+ }
130
187
  this.state = 'ready';
131
188
  this._fire('stateChange');
132
189
  });
@@ -149,8 +206,7 @@ export default class EditorWatchdog extends Watchdog {
149
206
  return Promise.resolve()
150
207
  .then(() => {
151
208
  this._stopErrorHandling();
152
- // Save data if there is a remaining editor data change.
153
- this._throttledSave.flush();
209
+ this._throttledSave.cancel();
154
210
  const editor = this._editor;
155
211
  this._editor = null;
156
212
  // Remove the `change:data` listener before destroying the editor.
@@ -168,6 +224,9 @@ export default class EditorWatchdog extends Watchdog {
168
224
  const version = this._editor.model.document.version;
169
225
  try {
170
226
  this._data = this._getData();
227
+ if (!this._initUsingData) {
228
+ this._editables = this._getEditables();
229
+ }
171
230
  this._lastDocumentVersion = version;
172
231
  }
173
232
  catch (err) {
@@ -182,15 +241,59 @@ export default class EditorWatchdog extends Watchdog {
182
241
  this._excludedProps = props;
183
242
  }
184
243
  /**
185
- * Returns the editor data.
244
+ * Gets all data that is required to reinitialize editor instance.
186
245
  */
187
246
  _getData() {
188
- const data = {};
189
- for (const rootName of this._editor.model.document.getRootNames()) {
190
- data[rootName] = this._editor.data.get({ rootName });
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 }));
191
281
  }
192
282
  return data;
193
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
+ }
194
297
  /**
195
298
  * Traverses the error context and the current editor to find out whether these structures are connected
196
299
  * to each other via properties.
@@ -215,3 +318,106 @@ export default class EditorWatchdog extends Watchdog {
215
318
  });
216
319
  }
217
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
+ }
package/src/index.d.ts CHANGED
@@ -8,3 +8,4 @@
8
8
  export { default as ContextWatchdog } from './contextwatchdog';
9
9
  export { default as EditorWatchdog } from './editorwatchdog';
10
10
  export { default as Watchdog } from './watchdog';
11
+ import './augmentation';
package/src/index.js CHANGED
@@ -8,3 +8,4 @@
8
8
  export { default as ContextWatchdog } from './contextwatchdog';
9
9
  export { default as EditorWatchdog } from './editorwatchdog';
10
10
  export { default as Watchdog } from './watchdog';
11
+ import './augmentation';