@ckeditor/ckeditor5-watchdog 48.2.0 → 48.3.0-alpha.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/dist/index.js CHANGED
@@ -2,1899 +2,1683 @@
2
2
  * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
3
3
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
4
4
  */
5
- import { isElement as isElement$2, throttle, cloneDeepWith, isPlainObject as isPlainObject$1 } from 'es-toolkit/compat';
5
+ import { cloneDeepWith, isElement, isPlainObject, throttle } from "es-toolkit/compat";
6
6
 
7
7
  /**
8
- * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
9
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
10
- */ /**
11
- * @module watchdog/watchdog
12
- */ /**
13
- * An abstract watchdog class that handles most of the error handling process and the state of the underlying component.
14
- *
15
- * See the {@glink features/watchdog Watchdog feature guide} to learn the rationale behind it and how to use it.
16
- *
17
- * @internal
18
- */ class Watchdog {
19
- /**
20
- * An array of crashes saved as an object with the following properties:
21
- *
22
- * * `message`: `String`,
23
- * * `stack`: `String`,
24
- * * `date`: `Number`,
25
- * * `filename`: `String | undefined`,
26
- * * `lineno`: `Number | undefined`,
27
- * * `colno`: `Number | undefined`,
28
- */ crashes = [];
29
- /**
30
- * Specifies the state of the item watched by the watchdog. The state can be one of the following values:
31
- *
32
- * * `initializing` – Before the first initialization, and after crashes, before the item is ready.
33
- * * `ready` – A state when the user can interact with the item.
34
- * * `crashed` – A state when an error occurs. It quickly changes to `initializing` or `crashedPermanently`
35
- * depending on how many and how frequent errors have been caught recently.
36
- * * `crashedPermanently` – A state when the watchdog stops reacting to errors and keeps the item it is watching crashed,
37
- * * `destroyed` – A state when the item is manually destroyed by the user after calling `watchdog.destroy()`.
38
- */ state = 'initializing';
39
- /**
40
- * @see module:watchdog/watchdog~WatchdogConfig
41
- */ _crashNumberLimit;
42
- /**
43
- * Returns the result of the `Date.now()` call. It can be overridden in tests to mock time as some popular
44
- * approaches like `sinon.useFakeTimers()` do not work well with error handling.
45
- */ _now = Date.now;
46
- /**
47
- * @see module:watchdog/watchdog~WatchdogConfig
48
- */ _minimumNonErrorTimePeriod;
49
- /**
50
- * Checks if the event error comes from the underlying item and restarts the item.
51
- */ _boundErrorHandler;
52
- /**
53
- * A dictionary of event emitter listeners.
54
- */ _listeners;
55
- /**
56
- * @param {module:watchdog/watchdog~WatchdogConfig} config The watchdog plugin configuration.
57
- */ constructor(config){
58
- this.crashes = [];
59
- this._crashNumberLimit = typeof config.crashNumberLimit === 'number' ? config.crashNumberLimit : 3;
60
- this._minimumNonErrorTimePeriod = typeof config.minimumNonErrorTimePeriod === 'number' ? config.minimumNonErrorTimePeriod : 5000;
61
- this._boundErrorHandler = (evt)=>{
62
- // `evt.error` is exposed by EventError while `evt.reason` is available in PromiseRejectionEvent.
63
- const error = 'error' in evt ? evt.error : evt.reason;
64
- // Note that `evt.reason` might be everything that is in the promise rejection.
65
- // Similarly everything that is thrown lands in `evt.error`.
66
- if (error instanceof Error) {
67
- this._handleError(error, evt);
68
- }
69
- };
70
- this._listeners = {};
71
- if (!this._restart) {
72
- throw new Error('The Watchdog class was split into the abstract `Watchdog` class and the `EditorWatchdog` class. ' + 'Please, use `EditorWatchdog` if you have used the `Watchdog` class previously.');
73
- }
74
- }
75
- /**
76
- * Destroys the watchdog and releases the resources.
77
- */ destroy() {
78
- this._stopErrorHandling();
79
- this._listeners = {};
80
- }
81
- /**
82
- * Starts listening to a specific event name by registering a callback that will be executed
83
- * whenever an event with a given name fires.
84
- *
85
- * Note that this method differs from the CKEditor 5's default `EventEmitterMixin` implementation.
86
- *
87
- * @param eventName The event name.
88
- * @param callback A callback which will be added to event listeners.
89
- */ on(eventName, callback) {
90
- if (!this._listeners[eventName]) {
91
- this._listeners[eventName] = [];
92
- }
93
- this._listeners[eventName].push(callback);
94
- }
95
- /**
96
- * Stops listening to the specified event name by removing the callback from event listeners.
97
- *
98
- * Note that this method differs from the CKEditor 5's default `EventEmitterMixin` implementation.
99
- *
100
- * @param eventName The event name.
101
- * @param callback A callback which will be removed from event listeners.
102
- */ off(eventName, callback) {
103
- this._listeners[eventName] = this._listeners[eventName].filter((cb)=>cb !== callback);
104
- }
105
- /**
106
- * Fires an event with a given event name and arguments.
107
- *
108
- * Note that this method differs from the CKEditor 5's default `EventEmitterMixin` implementation.
109
- */ _fire(eventName, ...args) {
110
- const callbacks = this._listeners[eventName] || [];
111
- for (const callback of callbacks){
112
- callback.apply(this, [
113
- null,
114
- ...args
115
- ]);
116
- }
117
- }
118
- /**
119
- * Starts error handling by attaching global error handlers.
120
- */ _startErrorHandling() {
121
- window.addEventListener('error', this._boundErrorHandler);
122
- window.addEventListener('unhandledrejection', this._boundErrorHandler);
123
- }
124
- /**
125
- * Stops error handling by detaching global error handlers.
126
- */ _stopErrorHandling() {
127
- window.removeEventListener('error', this._boundErrorHandler);
128
- window.removeEventListener('unhandledrejection', this._boundErrorHandler);
129
- }
130
- /**
131
- * Checks if an error comes from the watched item and restarts it.
132
- * It reacts to {@link module:utils/ckeditorerror~CKEditorError `CKEditorError` errors} only.
133
- *
134
- * @fires error
135
- * @param error Error.
136
- * @param evt An error event.
137
- */ _handleError(error, evt) {
138
- // @if CK_DEBUG // const err = error as CKEditorError;
139
- // @if CK_DEBUG // if ( err.is && err.is( 'CKEditorError' ) && err.context === undefined ) {
140
- // @if CK_DEBUG // console.warn( 'The error is missing its context and Watchdog cannot restart the proper item.' );
141
- // @if CK_DEBUG // }
142
- if (this._shouldReactToError(error)) {
143
- this.crashes.push({
144
- message: error.message,
145
- stack: error.stack,
146
- // `evt.filename`, `evt.lineno` and `evt.colno` are available only in ErrorEvent events
147
- filename: evt instanceof ErrorEvent ? evt.filename : undefined,
148
- lineno: evt instanceof ErrorEvent ? evt.lineno : undefined,
149
- colno: evt instanceof ErrorEvent ? evt.colno : undefined,
150
- date: this._now()
151
- });
152
- const causesRestart = this._shouldRestart();
153
- this.state = 'crashed';
154
- this._fire('stateChange');
155
- this._fire('error', {
156
- error,
157
- causesRestart
158
- });
159
- if (causesRestart) {
160
- this._restart();
161
- } else {
162
- this.state = 'crashedPermanently';
163
- this._fire('stateChange');
164
- }
165
- }
166
- }
167
- /**
168
- * Checks whether an error should be handled by the watchdog.
169
- *
170
- * @param error An error that was caught by the error handling process.
171
- */ _shouldReactToError(error) {
172
- return error.is && error.is('CKEditorError') && error.context !== undefined && // In some cases the watched item should not be restarted - e.g. during the item initialization.
173
- // That's why the `null` was introduced as a correct error context which does cause restarting.
174
- error.context !== null && // Do not react to errors if the watchdog is in states other than `ready`.
175
- this.state === 'ready' && this._isErrorComingFromThisItem(error);
176
- }
177
- /**
178
- * Checks if the watchdog should restart the underlying item.
179
- */ _shouldRestart() {
180
- if (this.crashes.length <= this._crashNumberLimit) {
181
- return true;
182
- }
183
- const lastErrorTime = this.crashes[this.crashes.length - 1].date;
184
- const firstMeaningfulErrorTime = this.crashes[this.crashes.length - 1 - this._crashNumberLimit].date;
185
- const averageNonErrorTimePeriod = (lastErrorTime - firstMeaningfulErrorTime) / this._crashNumberLimit;
186
- return averageNonErrorTimePeriod > this._minimumNonErrorTimePeriod;
187
- }
188
- }
8
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
9
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
10
+ */
11
+ /**
12
+ * An abstract watchdog class that handles most of the error handling process and the state of the underlying component.
13
+ *
14
+ * See the {@glink features/watchdog Watchdog feature guide} to learn the rationale behind it and how to use it.
15
+ *
16
+ * @internal
17
+ */
18
+ var Watchdog = class {
19
+ /**
20
+ * An array of crashes saved as an object with the following properties:
21
+ *
22
+ * * `message`: `String`,
23
+ * * `stack`: `String`,
24
+ * * `date`: `Number`,
25
+ * * `filename`: `String | undefined`,
26
+ * * `lineno`: `Number | undefined`,
27
+ * * `colno`: `Number | undefined`,
28
+ */
29
+ crashes = [];
30
+ /**
31
+ * Specifies the state of the item watched by the watchdog. The state can be one of the following values:
32
+ *
33
+ * * `initializing` &ndash; Before the first initialization, and after crashes, before the item is ready.
34
+ * * `ready` &ndash; A state when the user can interact with the item.
35
+ * * `crashed` &ndash; A state when an error occurs. It quickly changes to `initializing` or `crashedPermanently`
36
+ * depending on how many and how frequent errors have been caught recently.
37
+ * * `crashedPermanently` &ndash; A state when the watchdog stops reacting to errors and keeps the item it is watching crashed,
38
+ * * `destroyed` &ndash; A state when the item is manually destroyed by the user after calling `watchdog.destroy()`.
39
+ */
40
+ state = "initializing";
41
+ /**
42
+ * @see module:watchdog/watchdog~WatchdogConfig
43
+ */
44
+ _crashNumberLimit;
45
+ /**
46
+ * Returns the result of the `Date.now()` call. It can be overridden in tests to mock time as some popular
47
+ * approaches like `sinon.useFakeTimers()` do not work well with error handling.
48
+ */
49
+ _now = Date.now;
50
+ /**
51
+ * @see module:watchdog/watchdog~WatchdogConfig
52
+ */
53
+ _minimumNonErrorTimePeriod;
54
+ /**
55
+ * Checks if the event error comes from the underlying item and restarts the item.
56
+ */
57
+ _boundErrorHandler;
58
+ /**
59
+ * A dictionary of event emitter listeners.
60
+ */
61
+ _listeners;
62
+ /**
63
+ * @param {module:watchdog/watchdog~WatchdogConfig} config The watchdog plugin configuration.
64
+ */
65
+ constructor(config) {
66
+ this.crashes = [];
67
+ this._crashNumberLimit = typeof config.crashNumberLimit === "number" ? config.crashNumberLimit : 3;
68
+ this._minimumNonErrorTimePeriod = typeof config.minimumNonErrorTimePeriod === "number" ? config.minimumNonErrorTimePeriod : 5e3;
69
+ this._boundErrorHandler = (evt) => {
70
+ const error = "error" in evt ? evt.error : evt.reason;
71
+ if (error instanceof Error) this._handleError(error, evt);
72
+ };
73
+ this._listeners = {};
74
+ if (!this._restart) throw new Error("The Watchdog class was split into the abstract `Watchdog` class and the `EditorWatchdog` class. Please, use `EditorWatchdog` if you have used the `Watchdog` class previously.");
75
+ }
76
+ /**
77
+ * Destroys the watchdog and releases the resources.
78
+ */
79
+ destroy() {
80
+ this._stopErrorHandling();
81
+ this._listeners = {};
82
+ }
83
+ /**
84
+ * Starts listening to a specific event name by registering a callback that will be executed
85
+ * whenever an event with a given name fires.
86
+ *
87
+ * Note that this method differs from the CKEditor 5's default `EventEmitterMixin` implementation.
88
+ *
89
+ * @param eventName The event name.
90
+ * @param callback A callback which will be added to event listeners.
91
+ */
92
+ on(eventName, callback) {
93
+ if (!this._listeners[eventName]) this._listeners[eventName] = [];
94
+ this._listeners[eventName].push(callback);
95
+ }
96
+ /**
97
+ * Stops listening to the specified event name by removing the callback from event listeners.
98
+ *
99
+ * Note that this method differs from the CKEditor 5's default `EventEmitterMixin` implementation.
100
+ *
101
+ * @param eventName The event name.
102
+ * @param callback A callback which will be removed from event listeners.
103
+ */
104
+ off(eventName, callback) {
105
+ this._listeners[eventName] = this._listeners[eventName].filter((cb) => cb !== callback);
106
+ }
107
+ /**
108
+ * Fires an event with a given event name and arguments.
109
+ *
110
+ * Note that this method differs from the CKEditor 5's default `EventEmitterMixin` implementation.
111
+ */
112
+ _fire(eventName, ...args) {
113
+ const callbacks = this._listeners[eventName] || [];
114
+ for (const callback of callbacks) callback.apply(this, [null, ...args]);
115
+ }
116
+ /**
117
+ * Starts error handling by attaching global error handlers.
118
+ */
119
+ _startErrorHandling() {
120
+ window.addEventListener("error", this._boundErrorHandler);
121
+ window.addEventListener("unhandledrejection", this._boundErrorHandler);
122
+ }
123
+ /**
124
+ * Stops error handling by detaching global error handlers.
125
+ */
126
+ _stopErrorHandling() {
127
+ window.removeEventListener("error", this._boundErrorHandler);
128
+ window.removeEventListener("unhandledrejection", this._boundErrorHandler);
129
+ }
130
+ /**
131
+ * Checks if an error comes from the watched item and restarts it.
132
+ * It reacts to {@link module:utils/ckeditorerror~CKEditorError `CKEditorError` errors} only.
133
+ *
134
+ * @fires error
135
+ * @param error Error.
136
+ * @param evt An error event.
137
+ */
138
+ _handleError(error, evt) {
139
+ if (this._shouldReactToError(error)) {
140
+ this.crashes.push({
141
+ message: error.message,
142
+ stack: error.stack,
143
+ filename: evt instanceof ErrorEvent ? evt.filename : void 0,
144
+ lineno: evt instanceof ErrorEvent ? evt.lineno : void 0,
145
+ colno: evt instanceof ErrorEvent ? evt.colno : void 0,
146
+ date: this._now()
147
+ });
148
+ const causesRestart = this._shouldRestart();
149
+ this.state = "crashed";
150
+ this._fire("stateChange");
151
+ this._fire("error", {
152
+ error,
153
+ causesRestart
154
+ });
155
+ if (causesRestart) this._restart();
156
+ else {
157
+ this.state = "crashedPermanently";
158
+ this._fire("stateChange");
159
+ }
160
+ }
161
+ }
162
+ /**
163
+ * Checks whether an error should be handled by the watchdog.
164
+ *
165
+ * @param error An error that was caught by the error handling process.
166
+ */
167
+ _shouldReactToError(error) {
168
+ return error.is && error.is("CKEditorError") && error.context !== void 0 && error.context !== null && this.state === "ready" && this._isErrorComingFromThisItem(error);
169
+ }
170
+ /**
171
+ * Checks if the watchdog should restart the underlying item.
172
+ */
173
+ _shouldRestart() {
174
+ if (this.crashes.length <= this._crashNumberLimit) return true;
175
+ return (this.crashes[this.crashes.length - 1].date - this.crashes[this.crashes.length - 1 - this._crashNumberLimit].date) / this._crashNumberLimit > this._minimumNonErrorTimePeriod;
176
+ }
177
+ };
189
178
 
190
179
  /**
191
- * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
192
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
193
- */ /**
194
- * @module watchdog/utils/getsubnodes
195
- */ /**
196
- * @internal
197
- */ function getSubNodes(head, excludedProperties = new Set()) {
198
- const nodes = [
199
- head
200
- ];
201
- // @if CK_DEBUG_WATCHDOG // const prevNodeMap = new Map();
202
- // Nodes are stored to prevent infinite looping.
203
- const subNodes = new Set();
204
- let nodeIndex = 0;
205
- while(nodes.length > nodeIndex){
206
- // Incrementing the iterator is much faster than changing size of the array with Array.prototype.shift().
207
- const node = nodes[nodeIndex++];
208
- if (subNodes.has(node) || !shouldNodeBeIncluded(node) || excludedProperties.has(node)) {
209
- continue;
210
- }
211
- subNodes.add(node);
212
- // Handle arrays, maps, sets, custom collections that implements `[ Symbol.iterator ]()`, etc.
213
- if (Symbol.iterator in node) {
214
- // The custom editor iterators might cause some problems if the editor is crashed.
215
- try {
216
- for (const n of node){
217
- nodes.push(n);
218
- // @if CK_DEBUG_WATCHDOG // if ( !prevNodeMap.has( n ) ) {
219
- // @if CK_DEBUG_WATCHDOG // prevNodeMap.set( n, node );
220
- // @if CK_DEBUG_WATCHDOG // }
221
- }
222
- } catch {
223
- // Do not log errors for broken structures
224
- // since we are in the error handling process already.
225
- }
226
- } else {
227
- for(const key in node){
228
- // We share a reference via the protobuf library within the editors,
229
- // hence the shared value should be skipped. Although, it's not a perfect
230
- // solution since new places like that might occur in the future.
231
- if (key === 'defaultValue') {
232
- continue;
233
- }
234
- nodes.push(node[key]);
235
- // @if CK_DEBUG_WATCHDOG // if ( !prevNodeMap.has( node[ key ] ) ) {
236
- // @if CK_DEBUG_WATCHDOG // prevNodeMap.set( node[ key ], node );
237
- // @if CK_DEBUG_WATCHDOG // }
238
- }
239
- }
240
- }
241
- // @if CK_DEBUG_WATCHDOG // return { subNodes, prevNodeMap } as any;
242
- return subNodes;
180
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
181
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
182
+ */
183
+ /**
184
+ * @module watchdog/utils/getsubnodes
185
+ */
186
+ /**
187
+ * @internal
188
+ */
189
+ function getSubNodes(head, excludedProperties = /* @__PURE__ */ new Set()) {
190
+ const nodes = [head];
191
+ const subNodes = /* @__PURE__ */ new Set();
192
+ let nodeIndex = 0;
193
+ while (nodes.length > nodeIndex) {
194
+ const node = nodes[nodeIndex++];
195
+ if (subNodes.has(node) || !shouldNodeBeIncluded(node) || excludedProperties.has(node)) continue;
196
+ subNodes.add(node);
197
+ if (Symbol.iterator in node) try {
198
+ for (const n of node) nodes.push(n);
199
+ } catch {}
200
+ else for (const key in node) {
201
+ if (key === "defaultValue") continue;
202
+ nodes.push(node[key]);
203
+ }
204
+ }
205
+ return subNodes;
243
206
  }
244
207
  function shouldNodeBeIncluded(node) {
245
- const type = Object.prototype.toString.call(node);
246
- const typeOfNode = typeof node;
247
- return !(typeOfNode === 'number' || typeOfNode === 'boolean' || typeOfNode === 'string' || typeOfNode === 'symbol' || typeOfNode === 'function' || type === '[object Date]' || type === '[object RegExp]' || type === '[object Module]' || node === undefined || node === null || // This flag is meant to exclude singletons shared across editor instances. So when an error is thrown in one editor,
248
- // the other editors connected through the reference to the same singleton are not restarted. This is a temporary workaround
249
- // until a better solution is found.
250
- // More in https://github.com/ckeditor/ckeditor5/issues/12292.
251
- node._watchdogExcluded || // Skip native DOM objects, e.g. Window, nodes, events, etc.
252
- node instanceof EventTarget || node instanceof Event);
208
+ const type = Object.prototype.toString.call(node);
209
+ const typeOfNode = typeof node;
210
+ return !(typeOfNode === "number" || typeOfNode === "boolean" || typeOfNode === "string" || typeOfNode === "symbol" || typeOfNode === "function" || type === "[object Date]" || type === "[object RegExp]" || type === "[object Module]" || node === void 0 || node === null || node._watchdogExcluded || node instanceof EventTarget || node instanceof Event);
253
211
  }
254
212
 
255
213
  /**
256
- * Traverses both structures to find out whether there is a reference that is shared between both structures.
257
- *
258
- * @internal
259
- */ function areConnectedThroughProperties(target1, target2, excludedNodes = new Set()) {
260
- if (target1 === target2 && isObject(target1)) {
261
- return true;
262
- }
263
- // @if CK_DEBUG_WATCHDOG // return checkConnectionBetweenProps( target1, target2, excludedNodes );
264
- const subNodes1 = getSubNodes(target1, excludedNodes);
265
- const subNodes2 = getSubNodes(target2, excludedNodes);
266
- for (const node of subNodes1){
267
- if (subNodes2.has(node)) {
268
- return true;
269
- }
270
- }
271
- return false;
214
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
215
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
216
+ */
217
+ /**
218
+ * @module watchdog/utils/areconnectedthroughproperties
219
+ */
220
+ /**
221
+ * Traverses both structures to find out whether there is a reference that is shared between both structures.
222
+ *
223
+ * @internal
224
+ */
225
+ function areConnectedThroughProperties(target1, target2, excludedNodes = /* @__PURE__ */ new Set()) {
226
+ if (target1 === target2 && isObject(target1)) return true;
227
+ const subNodes1 = getSubNodes(target1, excludedNodes);
228
+ const subNodes2 = getSubNodes(target2, excludedNodes);
229
+ for (const node of subNodes1) if (subNodes2.has(node)) return true;
230
+ return false;
272
231
  }
232
+ /* v8 ignore stop -- @preserve */
273
233
  function isObject(structure) {
274
- return typeof structure === 'object' && structure !== null;
234
+ return typeof structure === "object" && structure !== null;
275
235
  }
276
236
 
277
- // Note: This file is a copy of core/editor/utils/normalizerootsconfig with some adjustments.
278
- // The main difference is that it does not use the `Config` class from `@ckeditor/ckeditor5-utils` and instead works
279
- // with a plain `EditorConfig` object. It also does not throw `CKEditorError` but a generic `Error` with a specific message.
280
- // It resolves root configuration for the editor, but does not care about real data in initialData as the watchdog does not need it.
281
- // It just needs to ensure that initialData is defined for all roots, so the editor features can work with it without additional checks.
282
- // Note that this helper modifies the provided config object.
283
237
  /**
284
- * Normalizes the editor roots configuration. It ensures that all root configurations are defined in `config.roots`
285
- * and that they have `initialData` defined.
286
- *
287
- * It normalizes a single-root configuration (where `config.root` is used) to a multi-root configuration
288
- * (where all roots are defined in `config.roots`). This is considered a standard configuration format,
289
- * so the editor features can always expect roots to be defined in `config.roots`.
290
- *
291
- * It also handles legacy configuration options, such as `config.initialData`, `config.placeholder`, and `config.label`.
292
- *
293
- * @internal
294
- */ function normalizeRootsConfig(sourceElementsOrData, config, defaultRootName) {
295
- const mainRootConfig = config.root;
296
- const rootsConfig = config.roots || Object.create(null);
297
- // Move `config.root` to `config.roots.main`.
298
- // This makes access to root configuration more consistent as all roots will be defined in `config.roots`.
299
- if (defaultRootName && !rootsConfig[defaultRootName]) {
300
- rootsConfig[defaultRootName] = mainRootConfig || Object.create(null);
301
- }
302
- const sourceElementIsPlainObject = isPlainObject(sourceElementsOrData);
303
- // Collect legacy configuration values for `initialData`, `placeholder`, and `label` from the config.
304
- const legacyInitialData = getLegacyInitialData(config, sourceElementIsPlainObject, defaultRootName);
305
- // Collect root names. This includes root names from the source element (if it's an object),
306
- // from `config.roots`, and from legacy config. This ensures that all roots are processed in the next step.
307
- const rootNames = Array.from(new Set([
308
- ...sourceElementIsPlainObject ? Object.keys(sourceElementsOrData) : [],
309
- ...Object.keys(rootsConfig),
310
- ...Object.keys(legacyInitialData)
311
- ]));
312
- // Ensure that all roots have `initialData` defined. If not, try to get it from the source element or data.
313
- for (const rootName of rootNames){
314
- const rootConfig = rootsConfig[rootName] || Object.create(null);
315
- // Watchdog does not need HTML, it uses dedicated plugin for restoring the model.
316
- // As watchdog uses only DOM element or map of DOM elements for sourceElementsOrData,
317
- // it can not use string as initial data, so it can be safely set to an empty string.
318
- rootConfig.initialData = '';
319
- // Handle legacy `config.placeholder` and `config.label` for the root.
320
- rootConfig.placeholder ??= getLegacyPlainConfigValue(config, 'placeholder', rootName);
321
- rootConfig.label ??= getLegacyPlainConfigValue(config, 'label', rootName);
322
- rootsConfig[rootName] = rootConfig;
323
- }
324
- config.roots = rootsConfig;
238
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
239
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
240
+ */
241
+ /**
242
+ * @module watchdog/utils/normalizerootsconfig
243
+ */
244
+ /**
245
+ * Normalizes the editor roots configuration. It ensures that all root configurations are defined in `config.roots`
246
+ * and that they have `initialData` defined.
247
+ *
248
+ * It normalizes a single-root configuration (where `config.root` is used) to a multi-root configuration
249
+ * (where all roots are defined in `config.roots`). This is considered a standard configuration format,
250
+ * so the editor features can always expect roots to be defined in `config.roots`.
251
+ *
252
+ * It also handles legacy configuration options, such as `config.initialData`, `config.placeholder`, and `config.label`.
253
+ *
254
+ * @internal
255
+ */
256
+ function normalizeRootsConfig(sourceElementsOrData, config, defaultRootName) {
257
+ const mainRootConfig = config.root;
258
+ const rootsConfig = config.roots || Object.create(null);
259
+ if (defaultRootName && !rootsConfig[defaultRootName]) rootsConfig[defaultRootName] = mainRootConfig || Object.create(null);
260
+ const sourceElementIsPlainObject = isPlainObject$1(sourceElementsOrData);
261
+ const legacyInitialData = getLegacyInitialData(config, sourceElementIsPlainObject, defaultRootName);
262
+ const rootNames = Array.from(new Set([
263
+ ...sourceElementIsPlainObject ? Object.keys(sourceElementsOrData) : [],
264
+ ...Object.keys(rootsConfig),
265
+ ...Object.keys(legacyInitialData)
266
+ ]));
267
+ for (const rootName of rootNames) {
268
+ const rootConfig = rootsConfig[rootName] || Object.create(null);
269
+ rootConfig.initialData = "";
270
+ rootConfig.placeholder ??= getLegacyPlainConfigValue(config, "placeholder", rootName);
271
+ rootConfig.label ??= getLegacyPlainConfigValue(config, "label", rootName);
272
+ rootsConfig[rootName] = rootConfig;
273
+ }
274
+ config.roots = rootsConfig;
325
275
  }
326
276
  /**
327
- * Type guard to check if the provided value is a plain object.
328
- */ function isPlainObject(sourceElementsOrData) {
329
- return !!sourceElementsOrData && typeof sourceElementsOrData == 'object' && !Array.isArray(sourceElementsOrData) && !isElement$1(sourceElementsOrData);
277
+ * Type guard to check if the provided value is a plain object.
278
+ */
279
+ function isPlainObject$1(sourceElementsOrData) {
280
+ return !!sourceElementsOrData && typeof sourceElementsOrData == "object" && !Array.isArray(sourceElementsOrData) && !isElement$2(sourceElementsOrData);
330
281
  }
331
282
  /**
332
- * Retrieve legacy configuration value for `initialData` from the config.
333
- * Normalize single-root config so returned value is always an object with root names as keys.
334
- */ function getLegacyInitialData(config, sourceElementIsObject, defaultRootName) {
335
- return sourceElementIsObject || !defaultRootName ? config.initialData || Object.create(null) : {
336
- [defaultRootName]: config.initialData
337
- };
283
+ * Retrieve legacy configuration value for `initialData` from the config.
284
+ * Normalize single-root config so returned value is always an object with root names as keys.
285
+ */
286
+ function getLegacyInitialData(config, sourceElementIsObject, defaultRootName) {
287
+ return sourceElementIsObject || !defaultRootName ? config.initialData || Object.create(null) : { [defaultRootName]: config.initialData };
338
288
  }
339
289
  /**
340
- * Retrieve legacy configuration value for `placeholder` or `label` from the config for a specific root.
341
- */ function getLegacyPlainConfigValue(config, key, rootName) {
342
- const legacyValue = config[key];
343
- if (legacyValue) {
344
- return typeof legacyValue == 'string' ? legacyValue : legacyValue[rootName];
345
- }
290
+ * Retrieve legacy configuration value for `placeholder` or `label` from the config for a specific root.
291
+ */
292
+ function getLegacyPlainConfigValue(config, key, rootName) {
293
+ const legacyValue = config[key];
294
+ if (legacyValue) return typeof legacyValue == "string" ? legacyValue : legacyValue[rootName];
346
295
  }
347
296
  /**
348
- * An alias for `isElement` from `es-toolkit/compat` with additional type guard.
349
- */ function isElement$1(value) {
350
- return isElement$2(value);
297
+ * An alias for `isElement` from `es-toolkit/compat` with additional type guard.
298
+ */
299
+ function isElement$2(value) {
300
+ return isElement(value);
351
301
  }
352
302
 
353
303
  /**
354
- * A watchdog for CKEditor 5 editors.
355
- *
356
- * See the {@glink features/watchdog Watchdog feature guide} to learn the rationale behind it and
357
- * how to use it.
358
- */ class EditorWatchdog extends Watchdog {
359
- /**
360
- * The current editor instance.
361
- */ _editor = null;
362
- /**
363
- * A promise associated with the life cycle of the editor (creation or destruction processes).
364
- *
365
- * It is used to prevent the initialization of the editor if the previous instance has not been destroyed yet,
366
- * and conversely, to prevent the destruction of the editor if it has not been initialized.
367
- */ _lifecyclePromise = null;
368
- /**
369
- * Throttled save method. The `save()` method is called the specified `saveInterval` after `throttledSave()` is called,
370
- * unless a new action happens in the meantime.
371
- */ _throttledSave;
372
- /**
373
- * The latest saved editor data represented as a root name -> root data object.
374
- */ _data;
375
- /**
376
- * The last document version.
377
- */ _lastDocumentVersion;
378
- /**
379
- * The editor source element or data.
380
- */ _elementOrData;
381
- /**
382
- * Stores the original DOM element for single-root editors.
383
- */ _editorAttachTo = null;
384
- /**
385
- * Specifies whether the editor is a single-root editor (e.g. ClassicEditor) or a multi-root editor (e.g. MultiRootEditor).
386
- */ _isSingleRootEditor = true;
387
- /**
388
- * Specifies whether the editor was created using config-based creator mode (without a source element or data as the first argument).
389
- *
390
- * @internal
391
- */ _isUsingConfigBasedCreator = false;
392
- /**
393
- * The latest record of the editor editable elements. Used to restart the editor.
394
- */ _editables = {};
395
- /**
396
- * The editor configuration.
397
- */ _config;
398
- _excludedProps;
399
- /**
400
- * @param Editor The editor class.
401
- * @param watchdogConfig The watchdog plugin configuration.
402
- */ constructor(Editor, watchdogConfig = {}){
403
- super(watchdogConfig);
404
- // this._editorClass = Editor;
405
- this._throttledSave = throttle(this._save.bind(this), typeof watchdogConfig.saveInterval === 'number' ? watchdogConfig.saveInterval : 5000);
406
- // Set default creator and destructor functions:
407
- if (Editor) {
408
- this._creator = (elementOrDataOrConfig, config)=>{
409
- if (config === undefined) {
410
- // Config-based mode: first argument is the config.
411
- return Editor.create(elementOrDataOrConfig);
412
- }
413
- // Legacy mode: first argument is element/data, second is config.
414
- return Editor.create(elementOrDataOrConfig, config);
415
- };
416
- }
417
- this._destructor = (editor)=>editor.destroy();
418
- }
419
- /**
420
- * The current editor instance.
421
- */ get editor() {
422
- return this._editor;
423
- }
424
- /**
425
- * @internal
426
- */ get _item() {
427
- return this._editor;
428
- }
429
- /**
430
- * Sets the function that is responsible for the editor creation.
431
- * It expects a function that should return a promise.
432
- *
433
- * For config-based editor creation:
434
- *
435
- * ```ts
436
- * watchdog.setCreator( config => ClassicEditor.create( config ) );
437
- * ```
438
- *
439
- * For legacy editor creation (with element or data as the first argument):
440
- *
441
- * ```ts
442
- * watchdog.setCreator( ( element, config ) => ClassicEditor.create( element, config ) );
443
- * ```
444
- */ setCreator(creator) {
445
- this._creator = creator;
446
- }
447
- /**
448
- * Sets the function that is responsible for the editor destruction.
449
- * Overrides the default destruction function, which destroys only the editor instance.
450
- * It expects a function that should return a promise or `undefined`.
451
- *
452
- * ```ts
453
- * watchdog.setDestructor( editor => {
454
- * // Do something before the editor is destroyed.
455
- *
456
- * return editor
457
- * .destroy()
458
- * .then( () => {
459
- * // Do something after the editor is destroyed.
460
- * } );
461
- * } );
462
- * ```
463
- */ setDestructor(destructor) {
464
- this._destructor = destructor;
465
- }
466
- /**
467
- * Restarts the editor instance. This method is called whenever an editor error occurs. It fires the `restart` event and changes
468
- * the state to `initializing`.
469
- *
470
- * @fires restart
471
- */ _restart() {
472
- return Promise.resolve().then(()=>{
473
- this.state = 'initializing';
474
- this._fire('stateChange');
475
- return this._destroy();
476
- }).catch((err)=>{
477
- console.error('An error happened during the editor destroying.', err);
478
- }).then(()=>{
479
- // Pre-process some data from the original editor config.
480
- // Our goal here is to make sure that the restarted editor will be reinitialized with correct set of roots.
481
- // We are not interested in any data set in config. It will be replaced anyway.
482
- // But we need to set them correctly to make sure that proper roots are created.
483
- //
484
- // Since a different set of roots will be created, lazy-roots and roots-attributes must be managed too.
485
- if (this._isUsingConfigBasedCreator) {
486
- // In config-based creator mode, normalize using an empty source to ensure `config.root` is moved
487
- // to `config.roots.main` and other legacy config properties are handled.
488
- normalizeRootsConfig(this._isSingleRootEditor ? '' : {}, this._config, this._isSingleRootEditor ? 'main' : false);
489
- } else {
490
- // Normalize the roots configuration based on the editor source element or data and the editor configuration.
491
- normalizeRootsConfig(this._isSingleRootEditor ? this._editorAttachTo || '' : this._editables, this._config, this._isSingleRootEditor ? 'main' : false);
492
- }
493
- const updatedConfig = {
494
- ...this._config,
495
- extraPlugins: this._config.extraPlugins || [],
496
- _watchdogInitialData: this._data
497
- };
498
- // Add content loading plugin to the editor configuration.
499
- // This plugin will be responsible for loading the editor data into the editor after it is restarted.
500
- updatedConfig.extraPlugins.push(EditorWatchdogInitPlugin);
501
- // Collect existing roots configuration and update it. This will ensure that the same set of roots
502
- // will be created after the restart, and they will have correct lazy loading and attributes configuration.
503
- const updatedRootsConfig = {};
504
- for (const [rootName, rootData] of Object.entries(this._data.roots)){
505
- const rootConfig = updatedConfig.roots[rootName] || Object.create(null);
506
- // Delete `initialData` as it is not needed. Data will be set by the watchdog based on `_watchdogInitialData`.
507
- rootConfig.initialData = '';
508
- // Copy root element name as it is created in the editor constructor.
509
- rootConfig.modelElement = rootData.modelElement;
510
- // Reuse previous DOM editable.
511
- if (this._isUsingConfigBasedCreator) {
512
- if (this._editables[rootName]?.isConnected) {
513
- rootConfig.element = this._editables[rootName];
514
- } else if (rootData.isLoaded && !rootConfig.element) {
515
- Object.assign(rootConfig, getSavedRootEditableOptions(rootData.attributes));
516
- }
517
- }
518
- if (rootData.isLoaded) {
519
- rootConfig.lazyLoad = false;
520
- } else {
521
- delete rootConfig.modelAttributes;
522
- }
523
- updatedRootsConfig[rootName] = rootConfig;
524
- }
525
- updatedConfig.roots = updatedRootsConfig;
526
- // Delete `initialData` as it is not needed. Data will be set by the watchdog based on `_watchdogInitialData`.
527
- delete updatedConfig.initialData;
528
- // Also alias for main root should not provide initial data.
529
- // In fact, it should not provide any data as it is only an alias for the root defined in `config.roots`
530
- // and it is the `config.roots` that should be used to set up the initial data for the main root.
531
- // This would cause a crash while normalizing conflict when left as is.
532
- delete updatedConfig.root;
533
- if (this._isUsingConfigBasedCreator) {
534
- return this.create(updatedConfig, updatedConfig.context);
535
- }
536
- const elementOrData = this._isSingleRootEditor ? this._editorAttachTo || '' : this._editables;
537
- return this.create(elementOrData, updatedConfig, updatedConfig.context);
538
- }).then(()=>{
539
- this._fire('restart');
540
- });
541
- }
542
- create(elementOrDataOrConfig = this._isUsingConfigBasedCreator ? this._config : this._elementOrData, configOrContext = this._isUsingConfigBasedCreator ? undefined : this._config, context) {
543
- // Detect config-based creator mode: first argument is a config object (not an element, string, or record of strings/elements).
544
- // The detection is skipped during restart (when `_elementOrData` or `_config` is already set).
545
- const isUsingConfigBasedCreator = this._detectConfigBasedCreator(elementOrDataOrConfig, configOrContext);
546
- const elementOrData = isUsingConfigBasedCreator ? undefined : elementOrDataOrConfig;
547
- const config = isUsingConfigBasedCreator ? elementOrDataOrConfig : configOrContext;
548
- const resolvedContext = isUsingConfigBasedCreator ? configOrContext : context;
549
- this._lifecyclePromise = Promise.resolve(this._lifecyclePromise).then(()=>{
550
- super._startErrorHandling();
551
- this._isUsingConfigBasedCreator = isUsingConfigBasedCreator;
552
- this._elementOrData = elementOrData;
553
- // Clone configuration because it might be shared within multiple watchdog instances. Otherwise,
554
- // when an error occurs in one of these editors, the watchdog will restart all of them.
555
- this._config = this._cloneEditorConfiguration(config || {});
556
- this._config.context = resolvedContext;
557
- // Store the original DOM element for single-root editors. We can't use editable elements as ClassicEditor
558
- // expects the attachment element.
559
- if (isUsingConfigBasedCreator) {
560
- // In config-based creator mode, element references are already in the config
561
- // (`config.attachTo` or `config.roots.*.element`), so there's no need to store them separately.
562
- this._editorAttachTo = null;
563
- // Detect single-root vs multi-root from config. The config might use `config.root` (alias),
564
- // `config.roots`, `config.attachTo`, or legacy `config.initialData`.
565
- const rootsCount = this._config.roots ? Object.keys(this._config.roots).length : 0;
566
- const legacyInitialData = this._config.initialData;
567
- const isMultiRootFromLegacy = legacyInitialData && typeof legacyInitialData === 'object';
568
- this._isSingleRootEditor = !isMultiRootFromLegacy && rootsCount <= 1;
569
- } else {
570
- this._editorAttachTo = isElement(elementOrData) ? elementOrData : null;
571
- this._isSingleRootEditor = isElement(elementOrData) || typeof elementOrData == 'string';
572
- }
573
- if (isUsingConfigBasedCreator) {
574
- return this._creator(this._config);
575
- }
576
- return this._creator(elementOrData, this._config);
577
- }).then((editor)=>{
578
- this._editor = editor;
579
- editor.model.document.on('change:data', this._throttledSave);
580
- this._lastDocumentVersion = editor.model.document.version;
581
- this._data = this._getData();
582
- if (!this._editorAttachTo) {
583
- this._editables = this._getEditables();
584
- }
585
- this.state = 'ready';
586
- this._fire('stateChange');
587
- }).finally(()=>{
588
- this._lifecyclePromise = null;
589
- });
590
- return this._lifecyclePromise;
591
- }
592
- /**
593
- * Destroys the watchdog and the current editor instance. It fires the callback
594
- * registered in {@link #setDestructor `setDestructor()`} and uses it to destroy the editor instance.
595
- * It also sets the state to `destroyed`.
596
- */ destroy() {
597
- this._lifecyclePromise = Promise.resolve(this._lifecyclePromise).then(()=>{
598
- this.state = 'destroyed';
599
- this._fire('stateChange');
600
- super.destroy();
601
- return this._destroy();
602
- }).finally(()=>{
603
- this._lifecyclePromise = null;
604
- });
605
- return this._lifecyclePromise;
606
- }
607
- _destroy() {
608
- return Promise.resolve().then(()=>{
609
- this._stopErrorHandling();
610
- this._throttledSave.cancel();
611
- const editor = this._editor;
612
- this._editor = null;
613
- // Remove the `change:data` listener before destroying the editor.
614
- // Incorrectly written plugins may trigger firing `change:data` events during the editor destruction phase
615
- // causing the watchdog to call `editor.getData()` when some parts of editor are already destroyed.
616
- editor.model.document.off('change:data', this._throttledSave);
617
- return this._destructor(editor);
618
- });
619
- }
620
- /**
621
- * Saves the editor data, so it can be restored after the crash even if the data cannot be fetched at
622
- * the moment of the crash.
623
- */ _save() {
624
- const version = this._editor.model.document.version;
625
- try {
626
- this._data = this._getData();
627
- if (!this._editorAttachTo) {
628
- this._editables = this._getEditables();
629
- }
630
- this._lastDocumentVersion = version;
631
- } catch (err) {
632
- console.error(err, 'An error happened during restoring editor data. ' + 'Editor will be restored from the previously saved data.');
633
- }
634
- }
635
- /**
636
- * @internal
637
- */ _setExcludedProperties(props) {
638
- this._excludedProps = props;
639
- }
640
- /**
641
- * Gets all data that is required to reinitialize editor instance.
642
- */ _getData() {
643
- const editor = this._editor;
644
- const roots = editor.model.document.roots.filter((root)=>root.isAttached() && root.rootName != '$graveyard');
645
- const { plugins } = editor;
646
- // `as any` to avoid linking from external private repo.
647
- const commentsRepository = plugins.has('CommentsRepository') && plugins.get('CommentsRepository');
648
- const trackChanges = plugins.has('TrackChanges') && plugins.get('TrackChanges');
649
- const data = {
650
- roots: {},
651
- markers: {},
652
- commentThreads: JSON.stringify([]),
653
- suggestions: JSON.stringify([])
654
- };
655
- roots.forEach((root)=>{
656
- data.roots[root.rootName] = {
657
- content: JSON.stringify(Array.from(root.getChildren())),
658
- attributes: JSON.stringify(Array.from(root.getAttributes())),
659
- modelElement: root.name,
660
- isLoaded: root._isLoaded
661
- };
662
- });
663
- for (const marker of editor.model.markers){
664
- if (!marker._affectsData) {
665
- continue;
666
- }
667
- data.markers[marker.name] = {
668
- rangeJSON: marker.getRange().toJSON(),
669
- usingOperation: marker._managedUsingOperations,
670
- affectsData: marker._affectsData
671
- };
672
- }
673
- if (commentsRepository) {
674
- data.commentThreads = JSON.stringify(commentsRepository.getCommentThreads({
675
- toJSON: true,
676
- skipNotAttached: true
677
- }));
678
- }
679
- if (trackChanges) {
680
- data.suggestions = JSON.stringify(trackChanges.getSuggestions({
681
- toJSON: true,
682
- skipNotAttached: true
683
- }));
684
- }
685
- return data;
686
- }
687
- /**
688
- * For each attached model root, returns its HTML editable element (if available).
689
- */ _getEditables() {
690
- const editables = {};
691
- for (const rootName of this.editor.model.document.getRootNames()){
692
- const editable = this.editor.ui.getEditableElement(rootName);
693
- if (editable) {
694
- editables[rootName] = editable;
695
- }
696
- }
697
- return editables;
698
- }
699
- /**
700
- * Traverses the error context and the current editor to find out whether these structures are connected
701
- * to each other via properties.
702
- *
703
- * @internal
704
- */ _isErrorComingFromThisItem(error) {
705
- return areConnectedThroughProperties(this._editor, error.context, this._excludedProps);
706
- }
707
- /**
708
- * Detects whether the `create()` call was made in config-based creator mode
709
- * (i.e., the first argument is a config object rather than a source element or data).
710
- */ _detectConfigBasedCreator(elementOrDataOrConfig, configOrContext) {
711
- // A string or DOM element is clearly the legacy signature.
712
- if (typeof elementOrDataOrConfig === 'string' || isElement(elementOrDataOrConfig)) {
713
- return false;
714
- }
715
- // If the second argument is a plain object with keys, it's a config → legacy signature.
716
- if (configOrContext && typeof configOrContext === 'object' && !('destroy' in configOrContext) && Object.keys(configOrContext).length > 0) {
717
- return false;
718
- }
719
- // If the first argument is an object where all values are strings or elements, it's multi-root legacy.
720
- if (elementOrDataOrConfig && typeof elementOrDataOrConfig === 'object') {
721
- const values = Object.values(elementOrDataOrConfig);
722
- if (values.length > 0 && values.every((v)=>typeof v === 'string' || isElement(v))) {
723
- return false;
724
- }
725
- }
726
- // Otherwise, it's config-based.
727
- return true;
728
- }
729
- /**
730
- * Clones the editor configuration.
731
- */ _cloneEditorConfiguration(config) {
732
- return cloneDeepWith(config, (value, key)=>{
733
- // Leave DOM references.
734
- if (isElement(value)) {
735
- return value;
736
- }
737
- if (key === 'context') {
738
- return value;
739
- }
740
- });
741
- }
742
- }
304
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
305
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
306
+ */
743
307
  /**
744
- * Internal plugin that is used to stop the default editor initialization and restoring the editor state
745
- * based on the `editor.config._watchdogInitialData` data.
746
- */ class EditorWatchdogInitPlugin {
747
- editor;
748
- _data;
749
- constructor(editor){
750
- this.editor = editor;
751
- this._data = editor.config.get('_watchdogInitialData');
752
- }
753
- /**
754
- * @inheritDoc
755
- */ init() {
756
- // Stops the default editor initialization and use the saved data to restore the editor state.
757
- // Some of data could not be initialize as a config properties. It is important to keep the data
758
- // in the same form as it was before the restarting.
759
- this.editor.data.on('init', (evt)=>{
760
- evt.stop();
761
- this.editor.model.enqueueChange({
762
- isUndoable: false
763
- }, (writer)=>{
764
- this._restoreCollaborationData();
765
- this._restoreEditorData(writer);
766
- });
767
- this.editor.data.fire('ready');
768
- // Keep priority `'high' - 1` to be sure that RTC initialization will be first.
769
- }, {
770
- priority: 1000 - 1
771
- });
772
- }
773
- /**
774
- * Creates a model node (element or text) based on provided JSON.
775
- */ _createNode(writer, jsonNode) {
776
- if ('name' in jsonNode) {
777
- // If child has name property, it is an Element.
778
- const element = writer.createElement(jsonNode.name, jsonNode.attributes);
779
- if (jsonNode.children) {
780
- for (const child of jsonNode.children){
781
- element._appendChild(this._createNode(writer, child));
782
- }
783
- }
784
- return element;
785
- } else {
786
- // Otherwise, it is a Text node.
787
- return writer.createText(jsonNode.data, jsonNode.attributes);
788
- }
789
- }
790
- /**
791
- * Restores the editor by setting the document data, roots attributes and markers.
792
- */ _restoreEditorData(writer) {
793
- const editor = this.editor;
794
- Object.entries(this._data.roots).forEach(([rootName, { content, attributes }])=>{
795
- const parsedNodes = JSON.parse(content);
796
- const parsedAttributes = JSON.parse(attributes);
797
- const rootElement = editor.model.document.getRoot(rootName);
798
- for (const [key, value] of parsedAttributes){
799
- writer.setAttribute(key, value, rootElement);
800
- }
801
- for (const child of parsedNodes){
802
- const node = this._createNode(writer, child);
803
- writer.insert(node, rootElement, 'end');
804
- }
805
- });
806
- Object.entries(this._data.markers).forEach(([markerName, markerOptions])=>{
807
- const { document } = editor.model;
808
- const { rangeJSON: { start, end }, ...options } = markerOptions;
809
- const root = document.getRoot(start.root);
810
- const startPosition = writer.createPositionFromPath(root, start.path, start.stickiness);
811
- const endPosition = writer.createPositionFromPath(root, end.path, end.stickiness);
812
- const range = writer.createRange(startPosition, endPosition);
813
- writer.addMarker(markerName, {
814
- range,
815
- ...options
816
- });
817
- });
818
- }
819
- /**
820
- * Restores the editor collaboration data - comment threads and suggestions.
821
- */ _restoreCollaborationData() {
822
- // `as any` to avoid linking from external private repo.
823
- const parsedCommentThreads = JSON.parse(this._data.commentThreads);
824
- const parsedSuggestions = JSON.parse(this._data.suggestions);
825
- if (this.editor.plugins.has('CommentsRepository')) {
826
- const commentsRepository = this.editor.plugins.get('CommentsRepository');
827
- // First, remove the existing comments that were created by integration plugins during initialization.
828
- // These comments may be outdated, and new instances will be created in the next step based on the saved data.
829
- for (const commentThread of commentsRepository.getCommentThreads()){
830
- // Use the internal API since it removes the comment thread directly and does not trigger events
831
- // that could cause side effects, such as removing markers.
832
- commentsRepository._removeCommentThread({
833
- threadId: commentThread.id
834
- });
835
- }
836
- parsedCommentThreads.forEach((commentThreadData)=>{
837
- const channelId = this.editor.config.get('collaboration.channelId');
838
- const commentsRepository = this.editor.plugins.get('CommentsRepository');
839
- commentsRepository.addCommentThread({
840
- channelId,
841
- ...commentThreadData
842
- });
843
- });
844
- }
845
- if (this.editor.plugins.has('TrackChangesEditing')) {
846
- const trackChangesEditing = this.editor.plugins.get('TrackChangesEditing');
847
- // First, remove the existing suggestions that were created by integration plugins during initialization.
848
- // These suggestions may be outdated, and new instances will be created in the next step based on the saved data.
849
- for (const suggestion of trackChangesEditing.getSuggestions()){
850
- trackChangesEditing._removeSuggestion(suggestion);
851
- }
852
- parsedSuggestions.forEach((suggestionData)=>{
853
- trackChangesEditing.addSuggestionData(suggestionData);
854
- });
855
- }
856
- }
857
- }
308
+ * @module watchdog/editorwatchdog
309
+ */
310
+ /**
311
+ * A watchdog for CKEditor 5 editors.
312
+ *
313
+ * See the {@glink features/watchdog Watchdog feature guide} to learn the rationale behind it and
314
+ * how to use it.
315
+ */
316
+ var EditorWatchdog = class extends Watchdog {
317
+ /**
318
+ * The current editor instance.
319
+ */
320
+ _editor = null;
321
+ /**
322
+ * A promise associated with the life cycle of the editor (creation or destruction processes).
323
+ *
324
+ * It is used to prevent the initialization of the editor if the previous instance has not been destroyed yet,
325
+ * and conversely, to prevent the destruction of the editor if it has not been initialized.
326
+ */
327
+ _lifecyclePromise = null;
328
+ /**
329
+ * Throttled save method. The `save()` method is called the specified `saveInterval` after `throttledSave()` is called,
330
+ * unless a new action happens in the meantime.
331
+ */
332
+ _throttledSave;
333
+ /**
334
+ * The latest saved editor data represented as a root name -> root data object.
335
+ */
336
+ _data;
337
+ /**
338
+ * The last document version.
339
+ */
340
+ _lastDocumentVersion;
341
+ /**
342
+ * The editor source element or data.
343
+ */
344
+ _elementOrData;
345
+ /**
346
+ * Stores the original DOM element for single-root editors.
347
+ */
348
+ _editorAttachTo = null;
349
+ /**
350
+ * Specifies whether the editor is a single-root editor (e.g. ClassicEditor) or a multi-root editor (e.g. MultiRootEditor).
351
+ */
352
+ _isSingleRootEditor = true;
353
+ /**
354
+ * Specifies whether the editor was created using config-based creator mode (without a source element or data as the first argument).
355
+ *
356
+ * @internal
357
+ */
358
+ _isUsingConfigBasedCreator = false;
359
+ /**
360
+ * The latest record of the editor editable elements. Used to restart the editor.
361
+ */
362
+ _editables = {};
363
+ /**
364
+ * The editor configuration.
365
+ */
366
+ _config;
367
+ _excludedProps;
368
+ /**
369
+ * @param Editor The editor class.
370
+ * @param watchdogConfig The watchdog plugin configuration.
371
+ */
372
+ constructor(Editor, watchdogConfig = {}) {
373
+ super(watchdogConfig);
374
+ this._throttledSave = throttle(this._save.bind(this), typeof watchdogConfig.saveInterval === "number" ? watchdogConfig.saveInterval : 5e3);
375
+ if (Editor) this._creator = ((elementOrDataOrConfig, config) => {
376
+ if (config === void 0) return Editor.create(elementOrDataOrConfig);
377
+ return Editor.create(elementOrDataOrConfig, config);
378
+ });
379
+ this._destructor = (editor) => editor.destroy();
380
+ }
381
+ /**
382
+ * The current editor instance.
383
+ */
384
+ get editor() {
385
+ return this._editor;
386
+ }
387
+ /**
388
+ * @internal
389
+ */
390
+ get _item() {
391
+ return this._editor;
392
+ }
393
+ /**
394
+ * Sets the function that is responsible for the editor creation.
395
+ * It expects a function that should return a promise.
396
+ *
397
+ * For config-based editor creation:
398
+ *
399
+ * ```ts
400
+ * watchdog.setCreator( config => ClassicEditor.create( config ) );
401
+ * ```
402
+ *
403
+ * For legacy editor creation (with element or data as the first argument):
404
+ *
405
+ * ```ts
406
+ * watchdog.setCreator( ( element, config ) => ClassicEditor.create( element, config ) );
407
+ * ```
408
+ */
409
+ setCreator(creator) {
410
+ this._creator = creator;
411
+ }
412
+ /**
413
+ * Sets the function that is responsible for the editor destruction.
414
+ * Overrides the default destruction function, which destroys only the editor instance.
415
+ * It expects a function that should return a promise or `undefined`.
416
+ *
417
+ * ```ts
418
+ * watchdog.setDestructor( editor => {
419
+ * // Do something before the editor is destroyed.
420
+ *
421
+ * return editor
422
+ * .destroy()
423
+ * .then( () => {
424
+ * // Do something after the editor is destroyed.
425
+ * } );
426
+ * } );
427
+ * ```
428
+ */
429
+ setDestructor(destructor) {
430
+ this._destructor = destructor;
431
+ }
432
+ /**
433
+ * Restarts the editor instance. This method is called whenever an editor error occurs. It fires the `restart` event and changes
434
+ * the state to `initializing`.
435
+ *
436
+ * @fires restart
437
+ */
438
+ _restart() {
439
+ return Promise.resolve().then(() => {
440
+ this.state = "initializing";
441
+ this._fire("stateChange");
442
+ return this._destroy();
443
+ }).catch((err) => {
444
+ console.error("An error happened during the editor destroying.", err);
445
+ }).then(() => {
446
+ if (this._isUsingConfigBasedCreator) normalizeRootsConfig(this._isSingleRootEditor ? "" : {}, this._config, this._isSingleRootEditor ? "main" : false);
447
+ else normalizeRootsConfig(this._isSingleRootEditor ? this._editorAttachTo || "" : this._editables, this._config, this._isSingleRootEditor ? "main" : false);
448
+ const updatedConfig = {
449
+ ...this._config,
450
+ extraPlugins: this._config.extraPlugins || [],
451
+ _watchdogInitialData: this._data
452
+ };
453
+ updatedConfig.extraPlugins.push(EditorWatchdogInitPlugin);
454
+ const updatedRootsConfig = {};
455
+ for (const [rootName, rootData] of Object.entries(this._data.roots)) {
456
+ const rootConfig = updatedConfig.roots[rootName] || Object.create(null);
457
+ rootConfig.initialData = "";
458
+ rootConfig.modelElement = rootData.modelElement;
459
+ if (this._isUsingConfigBasedCreator) {
460
+ if (this._editables[rootName]?.isConnected) rootConfig.element = this._editables[rootName];
461
+ else if (rootData.isLoaded && !rootConfig.element) Object.assign(rootConfig, getSavedRootEditableOptions(rootData.attributes));
462
+ }
463
+ if (rootData.isLoaded) rootConfig.lazyLoad = false;
464
+ else delete rootConfig.modelAttributes;
465
+ updatedRootsConfig[rootName] = rootConfig;
466
+ }
467
+ updatedConfig.roots = updatedRootsConfig;
468
+ delete updatedConfig.initialData;
469
+ delete updatedConfig.root;
470
+ if (this._isUsingConfigBasedCreator) return this.create(updatedConfig, updatedConfig.context);
471
+ const elementOrData = this._isSingleRootEditor ? this._editorAttachTo || "" : this._editables;
472
+ return this.create(elementOrData, updatedConfig, updatedConfig.context);
473
+ }).then(() => {
474
+ this._fire("restart");
475
+ });
476
+ }
477
+ create(elementOrDataOrConfig = this._isUsingConfigBasedCreator ? this._config : this._elementOrData, configOrContext = this._isUsingConfigBasedCreator ? void 0 : this._config, context) {
478
+ const isUsingConfigBasedCreator = this._detectConfigBasedCreator(elementOrDataOrConfig, configOrContext);
479
+ const elementOrData = isUsingConfigBasedCreator ? void 0 : elementOrDataOrConfig;
480
+ const config = isUsingConfigBasedCreator ? elementOrDataOrConfig : configOrContext;
481
+ const resolvedContext = isUsingConfigBasedCreator ? configOrContext : context;
482
+ this._lifecyclePromise = Promise.resolve(this._lifecyclePromise).then(() => {
483
+ super._startErrorHandling();
484
+ this._isUsingConfigBasedCreator = isUsingConfigBasedCreator;
485
+ this._elementOrData = elementOrData;
486
+ this._config = this._cloneEditorConfiguration(config || {});
487
+ this._config.context = resolvedContext;
488
+ if (isUsingConfigBasedCreator) {
489
+ this._editorAttachTo = null;
490
+ const rootsCount = this._config.roots ? Object.keys(this._config.roots).length : 0;
491
+ const legacyInitialData = this._config.initialData;
492
+ const isMultiRootFromLegacy = legacyInitialData && typeof legacyInitialData === "object";
493
+ this._isSingleRootEditor = !isMultiRootFromLegacy && rootsCount <= 1;
494
+ } else {
495
+ this._editorAttachTo = isElement$1(elementOrData) ? elementOrData : null;
496
+ this._isSingleRootEditor = isElement$1(elementOrData) || typeof elementOrData == "string";
497
+ }
498
+ if (isUsingConfigBasedCreator) return this._creator(this._config);
499
+ return this._creator(elementOrData, this._config);
500
+ }).then((editor) => {
501
+ this._editor = editor;
502
+ editor.model.document.on("change:data", this._throttledSave);
503
+ this._lastDocumentVersion = editor.model.document.version;
504
+ this._data = this._getData();
505
+ if (!this._editorAttachTo) this._editables = this._getEditables();
506
+ this.state = "ready";
507
+ this._fire("stateChange");
508
+ }).finally(() => {
509
+ this._lifecyclePromise = null;
510
+ });
511
+ return this._lifecyclePromise;
512
+ }
513
+ /**
514
+ * Destroys the watchdog and the current editor instance. It fires the callback
515
+ * registered in {@link #setDestructor `setDestructor()`} and uses it to destroy the editor instance.
516
+ * It also sets the state to `destroyed`.
517
+ */
518
+ destroy() {
519
+ this._lifecyclePromise = Promise.resolve(this._lifecyclePromise).then(() => {
520
+ this.state = "destroyed";
521
+ this._fire("stateChange");
522
+ super.destroy();
523
+ return this._destroy();
524
+ }).finally(() => {
525
+ this._lifecyclePromise = null;
526
+ });
527
+ return this._lifecyclePromise;
528
+ }
529
+ _destroy() {
530
+ return Promise.resolve().then(() => {
531
+ this._stopErrorHandling();
532
+ this._throttledSave.cancel();
533
+ const editor = this._editor;
534
+ this._editor = null;
535
+ editor.model.document.off("change:data", this._throttledSave);
536
+ return this._destructor(editor);
537
+ });
538
+ }
539
+ /**
540
+ * Saves the editor data, so it can be restored after the crash even if the data cannot be fetched at
541
+ * the moment of the crash.
542
+ */
543
+ _save() {
544
+ const version = this._editor.model.document.version;
545
+ try {
546
+ this._data = this._getData();
547
+ if (!this._editorAttachTo) this._editables = this._getEditables();
548
+ this._lastDocumentVersion = version;
549
+ } catch (err) {
550
+ console.error(err, "An error happened during restoring editor data. Editor will be restored from the previously saved data.");
551
+ }
552
+ }
553
+ /**
554
+ * @internal
555
+ */
556
+ _setExcludedProperties(props) {
557
+ this._excludedProps = props;
558
+ }
559
+ /**
560
+ * Gets all data that is required to reinitialize editor instance.
561
+ */
562
+ _getData() {
563
+ const editor = this._editor;
564
+ const roots = editor.model.document.roots.filter((root) => root.isAttached() && root.rootName != "$graveyard");
565
+ const { plugins } = editor;
566
+ const commentsRepository = plugins.has("CommentsRepository") && plugins.get("CommentsRepository");
567
+ const trackChanges = plugins.has("TrackChanges") && plugins.get("TrackChanges");
568
+ const data = {
569
+ roots: {},
570
+ markers: {},
571
+ commentThreads: JSON.stringify([]),
572
+ suggestions: JSON.stringify([])
573
+ };
574
+ roots.forEach((root) => {
575
+ data.roots[root.rootName] = {
576
+ content: JSON.stringify(Array.from(root.getChildren())),
577
+ attributes: JSON.stringify(Array.from(root.getAttributes())),
578
+ modelElement: root.name,
579
+ isLoaded: root._isLoaded
580
+ };
581
+ });
582
+ for (const marker of editor.model.markers) {
583
+ if (!marker._affectsData) continue;
584
+ data.markers[marker.name] = {
585
+ rangeJSON: marker.getRange().toJSON(),
586
+ usingOperation: marker._managedUsingOperations,
587
+ affectsData: marker._affectsData
588
+ };
589
+ }
590
+ if (commentsRepository) data.commentThreads = JSON.stringify(commentsRepository.getCommentThreads({
591
+ toJSON: true,
592
+ skipNotAttached: true
593
+ }));
594
+ if (trackChanges) data.suggestions = JSON.stringify(trackChanges.getSuggestions({
595
+ toJSON: true,
596
+ skipNotAttached: true
597
+ }));
598
+ return data;
599
+ }
600
+ /**
601
+ * For each attached model root, returns its HTML editable element (if available).
602
+ */
603
+ _getEditables() {
604
+ const editables = {};
605
+ for (const rootName of this.editor.model.document.getRootNames()) {
606
+ const editable = this.editor.ui.getEditableElement(rootName);
607
+ if (editable) editables[rootName] = editable;
608
+ }
609
+ return editables;
610
+ }
611
+ /**
612
+ * Traverses the error context and the current editor to find out whether these structures are connected
613
+ * to each other via properties.
614
+ *
615
+ * @internal
616
+ */
617
+ _isErrorComingFromThisItem(error) {
618
+ return areConnectedThroughProperties(this._editor, error.context, this._excludedProps);
619
+ }
620
+ /**
621
+ * Detects whether the `create()` call was made in config-based creator mode
622
+ * (i.e., the first argument is a config object rather than a source element or data).
623
+ */
624
+ _detectConfigBasedCreator(elementOrDataOrConfig, configOrContext) {
625
+ if (typeof elementOrDataOrConfig === "string" || isElement$1(elementOrDataOrConfig)) return false;
626
+ if (configOrContext && typeof configOrContext === "object" && !("destroy" in configOrContext) && Object.keys(configOrContext).length > 0) return false;
627
+ if (elementOrDataOrConfig && typeof elementOrDataOrConfig === "object") {
628
+ const values = Object.values(elementOrDataOrConfig);
629
+ if (values.length > 0 && values.every((v) => typeof v === "string" || isElement$1(v))) return false;
630
+ }
631
+ return true;
632
+ }
633
+ /**
634
+ * Clones the editor configuration.
635
+ */
636
+ _cloneEditorConfiguration(config) {
637
+ return cloneDeepWith(config, (value, key) => {
638
+ if (isElement$1(value)) return value;
639
+ if (key === "context") return value;
640
+ });
641
+ }
642
+ };
858
643
  /**
859
- * An alias for `isElement` from `es-toolkit/compat` with additional type guard.
860
- */ function isElement(value) {
861
- return isElement$2(value);
644
+ * Internal plugin that is used to stop the default editor initialization and restoring the editor state
645
+ * based on the `editor.config._watchdogInitialData` data.
646
+ */
647
+ var EditorWatchdogInitPlugin = class {
648
+ editor;
649
+ _data;
650
+ constructor(editor) {
651
+ this.editor = editor;
652
+ this._data = editor.config.get("_watchdogInitialData");
653
+ }
654
+ /**
655
+ * @inheritDoc
656
+ */
657
+ init() {
658
+ this.editor.data.on("init", (evt) => {
659
+ evt.stop();
660
+ this.editor.model.enqueueChange({ isUndoable: false }, (writer) => {
661
+ this._restoreCollaborationData();
662
+ this._restoreEditorData(writer);
663
+ });
664
+ this.editor.data.fire("ready");
665
+ }, { priority: 999 });
666
+ }
667
+ /**
668
+ * Creates a model node (element or text) based on provided JSON.
669
+ */
670
+ _createNode(writer, jsonNode) {
671
+ if ("name" in jsonNode) {
672
+ const element = writer.createElement(jsonNode.name, jsonNode.attributes);
673
+ if (jsonNode.children) for (const child of jsonNode.children) element._appendChild(this._createNode(writer, child));
674
+ return element;
675
+ } else return writer.createText(jsonNode.data, jsonNode.attributes);
676
+ }
677
+ /**
678
+ * Restores the editor by setting the document data, roots attributes and markers.
679
+ */
680
+ _restoreEditorData(writer) {
681
+ const editor = this.editor;
682
+ Object.entries(this._data.roots).forEach(([rootName, { content, attributes }]) => {
683
+ const parsedNodes = JSON.parse(content);
684
+ const parsedAttributes = JSON.parse(attributes);
685
+ const rootElement = editor.model.document.getRoot(rootName);
686
+ for (const [key, value] of parsedAttributes) writer.setAttribute(key, value, rootElement);
687
+ for (const child of parsedNodes) {
688
+ const node = this._createNode(writer, child);
689
+ writer.insert(node, rootElement, "end");
690
+ }
691
+ });
692
+ Object.entries(this._data.markers).forEach(([markerName, markerOptions]) => {
693
+ const { document } = editor.model;
694
+ const { rangeJSON: { start, end }, ...options } = markerOptions;
695
+ const root = document.getRoot(start.root);
696
+ const startPosition = writer.createPositionFromPath(root, start.path, start.stickiness);
697
+ const endPosition = writer.createPositionFromPath(root, end.path, end.stickiness);
698
+ const range = writer.createRange(startPosition, endPosition);
699
+ writer.addMarker(markerName, {
700
+ range,
701
+ ...options
702
+ });
703
+ });
704
+ }
705
+ /**
706
+ * Restores the editor collaboration data - comment threads and suggestions.
707
+ */
708
+ _restoreCollaborationData() {
709
+ const parsedCommentThreads = JSON.parse(this._data.commentThreads);
710
+ const parsedSuggestions = JSON.parse(this._data.suggestions);
711
+ if (this.editor.plugins.has("CommentsRepository")) {
712
+ const commentsRepository = this.editor.plugins.get("CommentsRepository");
713
+ for (const commentThread of commentsRepository.getCommentThreads()) commentsRepository._removeCommentThread({ threadId: commentThread.id });
714
+ parsedCommentThreads.forEach((commentThreadData) => {
715
+ const channelId = this.editor.config.get("collaboration.channelId");
716
+ this.editor.plugins.get("CommentsRepository").addCommentThread({
717
+ channelId,
718
+ ...commentThreadData
719
+ });
720
+ });
721
+ }
722
+ if (this.editor.plugins.has("TrackChangesEditing")) {
723
+ const trackChangesEditing = this.editor.plugins.get("TrackChangesEditing");
724
+ for (const suggestion of trackChangesEditing.getSuggestions()) trackChangesEditing._removeSuggestion(suggestion);
725
+ parsedSuggestions.forEach((suggestionData) => {
726
+ trackChangesEditing.addSuggestionData(suggestionData);
727
+ });
728
+ }
729
+ }
730
+ };
731
+ /**
732
+ * An alias for `isElement` from `es-toolkit/compat` with additional type guard.
733
+ */
734
+ function isElement$1(value) {
735
+ return isElement(value);
862
736
  }
863
737
  /**
864
- * Reads the editable options persisted in the root's `$rootEditableOptions` model attribute and returns them as a
865
- * {@link module:core/editor/editorconfig~RootConfig} partial. It is used to recreate a detached editable when the
866
- * editor is restarted in the config-based creator mode.
867
- *
868
- * Only the known `placeholder`, `label` and `element` fields with a value are copied; their values are validated and
869
- * normalized later by the editor constructor. Fields are returned only when set, so merging the result does not
870
- * overwrite the existing restart config with `undefined`. A missing or tampered (non-object) value is dropped so it
871
- * cannot break the restart.
872
- *
873
- * @param serializedAttributes The root attributes serialized as a JSON string of `[ key, value ]` entries.
874
- */ function getSavedRootEditableOptions(serializedAttributes) {
875
- const { $rootEditableOptions: options } = Object.fromEntries(JSON.parse(serializedAttributes));
876
- if (!options || typeof options != 'object') {
877
- return {};
878
- }
879
- return {
880
- ...options.placeholder && {
881
- placeholder: options.placeholder
882
- },
883
- ...options.label && {
884
- label: options.label
885
- },
886
- ...options.element && {
887
- element: options.element
888
- }
889
- };
738
+ * Reads the editable options persisted in the root's `$rootEditableOptions` model attribute and returns them as a
739
+ * {@link module:core/editor/editorconfig~RootConfig} partial. It is used to recreate a detached editable when the
740
+ * editor is restarted in the config-based creator mode.
741
+ *
742
+ * Only the known `placeholder`, `label` and `element` fields with a value are copied; their values are validated and
743
+ * normalized later by the editor constructor. Fields are returned only when set, so merging the result does not
744
+ * overwrite the existing restart config with `undefined`. A missing or tampered (non-object) value is dropped so it
745
+ * cannot break the restart.
746
+ *
747
+ * @param serializedAttributes The root attributes serialized as a JSON string of `[ key, value ]` entries.
748
+ */
749
+ function getSavedRootEditableOptions(serializedAttributes) {
750
+ const { $rootEditableOptions: options } = Object.fromEntries(JSON.parse(serializedAttributes));
751
+ if (!options || typeof options != "object") return {};
752
+ return {
753
+ ...options.placeholder && { placeholder: options.placeholder },
754
+ ...options.label && { label: options.label },
755
+ ...options.element && { element: options.element }
756
+ };
890
757
  }
891
758
 
892
- const mainQueueId = Symbol('MainQueueId');
893
759
  /**
894
- * A watchdog for the {@link module:core/context~Context} class.
895
- *
896
- * See the {@glink features/watchdog Watchdog feature guide} to learn the rationale behind it and
897
- * how to use it.
898
- */ class ContextWatchdog extends Watchdog {
899
- /**
900
- * A map of internal watchdogs for added items.
901
- */ _watchdogs = new Map();
902
- /**
903
- * The watchdog configuration.
904
- */ _watchdogConfig;
905
- /**
906
- * The current context instance.
907
- */ _context = null;
908
- /**
909
- * Context properties (nodes/references) that are gathered during the initial context creation
910
- * and are used to distinguish the origin of an error.
911
- */ _contextProps = new Set();
912
- /**
913
- * An action queue, which is used to handle async functions queuing.
914
- */ _actionQueues = new ActionQueues();
915
- /**
916
- * The configuration for the {@link module:core/context~Context}.
917
- */ _contextConfig;
918
- /**
919
- * The watched item.
920
- */ _item;
921
- /**
922
- * The context watchdog class constructor.
923
- *
924
- * ```ts
925
- * const watchdog = new ContextWatchdog( Context );
926
- *
927
- * await watchdog.create( contextConfiguration );
928
- *
929
- * await watchdog.add( item );
930
- * ```
931
- *
932
- * See the {@glink features/watchdog Watchdog feature guide} to learn more how to use this feature.
933
- *
934
- * @param Context The {@link module:core/context~Context} class.
935
- * @param watchdogConfig The watchdog configuration.
936
- */ constructor(Context, watchdogConfig = {}){
937
- super(watchdogConfig);
938
- this._watchdogConfig = watchdogConfig;
939
- // Default creator and destructor.
940
- this._creator = (contextConfig)=>Context.create(contextConfig);
941
- this._destructor = (context)=>context.destroy();
942
- this._actionQueues.onEmpty(()=>{
943
- if (this.state === 'initializing') {
944
- this.state = 'ready';
945
- this._fire('stateChange');
946
- }
947
- });
948
- }
949
- /**
950
- * Sets the function that is responsible for the context creation.
951
- * It expects a function that should return a promise (or `undefined`).
952
- *
953
- * ```ts
954
- * watchdog.setCreator( config => Context.create( config ) );
955
- * ```
956
- */ setCreator(creator) {
957
- this._creator = creator;
958
- }
959
- /**
960
- * Sets the function that is responsible for the context destruction.
961
- * Overrides the default destruction function, which destroys only the context instance.
962
- * It expects a function that should return a promise (or `undefined`).
963
- *
964
- * ```ts
965
- * watchdog.setDestructor( context => {
966
- * // Do something before the context is destroyed.
967
- *
968
- * return context
969
- * .destroy()
970
- * .then( () => {
971
- * // Do something after the context is destroyed.
972
- * } );
973
- * } );
974
- * ```
975
- */ setDestructor(destructor) {
976
- this._destructor = destructor;
977
- }
978
- /**
979
- * The context instance. Keep in mind that this property might be changed when the context watchdog restarts,
980
- * so do not keep this instance internally. Always operate on the `ContextWatchdog#context` property.
981
- */ get context() {
982
- return this._context;
983
- }
984
- /**
985
- * Initializes the context watchdog. Once it is created, the watchdog takes care about
986
- * recreating the context and the provided items, and starts the error handling mechanism.
987
- *
988
- * ```ts
989
- * await watchdog.create( {
990
- * plugins: []
991
- * } );
992
- * ```
993
- *
994
- * @param contextConfig The context configuration. See {@link module:core/context~Context}.
995
- */ create(contextConfig = {}) {
996
- return this._actionQueues.enqueue(mainQueueId, ()=>{
997
- this._contextConfig = contextConfig;
998
- return this._create();
999
- });
1000
- }
1001
- /**
1002
- * Returns an item instance with the given `itemId`.
1003
- *
1004
- * ```ts
1005
- * const editor1 = watchdog.getItem( 'editor1' );
1006
- * ```
1007
- *
1008
- * @param itemId The item ID.
1009
- * @returns The item instance or `undefined` if an item with a given ID has not been found.
1010
- */ getItem(itemId) {
1011
- const watchdog = this._getWatchdog(itemId);
1012
- return watchdog._item;
1013
- }
1014
- /**
1015
- * Gets the state of the given item. See {@link #state} for a list of available states.
1016
- *
1017
- * ```ts
1018
- * const editor1State = watchdog.getItemState( 'editor1' );
1019
- * ```
1020
- *
1021
- * @param itemId Item ID.
1022
- * @returns The state of the item.
1023
- */ getItemState(itemId) {
1024
- const watchdog = this._getWatchdog(itemId);
1025
- return watchdog.state;
1026
- }
1027
- /**
1028
- * Adds items to the watchdog. Once created, instances of these items will be available using the {@link #getItem} method.
1029
- *
1030
- * Items can be passed together as an array of objects:
1031
- *
1032
- * ```ts
1033
- * await watchdog.add( [ {
1034
- * id: 'editor1',
1035
- * type: 'editor',
1036
- * config: {
1037
- * attachTo: document.querySelector( '#editor' ),
1038
- * plugins: [ Essentials, Paragraph, Bold, Italic ],
1039
- * toolbar: [ 'bold', 'italic', 'alignment' ]
1040
- * },
1041
- * creator: config => ClassicEditor.create( config )
1042
- * } ] );
1043
- * ```
1044
- *
1045
- * Or one by one as objects:
1046
- *
1047
- * ```ts
1048
- * await watchdog.add( {
1049
- * id: 'editor1',
1050
- * type: 'editor',
1051
- * config: {
1052
- * attachTo: document.querySelector( '#editor' ),
1053
- * plugins: [ Essentials, Paragraph, Bold, Italic ],
1054
- * toolbar: [ 'bold', 'italic', 'alignment' ]
1055
- * },
1056
- * creator: config => ClassicEditor.create( config )
1057
- * } );
1058
- * ```
1059
- *
1060
- * Then an instance can be retrieved using the {@link #getItem} method:
1061
- *
1062
- * ```ts
1063
- * const editor1 = watchdog.getItem( 'editor1' );
1064
- * ```
1065
- *
1066
- * Note that this method can be called multiple times, but for performance reasons it is better
1067
- * to pass all items together.
1068
- *
1069
- * @param itemConfigurationOrItemConfigurations An item configuration object or an array of item configurations.
1070
- */ add(itemConfigurationOrItemConfigurations) {
1071
- const itemConfigurations = toArray(itemConfigurationOrItemConfigurations);
1072
- return Promise.all(itemConfigurations.map((item)=>{
1073
- return this._actionQueues.enqueue(item.id, ()=>{
1074
- if (this.state === 'destroyed') {
1075
- throw new Error('Cannot add items to destroyed watchdog.');
1076
- }
1077
- if (!this._context) {
1078
- throw new Error('Context was not created yet. You should call the `ContextWatchdog#create()` method first.');
1079
- }
1080
- let watchdog;
1081
- if (this._watchdogs.has(item.id)) {
1082
- throw new Error(`Item with the given id is already added: '${item.id}'.`);
1083
- }
1084
- if (item.type === 'editor') {
1085
- watchdog = new EditorWatchdog(null, this._watchdogConfig);
1086
- watchdog.setCreator(item.creator);
1087
- watchdog._setExcludedProperties(this._contextProps);
1088
- if (item.destructor) {
1089
- watchdog.setDestructor(item.destructor);
1090
- }
1091
- this._watchdogs.set(item.id, watchdog);
1092
- // Enqueue the internal watchdog errors within the main queue.
1093
- // And propagate the internal `error` events as `itemError` event.
1094
- watchdog.on('error', (evt, { error, causesRestart })=>{
1095
- this._fire('itemError', {
1096
- itemId: item.id,
1097
- error
1098
- });
1099
- // Do not enqueue the item restart action if the item will not restart.
1100
- if (!causesRestart) {
1101
- return;
1102
- }
1103
- this._actionQueues.enqueue(item.id, ()=>new Promise((res)=>{
1104
- const rethrowRestartEventOnce = ()=>{
1105
- watchdog.off('restart', rethrowRestartEventOnce);
1106
- this._fire('itemRestart', {
1107
- itemId: item.id
1108
- });
1109
- res();
1110
- };
1111
- watchdog.on('restart', rethrowRestartEventOnce);
1112
- }));
1113
- });
1114
- if (item.sourceElementOrData !== undefined) {
1115
- return watchdog.create(item.sourceElementOrData, item.config, this._context);
1116
- }
1117
- return watchdog.create(item.config, this._context);
1118
- } else {
1119
- throw new Error(`Not supported item type: '${item.type}'.`);
1120
- }
1121
- });
1122
- }));
1123
- }
1124
- /**
1125
- * Removes and destroys item(s) with given ID(s).
1126
- *
1127
- * ```ts
1128
- * await watchdog.remove( 'editor1' );
1129
- * ```
1130
- *
1131
- * Or
1132
- *
1133
- * ```ts
1134
- * await watchdog.remove( [ 'editor1', 'editor2' ] );
1135
- * ```
1136
- *
1137
- * @param itemIdOrItemIds Item ID or an array of item IDs.
1138
- */ remove(itemIdOrItemIds) {
1139
- const itemIds = toArray(itemIdOrItemIds);
1140
- return Promise.all(itemIds.map((itemId)=>{
1141
- return this._actionQueues.enqueue(itemId, ()=>{
1142
- const watchdog = this._getWatchdog(itemId);
1143
- this._watchdogs.delete(itemId);
1144
- return watchdog.destroy();
1145
- });
1146
- }));
1147
- }
1148
- /**
1149
- * Destroys the context watchdog and all added items.
1150
- * Once the context watchdog is destroyed, new items cannot be added.
1151
- *
1152
- * ```ts
1153
- * await watchdog.destroy();
1154
- * ```
1155
- */ destroy() {
1156
- return this._actionQueues.enqueue(mainQueueId, ()=>{
1157
- this.state = 'destroyed';
1158
- this._fire('stateChange');
1159
- super.destroy();
1160
- return this._destroy();
1161
- });
1162
- }
1163
- /**
1164
- * Restarts the context watchdog.
1165
- */ _restart() {
1166
- return this._actionQueues.enqueue(mainQueueId, ()=>{
1167
- this.state = 'initializing';
1168
- this._fire('stateChange');
1169
- return this._destroy().catch((err)=>{
1170
- console.error('An error happened during destroying the context or items.', err);
1171
- }).then(()=>this._create()).then(()=>this._fire('restart'));
1172
- });
1173
- }
1174
- /**
1175
- * Initializes the context watchdog.
1176
- */ _create() {
1177
- return Promise.resolve().then(()=>{
1178
- this._startErrorHandling();
1179
- return this._creator(this._contextConfig);
1180
- }).then((context)=>{
1181
- this._context = context;
1182
- this._contextProps = getSubNodes(this._context);
1183
- return Promise.all(Array.from(this._watchdogs.values()).map((watchdog)=>{
1184
- watchdog._setExcludedProperties(this._contextProps);
1185
- if (watchdog._isUsingConfigBasedCreator) {
1186
- return watchdog.create(undefined, this._context);
1187
- }
1188
- return watchdog.create(undefined, undefined, this._context);
1189
- }));
1190
- });
1191
- }
1192
- /**
1193
- * Destroys the context instance and all added items.
1194
- */ _destroy() {
1195
- return Promise.resolve().then(()=>{
1196
- this._stopErrorHandling();
1197
- const context = this._context;
1198
- this._context = null;
1199
- this._contextProps = new Set();
1200
- return Promise.all(Array.from(this._watchdogs.values()).map((watchdog)=>watchdog.destroy()))// Context destructor destroys each editor.
1201
- .then(()=>this._destructor(context));
1202
- });
1203
- }
1204
- /**
1205
- * Returns the watchdog for a given item ID.
1206
- *
1207
- * @param itemId Item ID.
1208
- */ _getWatchdog(itemId) {
1209
- const watchdog = this._watchdogs.get(itemId);
1210
- if (!watchdog) {
1211
- throw new Error(`Item with the given id was not registered: ${itemId}.`);
1212
- }
1213
- return watchdog;
1214
- }
1215
- /**
1216
- * Checks whether an error comes from the context instance and not from the item instances.
1217
- *
1218
- * @internal
1219
- */ _isErrorComingFromThisItem(error) {
1220
- for (const watchdog of this._watchdogs.values()){
1221
- if (watchdog._isErrorComingFromThisItem(error)) {
1222
- return false;
1223
- }
1224
- }
1225
- return areConnectedThroughProperties(this._context, error.context);
1226
- }
1227
- }
760
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
761
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
762
+ */
1228
763
  /**
1229
- * Manager of action queues that allows queuing async functions.
1230
- */ class ActionQueues {
1231
- _onEmptyCallbacks = [];
1232
- _queues = new Map();
1233
- _activeActions = 0;
1234
- /**
1235
- * Used to register callbacks that will be run when the queue becomes empty.
1236
- *
1237
- * @param onEmptyCallback A callback that will be run whenever the queue becomes empty.
1238
- */ onEmpty(onEmptyCallback) {
1239
- this._onEmptyCallbacks.push(onEmptyCallback);
1240
- }
1241
- /**
1242
- * It adds asynchronous actions (functions) to the proper queue and runs them one by one.
1243
- *
1244
- * @param queueId The action queue ID.
1245
- * @param action A function that should be enqueued.
1246
- */ enqueue(queueId, action) {
1247
- const isMainAction = queueId === mainQueueId;
1248
- this._activeActions++;
1249
- if (!this._queues.get(queueId)) {
1250
- this._queues.set(queueId, Promise.resolve());
1251
- }
1252
- // List all sources of actions that the current action needs to await for.
1253
- // For the main action wait for all other actions.
1254
- // For the item action wait only for the item queue and the main queue.
1255
- const awaitedActions = isMainAction ? Promise.all(this._queues.values()) : Promise.all([
1256
- this._queues.get(mainQueueId),
1257
- this._queues.get(queueId)
1258
- ]);
1259
- const queueWithAction = awaitedActions.then(action);
1260
- // Catch all errors in the main queue to stack promises even if an error occurred in the past.
1261
- const nonErrorQueue = queueWithAction.catch(()=>{});
1262
- this._queues.set(queueId, nonErrorQueue);
1263
- return queueWithAction.finally(()=>{
1264
- this._activeActions--;
1265
- if (this._queues.get(queueId) === nonErrorQueue && this._activeActions === 0) {
1266
- this._onEmptyCallbacks.forEach((cb)=>cb());
1267
- }
1268
- });
1269
- }
1270
- }
764
+ * @module watchdog/contextwatchdog
765
+ */
766
+ const mainQueueId = Symbol("MainQueueId");
1271
767
  /**
1272
- * Transforms any value to an array. If the provided value is already an array, it is returned unchanged.
1273
- *
1274
- * @param elementOrArray The value to transform to an array.
1275
- * @returns An array created from data.
1276
- */ function toArray(elementOrArray) {
1277
- return Array.isArray(elementOrArray) ? elementOrArray : [
1278
- elementOrArray
1279
- ];
768
+ * A watchdog for the {@link module:core/context~Context} class.
769
+ *
770
+ * See the {@glink features/watchdog Watchdog feature guide} to learn the rationale behind it and
771
+ * how to use it.
772
+ */
773
+ var ContextWatchdog = class extends Watchdog {
774
+ /**
775
+ * A map of internal watchdogs for added items.
776
+ */
777
+ _watchdogs = /* @__PURE__ */ new Map();
778
+ /**
779
+ * The watchdog configuration.
780
+ */
781
+ _watchdogConfig;
782
+ /**
783
+ * The current context instance.
784
+ */
785
+ _context = null;
786
+ /**
787
+ * Context properties (nodes/references) that are gathered during the initial context creation
788
+ * and are used to distinguish the origin of an error.
789
+ */
790
+ _contextProps = /* @__PURE__ */ new Set();
791
+ /**
792
+ * An action queue, which is used to handle async functions queuing.
793
+ */
794
+ _actionQueues = new ActionQueues();
795
+ /**
796
+ * The configuration for the {@link module:core/context~Context}.
797
+ */
798
+ _contextConfig;
799
+ /**
800
+ * The watched item.
801
+ */
802
+ _item;
803
+ /**
804
+ * The context watchdog class constructor.
805
+ *
806
+ * ```ts
807
+ * const watchdog = new ContextWatchdog( Context );
808
+ *
809
+ * await watchdog.create( contextConfiguration );
810
+ *
811
+ * await watchdog.add( item );
812
+ * ```
813
+ *
814
+ * See the {@glink features/watchdog Watchdog feature guide} to learn more how to use this feature.
815
+ *
816
+ * @param Context The {@link module:core/context~Context} class.
817
+ * @param watchdogConfig The watchdog configuration.
818
+ */
819
+ constructor(Context, watchdogConfig = {}) {
820
+ super(watchdogConfig);
821
+ this._watchdogConfig = watchdogConfig;
822
+ this._creator = (contextConfig) => Context.create(contextConfig);
823
+ this._destructor = (context) => context.destroy();
824
+ this._actionQueues.onEmpty(() => {
825
+ if (this.state === "initializing") {
826
+ this.state = "ready";
827
+ this._fire("stateChange");
828
+ }
829
+ });
830
+ }
831
+ /**
832
+ * Sets the function that is responsible for the context creation.
833
+ * It expects a function that should return a promise (or `undefined`).
834
+ *
835
+ * ```ts
836
+ * watchdog.setCreator( config => Context.create( config ) );
837
+ * ```
838
+ */
839
+ setCreator(creator) {
840
+ this._creator = creator;
841
+ }
842
+ /**
843
+ * Sets the function that is responsible for the context destruction.
844
+ * Overrides the default destruction function, which destroys only the context instance.
845
+ * It expects a function that should return a promise (or `undefined`).
846
+ *
847
+ * ```ts
848
+ * watchdog.setDestructor( context => {
849
+ * // Do something before the context is destroyed.
850
+ *
851
+ * return context
852
+ * .destroy()
853
+ * .then( () => {
854
+ * // Do something after the context is destroyed.
855
+ * } );
856
+ * } );
857
+ * ```
858
+ */
859
+ setDestructor(destructor) {
860
+ this._destructor = destructor;
861
+ }
862
+ /**
863
+ * The context instance. Keep in mind that this property might be changed when the context watchdog restarts,
864
+ * so do not keep this instance internally. Always operate on the `ContextWatchdog#context` property.
865
+ */
866
+ get context() {
867
+ return this._context;
868
+ }
869
+ /**
870
+ * Initializes the context watchdog. Once it is created, the watchdog takes care about
871
+ * recreating the context and the provided items, and starts the error handling mechanism.
872
+ *
873
+ * ```ts
874
+ * await watchdog.create( {
875
+ * plugins: []
876
+ * } );
877
+ * ```
878
+ *
879
+ * @param contextConfig The context configuration. See {@link module:core/context~Context}.
880
+ */
881
+ create(contextConfig = {}) {
882
+ return this._actionQueues.enqueue(mainQueueId, () => {
883
+ this._contextConfig = contextConfig;
884
+ return this._create();
885
+ });
886
+ }
887
+ /**
888
+ * Returns an item instance with the given `itemId`.
889
+ *
890
+ * ```ts
891
+ * const editor1 = watchdog.getItem( 'editor1' );
892
+ * ```
893
+ *
894
+ * @param itemId The item ID.
895
+ * @returns The item instance or `undefined` if an item with a given ID has not been found.
896
+ */
897
+ getItem(itemId) {
898
+ return this._getWatchdog(itemId)._item;
899
+ }
900
+ /**
901
+ * Gets the state of the given item. See {@link #state} for a list of available states.
902
+ *
903
+ * ```ts
904
+ * const editor1State = watchdog.getItemState( 'editor1' );
905
+ * ```
906
+ *
907
+ * @param itemId Item ID.
908
+ * @returns The state of the item.
909
+ */
910
+ getItemState(itemId) {
911
+ return this._getWatchdog(itemId).state;
912
+ }
913
+ /**
914
+ * Adds items to the watchdog. Once created, instances of these items will be available using the {@link #getItem} method.
915
+ *
916
+ * Items can be passed together as an array of objects:
917
+ *
918
+ * ```ts
919
+ * await watchdog.add( [ {
920
+ * id: 'editor1',
921
+ * type: 'editor',
922
+ * config: {
923
+ * attachTo: document.querySelector( '#editor' ),
924
+ * plugins: [ Essentials, Paragraph, Bold, Italic ],
925
+ * toolbar: [ 'bold', 'italic', 'alignment' ]
926
+ * },
927
+ * creator: config => ClassicEditor.create( config )
928
+ * } ] );
929
+ * ```
930
+ *
931
+ * Or one by one as objects:
932
+ *
933
+ * ```ts
934
+ * await watchdog.add( {
935
+ * id: 'editor1',
936
+ * type: 'editor',
937
+ * config: {
938
+ * attachTo: document.querySelector( '#editor' ),
939
+ * plugins: [ Essentials, Paragraph, Bold, Italic ],
940
+ * toolbar: [ 'bold', 'italic', 'alignment' ]
941
+ * },
942
+ * creator: config => ClassicEditor.create( config )
943
+ * } );
944
+ * ```
945
+ *
946
+ * Then an instance can be retrieved using the {@link #getItem} method:
947
+ *
948
+ * ```ts
949
+ * const editor1 = watchdog.getItem( 'editor1' );
950
+ * ```
951
+ *
952
+ * Note that this method can be called multiple times, but for performance reasons it is better
953
+ * to pass all items together.
954
+ *
955
+ * @param itemConfigurationOrItemConfigurations An item configuration object or an array of item configurations.
956
+ */
957
+ add(itemConfigurationOrItemConfigurations) {
958
+ const itemConfigurations = toArray(itemConfigurationOrItemConfigurations);
959
+ return Promise.all(itemConfigurations.map((item) => {
960
+ return this._actionQueues.enqueue(item.id, () => {
961
+ if (this.state === "destroyed") throw new Error("Cannot add items to destroyed watchdog.");
962
+ if (!this._context) throw new Error("Context was not created yet. You should call the `ContextWatchdog#create()` method first.");
963
+ let watchdog;
964
+ if (this._watchdogs.has(item.id)) throw new Error(`Item with the given id is already added: '${item.id}'.`);
965
+ if (item.type === "editor") {
966
+ watchdog = new EditorWatchdog(null, this._watchdogConfig);
967
+ watchdog.setCreator(item.creator);
968
+ watchdog._setExcludedProperties(this._contextProps);
969
+ if (item.destructor) watchdog.setDestructor(item.destructor);
970
+ this._watchdogs.set(item.id, watchdog);
971
+ watchdog.on("error", (evt, { error, causesRestart }) => {
972
+ this._fire("itemError", {
973
+ itemId: item.id,
974
+ error
975
+ });
976
+ if (!causesRestart) return;
977
+ this._actionQueues.enqueue(item.id, () => new Promise((res) => {
978
+ const rethrowRestartEventOnce = () => {
979
+ watchdog.off("restart", rethrowRestartEventOnce);
980
+ this._fire("itemRestart", { itemId: item.id });
981
+ res();
982
+ };
983
+ watchdog.on("restart", rethrowRestartEventOnce);
984
+ }));
985
+ });
986
+ if (item.sourceElementOrData !== void 0) return watchdog.create(item.sourceElementOrData, item.config, this._context);
987
+ return watchdog.create(item.config, this._context);
988
+ } else throw new Error(`Not supported item type: '${item.type}'.`);
989
+ });
990
+ }));
991
+ }
992
+ /**
993
+ * Removes and destroys item(s) with given ID(s).
994
+ *
995
+ * ```ts
996
+ * await watchdog.remove( 'editor1' );
997
+ * ```
998
+ *
999
+ * Or
1000
+ *
1001
+ * ```ts
1002
+ * await watchdog.remove( [ 'editor1', 'editor2' ] );
1003
+ * ```
1004
+ *
1005
+ * @param itemIdOrItemIds Item ID or an array of item IDs.
1006
+ */
1007
+ remove(itemIdOrItemIds) {
1008
+ const itemIds = toArray(itemIdOrItemIds);
1009
+ return Promise.all(itemIds.map((itemId) => {
1010
+ return this._actionQueues.enqueue(itemId, () => {
1011
+ const watchdog = this._getWatchdog(itemId);
1012
+ this._watchdogs.delete(itemId);
1013
+ return watchdog.destroy();
1014
+ });
1015
+ }));
1016
+ }
1017
+ /**
1018
+ * Destroys the context watchdog and all added items.
1019
+ * Once the context watchdog is destroyed, new items cannot be added.
1020
+ *
1021
+ * ```ts
1022
+ * await watchdog.destroy();
1023
+ * ```
1024
+ */
1025
+ destroy() {
1026
+ return this._actionQueues.enqueue(mainQueueId, () => {
1027
+ this.state = "destroyed";
1028
+ this._fire("stateChange");
1029
+ super.destroy();
1030
+ return this._destroy();
1031
+ });
1032
+ }
1033
+ /**
1034
+ * Restarts the context watchdog.
1035
+ */
1036
+ _restart() {
1037
+ return this._actionQueues.enqueue(mainQueueId, () => {
1038
+ this.state = "initializing";
1039
+ this._fire("stateChange");
1040
+ return this._destroy().catch((err) => {
1041
+ console.error("An error happened during destroying the context or items.", err);
1042
+ }).then(() => this._create()).then(() => this._fire("restart"));
1043
+ });
1044
+ }
1045
+ /**
1046
+ * Initializes the context watchdog.
1047
+ */
1048
+ _create() {
1049
+ return Promise.resolve().then(() => {
1050
+ this._startErrorHandling();
1051
+ return this._creator(this._contextConfig);
1052
+ }).then((context) => {
1053
+ this._context = context;
1054
+ this._contextProps = getSubNodes(this._context);
1055
+ return Promise.all(Array.from(this._watchdogs.values()).map((watchdog) => {
1056
+ watchdog._setExcludedProperties(this._contextProps);
1057
+ if (watchdog._isUsingConfigBasedCreator) return watchdog.create(void 0, this._context);
1058
+ return watchdog.create(void 0, void 0, this._context);
1059
+ }));
1060
+ });
1061
+ }
1062
+ /**
1063
+ * Destroys the context instance and all added items.
1064
+ */
1065
+ _destroy() {
1066
+ return Promise.resolve().then(() => {
1067
+ this._stopErrorHandling();
1068
+ const context = this._context;
1069
+ this._context = null;
1070
+ this._contextProps = /* @__PURE__ */ new Set();
1071
+ return Promise.all(Array.from(this._watchdogs.values()).map((watchdog) => watchdog.destroy())).then(() => this._destructor(context));
1072
+ });
1073
+ }
1074
+ /**
1075
+ * Returns the watchdog for a given item ID.
1076
+ *
1077
+ * @param itemId Item ID.
1078
+ */
1079
+ _getWatchdog(itemId) {
1080
+ const watchdog = this._watchdogs.get(itemId);
1081
+ if (!watchdog) throw new Error(`Item with the given id was not registered: ${itemId}.`);
1082
+ return watchdog;
1083
+ }
1084
+ /**
1085
+ * Checks whether an error comes from the context instance and not from the item instances.
1086
+ *
1087
+ * @internal
1088
+ */
1089
+ _isErrorComingFromThisItem(error) {
1090
+ for (const watchdog of this._watchdogs.values()) if (watchdog._isErrorComingFromThisItem(error)) return false;
1091
+ return areConnectedThroughProperties(this._context, error.context);
1092
+ }
1093
+ };
1094
+ /**
1095
+ * Manager of action queues that allows queuing async functions.
1096
+ */
1097
+ var ActionQueues = class {
1098
+ _onEmptyCallbacks = [];
1099
+ _queues = /* @__PURE__ */ new Map();
1100
+ _activeActions = 0;
1101
+ /**
1102
+ * Used to register callbacks that will be run when the queue becomes empty.
1103
+ *
1104
+ * @param onEmptyCallback A callback that will be run whenever the queue becomes empty.
1105
+ */
1106
+ onEmpty(onEmptyCallback) {
1107
+ this._onEmptyCallbacks.push(onEmptyCallback);
1108
+ }
1109
+ /**
1110
+ * It adds asynchronous actions (functions) to the proper queue and runs them one by one.
1111
+ *
1112
+ * @param queueId The action queue ID.
1113
+ * @param action A function that should be enqueued.
1114
+ */
1115
+ enqueue(queueId, action) {
1116
+ const isMainAction = queueId === mainQueueId;
1117
+ this._activeActions++;
1118
+ if (!this._queues.get(queueId)) this._queues.set(queueId, Promise.resolve());
1119
+ const queueWithAction = (isMainAction ? Promise.all(this._queues.values()) : Promise.all([this._queues.get(mainQueueId), this._queues.get(queueId)])).then(action);
1120
+ const nonErrorQueue = queueWithAction.catch(() => {});
1121
+ this._queues.set(queueId, nonErrorQueue);
1122
+ return queueWithAction.finally(() => {
1123
+ this._activeActions--;
1124
+ if (this._queues.get(queueId) === nonErrorQueue && this._activeActions === 0) this._onEmptyCallbacks.forEach((cb) => cb());
1125
+ });
1126
+ }
1127
+ };
1128
+ /**
1129
+ * Transforms any value to an array. If the provided value is already an array, it is returned unchanged.
1130
+ *
1131
+ * @param elementOrArray The value to transform to an array.
1132
+ * @returns An array created from data.
1133
+ */
1134
+ function toArray(elementOrArray) {
1135
+ return Array.isArray(elementOrArray) ? elementOrArray : [elementOrArray];
1280
1136
  }
1281
1137
 
1282
1138
  /**
1283
- * A plugin that records user actions and editor state changes for debugging purposes. It tracks commands execution, model operations,
1284
- * UI interactions, and document events. It just collects data locally, and does not send it anywhere, integrator is responsible
1285
- * for gathering data from this plugin for further processing.
1286
- *
1287
- * **Important! `ActionsRecorder` is an experimental feature, and may become deprecated.**
1288
- *
1289
- * By default, plugin stores latest 1000 action entries. Integrator can register an `onError` callback to collect those entries
1290
- * in case of exception. Integrator should augment this data with application specific data such as page-id or session-id,
1291
- * depending on the application. Augmented data should be processed by the integrator, for example integrator should send it
1292
- * to some data collecting endpoint for later analysis.
1293
- *
1294
- * Example:
1295
- *
1296
- * ```ts
1297
- * ClassicEditor
1298
- * .create( {
1299
- * plugins: [ ActionsRecorder, ... ],
1300
- * actionsRecorder: {
1301
- * maxEntries: 1000, // This is the default value and could be adjusted.
1302
- *
1303
- * onError( error, entries ) {
1304
- * console.error( 'ActionsRecorder - Error detected:', error );
1305
- * console.warn( 'Actions recorded before error:', entries );
1306
- *
1307
- * this.flushEntries();
1308
- *
1309
- * // Integrator should send and store the entries. The error is already in the last entry in serializable form.
1310
- * }
1311
- * }
1312
- * } )
1313
- * .then( ... )
1314
- * .catch( ... );
1315
- * ```
1316
- *
1317
- * Alternatively integrator could continuously collect actions in batches and send them to theirs endpoint for later analysis:
1318
- *
1319
- * ```ts
1320
- * ClassicEditor
1321
- * .create( {
1322
- * plugins: [ ActionsRecorder, ... ],
1323
- * actionsRecorder: {
1324
- * maxEntries: 50, // This is the batch size.
1325
- *
1326
- * onMaxEntries() {
1327
- * const entries = this.getEntries();
1328
- *
1329
- * this.flushEntries();
1330
- *
1331
- * console.log( 'ActionsRecorder - Batch of entries:', entries );
1332
- *
1333
- * // Integrator should send and store the entries.
1334
- * },
1335
- *
1336
- * onError( error, entries ) {
1337
- * console.error( 'ActionsRecorder - Error detected:', error );
1338
- * console.warn( 'Actions recorded before error:', entries );
1339
- *
1340
- * this.flushEntries();
1341
- *
1342
- * // Integrator should send and store the entries. The error is already in the last entry in serializable form.
1343
- * }
1344
- * }
1345
- * } )
1346
- * .then( ... )
1347
- * .catch( ... );
1348
- * ```
1349
- *
1350
- * See {@link module:watchdog/actionsrecorderconfig~ActionsRecorderConfig plugin configuration} for more details.
1351
- *
1352
- */ class ActionsRecorder {
1353
- /**
1354
- * The editor instance.
1355
- */ editor;
1356
- /**
1357
- * Array storing all recorded action entries with their context and state snapshots.
1358
- */ _entries = [];
1359
- /**
1360
- * Stack tracking nested action frames to maintain call hierarchy.
1361
- */ _frameStack = [];
1362
- /**
1363
- * Set of already reported errors used to notify only once for each error (not on every try-catch nested block).
1364
- */ _errors = new Set();
1365
- /**
1366
- * Maximum number of action entries to keep in memory.
1367
- */ _maxEntries;
1368
- /**
1369
- * Error callback.
1370
- */ _errorCallback;
1371
- /**
1372
- * Filter function to determine which entries should be stored.
1373
- */ _filterCallback;
1374
- /**
1375
- * Callback triggered every time count of recorded entries reaches maxEntries.
1376
- */ _maxEntriesCallback;
1377
- /**
1378
- * @inheritDoc
1379
- */ static get pluginName() {
1380
- return 'ActionsRecorder';
1381
- }
1382
- /**
1383
- * @inheritDoc
1384
- */ static get isOfficialPlugin() {
1385
- return true;
1386
- }
1387
- /**
1388
- * @inheritDoc
1389
- */ constructor(editor){
1390
- this.editor = editor;
1391
- editor.config.define('actionsRecorder.maxEntries', 1000);
1392
- const config = editor.config.get('actionsRecorder');
1393
- this._maxEntries = config.maxEntries;
1394
- this._filterCallback = config.onFilter;
1395
- this._errorCallback = config.onError;
1396
- this._maxEntriesCallback = config.onMaxEntries || this._maxEntriesDefaultHandler;
1397
- this._tapCommands();
1398
- this._tapOperationApply();
1399
- this._tapModelMethods();
1400
- this._tapModelSelection();
1401
- this._tapComponentFactory();
1402
- this._tapViewDocumentEvents();
1403
- }
1404
- /**
1405
- * Returns all recorded action entries.
1406
- */ getEntries() {
1407
- // Return a shallow copy instead of reference as this array could be modified.
1408
- return this._entries.slice();
1409
- }
1410
- /**
1411
- * Flushes all recorded entries.
1412
- */ flushEntries() {
1413
- this._entries = [];
1414
- }
1415
- /**
1416
- * Creates a new action frame and adds it to the recording stack.
1417
- *
1418
- * @param action The name/type of the action being recorded.
1419
- * @param params Optional parameters associated with the event.
1420
- * @returns The created call frame object.
1421
- */ _enterFrame(action, params) {
1422
- const callFrame = {
1423
- timeStamp: new Date().toISOString(),
1424
- ...this._frameStack.length && {
1425
- parentEntry: this._frameStack.at(-1)
1426
- },
1427
- action,
1428
- params: params?.map((param)=>serializeValue(param)),
1429
- before: this._buildStateSnapshot()
1430
- };
1431
- // Apply filter if configured, only add to entries if filter passes.
1432
- if (!this._filterCallback || this._filterCallback(callFrame, this._entries)) {
1433
- // Add the call frame to the entries.
1434
- this._entries.push(callFrame);
1435
- }
1436
- this._frameStack.push(callFrame);
1437
- return callFrame;
1438
- }
1439
- /**
1440
- * Closes an action frame and records its final state and results.
1441
- *
1442
- * @param callFrame The call frame to close.
1443
- * @param result Optional result value from the action.
1444
- * @param error Optional error that occurred during the action.
1445
- */ _leaveFrame(callFrame, result, error) {
1446
- const topFrame = this._frameStack.pop();
1447
- // Handle scenario when the stack has been cleared in the meantime
1448
- // or the callFrame is not the top frame.
1449
- if (!topFrame || topFrame !== callFrame) {
1450
- return;
1451
- }
1452
- if (result !== undefined) {
1453
- topFrame.result = serializeValue(result);
1454
- }
1455
- if (error) {
1456
- topFrame.error = serializeValue(error);
1457
- }
1458
- topFrame.after = this._buildStateSnapshot();
1459
- if (error) {
1460
- this._callErrorCallback(error);
1461
- }
1462
- if (this._frameStack.length == 0) {
1463
- this._errors.clear();
1464
- }
1465
- // Enforce max entries limit after leaving the frame so that complete entry is provided.
1466
- if (this._entries.length >= this._maxEntries) {
1467
- this._maxEntriesCallback();
1468
- }
1469
- }
1470
- /**
1471
- * Builds a snapshot of the current editor state including document version,
1472
- * read-only status, focus state, and model selection.
1473
- *
1474
- * @returns An object containing the current editor state snapshot.
1475
- */ _buildStateSnapshot() {
1476
- const { model, isReadOnly, editing } = this.editor;
1477
- return {
1478
- documentVersion: model.document.version,
1479
- editorReadOnly: isReadOnly,
1480
- editorFocused: editing.view.document.isFocused,
1481
- modelSelection: serializeValue(model.document.selection)
1482
- };
1483
- }
1484
- /**
1485
- * Sets up recording for all editor commands, both existing and future ones.
1486
- * Taps into the command execution to track when commands are run.
1487
- */ _tapCommands() {
1488
- // Tap already registered commands.
1489
- for (const [commandName, command] of this.editor.commands){
1490
- this._tapCommand(commandName, command);
1491
- }
1492
- // Tap commands registered after the constructor was called.
1493
- tapObjectMethod(this.editor.commands, 'add', {
1494
- before: (callContext, [commandName, command])=>{
1495
- this._tapCommand(commandName, command);
1496
- return false;
1497
- }
1498
- });
1499
- }
1500
- /**
1501
- * Sets up recording for model operation applications.
1502
- * Tracks when operations are applied to the model document.
1503
- */ _tapOperationApply() {
1504
- tapObjectMethod(this.editor.model, 'applyOperation', {
1505
- before: (callContext, [operation])=>{
1506
- // Ignore operations applied to document fragments.
1507
- if (operation.baseVersion === null) {
1508
- return false;
1509
- }
1510
- callContext.callFrame = this._enterFrame('model.applyOperation', [
1511
- operation
1512
- ]);
1513
- return true;
1514
- },
1515
- after: (callContext)=>{
1516
- this._leaveFrame(callContext.callFrame);
1517
- },
1518
- error: (callContext, error)=>{
1519
- this._leaveFrame(callContext.callFrame, undefined, error);
1520
- }
1521
- });
1522
- }
1523
- /**
1524
- * Sets up recording for key model methods like insertContent, insertObject, and deleteContent.
1525
- * These methods represent high-level model manipulation operations.
1526
- */ _tapModelMethods() {
1527
- for (const methodName of [
1528
- 'insertContent',
1529
- 'insertObject',
1530
- 'deleteContent'
1531
- ]){
1532
- tapObjectMethod(this.editor.model, methodName, {
1533
- before: (callContext, ...params)=>{
1534
- callContext.callFrame = this._enterFrame(`model.${methodName}`, params);
1535
- return true;
1536
- },
1537
- after: (callContext, result)=>{
1538
- this._leaveFrame(callContext.callFrame, result);
1539
- },
1540
- error: (callContext, error)=>{
1541
- this._leaveFrame(callContext.callFrame, undefined, error);
1542
- }
1543
- });
1544
- }
1545
- }
1546
- /**
1547
- * Sets up recording for model selection changes.
1548
- * Tracks when the selection range, attributes, or markers change.
1549
- */ _tapModelSelection() {
1550
- const events = [
1551
- 'change:range',
1552
- 'change:attribute',
1553
- 'change:marker'
1554
- ];
1555
- this._tapFireMethod(this.editor.model.document.selection, events, {
1556
- eventSource: 'model-selection'
1557
- });
1558
- }
1559
- /**
1560
- * Sets up recording for a specific command execution.
1561
- *
1562
- * @param commandName The name of the command to record.
1563
- * @param command The command instance to tap into.
1564
- */ _tapCommand(commandName, command) {
1565
- tapObjectMethod(command, 'execute', {
1566
- before: (callContext, params)=>{
1567
- callContext.callFrame = this._enterFrame(`commands.${commandName}:execute`, params);
1568
- return true;
1569
- },
1570
- after: (callContext, result)=>{
1571
- this._leaveFrame(callContext.callFrame, result);
1572
- },
1573
- error: (callContext, error)=>{
1574
- this._leaveFrame(callContext.callFrame, undefined, error);
1575
- }
1576
- });
1577
- }
1578
- /**
1579
- * Sets up recording for UI component factory creation and component interactions.
1580
- * Tracks when components are created and their execute events.
1581
- */ _tapComponentFactory() {
1582
- tapObjectMethod(this.editor.ui.componentFactory, 'create', {
1583
- before: (callContext, [componentName])=>{
1584
- callContext.componentName = componentName;
1585
- callContext.callFrame = this._enterFrame(`component-factory.create:${componentName}`);
1586
- return true;
1587
- },
1588
- after: (callContext, componentInstance)=>{
1589
- const executeContext = {
1590
- ...callContext,
1591
- eventSource: `component.${callContext.componentName}`
1592
- };
1593
- if (typeof componentInstance.fire == 'function') {
1594
- this._tapFireMethod(componentInstance, [
1595
- 'execute'
1596
- ], executeContext);
1597
- }
1598
- if (typeof componentInstance.panelView?.fire == 'function') {
1599
- this._tapFireMethod(componentInstance.panelView, [
1600
- 'execute'
1601
- ], executeContext);
1602
- }
1603
- if (typeof componentInstance.buttonView?.actionView?.fire == 'function') {
1604
- this._tapFireMethod(componentInstance.buttonView.actionView, [
1605
- 'execute'
1606
- ], executeContext);
1607
- }
1608
- this._leaveFrame(callContext.callFrame);
1609
- },
1610
- error: (callContext, error)=>{
1611
- this._leaveFrame(callContext.callFrame, undefined, error);
1612
- }
1613
- });
1614
- }
1615
- /**
1616
- * Sets up recording for view document events like clicks, keyboard input,
1617
- * selection changes, and other user interactions.
1618
- */ _tapViewDocumentEvents() {
1619
- const events = [
1620
- 'click',
1621
- 'mousedown',
1622
- 'mouseup',
1623
- 'pointerdown',
1624
- 'pointerup',
1625
- 'focus',
1626
- 'blur',
1627
- 'keydown',
1628
- 'keyup',
1629
- 'selectionChange',
1630
- 'compositionstart',
1631
- 'compositionend',
1632
- 'beforeinput',
1633
- 'mutations',
1634
- 'enter',
1635
- 'delete',
1636
- 'insertText',
1637
- 'paste',
1638
- 'copy',
1639
- 'cut',
1640
- 'dragstart',
1641
- 'drop'
1642
- ];
1643
- this._tapFireMethod(this.editor.editing.view.document, events, {
1644
- eventSource: 'observers'
1645
- });
1646
- }
1647
- /**
1648
- * Sets up recording for specific events fired by an emitter object.
1649
- *
1650
- * @param emitter The object that fires events to be recorded.
1651
- * @param eventNames Array of event names to record.
1652
- * @param context Additional context to include with recorded events.
1653
- */ _tapFireMethod(emitter, eventNames, context = {}) {
1654
- tapObjectMethod(emitter, 'fire', {
1655
- before: (callContext, [eventInfoOrName, ...params])=>{
1656
- const eventName = typeof eventInfoOrName == 'string' ? eventInfoOrName : eventInfoOrName.name;
1657
- if (!eventNames.includes(eventName)) {
1658
- return false;
1659
- }
1660
- callContext.callFrame = this._enterFrame(`${callContext.eventSource}:${eventName}`, params);
1661
- return true;
1662
- },
1663
- after: (callContext, result)=>{
1664
- this._leaveFrame(callContext.callFrame, result);
1665
- },
1666
- error: (callContext, error)=>{
1667
- this._leaveFrame(callContext.callFrame, undefined, error);
1668
- }
1669
- }, context);
1670
- }
1671
- /**
1672
- * Triggers error callback.
1673
- */ _callErrorCallback(error) {
1674
- if (!this._errorCallback || this._errors.has(error)) {
1675
- return;
1676
- }
1677
- this._errors.add(error);
1678
- try {
1679
- // Provide a shallow copy of entries as it might be modified before error handler serializes it.
1680
- this._errorCallback(error, this.getEntries());
1681
- } catch (observerError) {
1682
- // Silently catch observer errors to prevent them from affecting the recording.
1683
- console.error('ActionsRecorder onError callback error:', observerError);
1684
- }
1685
- }
1686
- /**
1687
- * The default handler for maxEntries callback.
1688
- */ _maxEntriesDefaultHandler() {
1689
- this._entries.shift();
1690
- }
1691
- }
1139
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
1140
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
1141
+ */
1692
1142
  /**
1693
- * Creates a wrapper around a method to record its calls, results, and errors.
1694
- *
1695
- * @internal
1696
- *
1697
- * @param object The object containing the method to tap.
1698
- * @param methodName The name of the method to tap.
1699
- * @param tap The tap configuration with before/after/error hooks.
1700
- * @param context Additional context to include with the method calls.
1701
- */ function tapObjectMethod(object, methodName, tap, context = {}) {
1702
- const originalMethod = object[methodName];
1703
- if (originalMethod[Symbol.for('Tapped method')]) {
1704
- return;
1705
- }
1706
- object[methodName] = (...args)=>{
1707
- const callContext = Object.assign({}, context);
1708
- let shouldHandle;
1709
- try {
1710
- shouldHandle = tap.before?.(callContext, args);
1711
- const result = originalMethod.apply(object, args);
1712
- if (shouldHandle) {
1713
- tap.after?.(callContext, result);
1714
- }
1715
- return result;
1716
- } catch (error) {
1717
- if (shouldHandle) {
1718
- tap.error?.(callContext, error);
1719
- }
1720
- throw error;
1721
- }
1722
- };
1723
- object[methodName][Symbol.for('Tapped method')] = originalMethod;
1143
+ * A plugin that records user actions and editor state changes for debugging purposes. It tracks commands execution, model operations,
1144
+ * UI interactions, and document events. It just collects data locally, and does not send it anywhere, integrator is responsible
1145
+ * for gathering data from this plugin for further processing.
1146
+ *
1147
+ * **Important! `ActionsRecorder` is an experimental feature, and may become deprecated.**
1148
+ *
1149
+ * By default, plugin stores latest 1000 action entries. Integrator can register an `onError` callback to collect those entries
1150
+ * in case of exception. Integrator should augment this data with application specific data such as page-id or session-id,
1151
+ * depending on the application. Augmented data should be processed by the integrator, for example integrator should send it
1152
+ * to some data collecting endpoint for later analysis.
1153
+ *
1154
+ * Example:
1155
+ *
1156
+ * ```ts
1157
+ * ClassicEditor
1158
+ * .create( {
1159
+ * plugins: [ ActionsRecorder, ... ],
1160
+ * actionsRecorder: {
1161
+ * maxEntries: 1000, // This is the default value and could be adjusted.
1162
+ *
1163
+ * onError( error, entries ) {
1164
+ * console.error( 'ActionsRecorder - Error detected:', error );
1165
+ * console.warn( 'Actions recorded before error:', entries );
1166
+ *
1167
+ * this.flushEntries();
1168
+ *
1169
+ * // Integrator should send and store the entries. The error is already in the last entry in serializable form.
1170
+ * }
1171
+ * }
1172
+ * } )
1173
+ * .then( ... )
1174
+ * .catch( ... );
1175
+ * ```
1176
+ *
1177
+ * Alternatively integrator could continuously collect actions in batches and send them to theirs endpoint for later analysis:
1178
+ *
1179
+ * ```ts
1180
+ * ClassicEditor
1181
+ * .create( {
1182
+ * plugins: [ ActionsRecorder, ... ],
1183
+ * actionsRecorder: {
1184
+ * maxEntries: 50, // This is the batch size.
1185
+ *
1186
+ * onMaxEntries() {
1187
+ * const entries = this.getEntries();
1188
+ *
1189
+ * this.flushEntries();
1190
+ *
1191
+ * console.log( 'ActionsRecorder - Batch of entries:', entries );
1192
+ *
1193
+ * // Integrator should send and store the entries.
1194
+ * },
1195
+ *
1196
+ * onError( error, entries ) {
1197
+ * console.error( 'ActionsRecorder - Error detected:', error );
1198
+ * console.warn( 'Actions recorded before error:', entries );
1199
+ *
1200
+ * this.flushEntries();
1201
+ *
1202
+ * // Integrator should send and store the entries. The error is already in the last entry in serializable form.
1203
+ * }
1204
+ * }
1205
+ * } )
1206
+ * .then( ... )
1207
+ * .catch( ... );
1208
+ * ```
1209
+ *
1210
+ * See {@link module:watchdog/actionsrecorderconfig~ActionsRecorderConfig plugin configuration} for more details.
1211
+ *
1212
+ */
1213
+ var ActionsRecorder = class {
1214
+ /**
1215
+ * The editor instance.
1216
+ */
1217
+ editor;
1218
+ /**
1219
+ * Array storing all recorded action entries with their context and state snapshots.
1220
+ */
1221
+ _entries = [];
1222
+ /**
1223
+ * Stack tracking nested action frames to maintain call hierarchy.
1224
+ */
1225
+ _frameStack = [];
1226
+ /**
1227
+ * Set of already reported errors used to notify only once for each error (not on every try-catch nested block).
1228
+ */
1229
+ _errors = /* @__PURE__ */ new Set();
1230
+ /**
1231
+ * Maximum number of action entries to keep in memory.
1232
+ */
1233
+ _maxEntries;
1234
+ /**
1235
+ * Error callback.
1236
+ */
1237
+ _errorCallback;
1238
+ /**
1239
+ * Filter function to determine which entries should be stored.
1240
+ */
1241
+ _filterCallback;
1242
+ /**
1243
+ * Callback triggered every time count of recorded entries reaches maxEntries.
1244
+ */
1245
+ _maxEntriesCallback;
1246
+ /**
1247
+ * @inheritDoc
1248
+ */
1249
+ static get pluginName() {
1250
+ return "ActionsRecorder";
1251
+ }
1252
+ /**
1253
+ * @inheritDoc
1254
+ */
1255
+ static get isOfficialPlugin() {
1256
+ return true;
1257
+ }
1258
+ /**
1259
+ * @inheritDoc
1260
+ */
1261
+ constructor(editor) {
1262
+ this.editor = editor;
1263
+ editor.config.define("actionsRecorder.maxEntries", 1e3);
1264
+ const config = editor.config.get("actionsRecorder");
1265
+ this._maxEntries = config.maxEntries;
1266
+ this._filterCallback = config.onFilter;
1267
+ this._errorCallback = config.onError;
1268
+ this._maxEntriesCallback = config.onMaxEntries || this._maxEntriesDefaultHandler;
1269
+ this._tapCommands();
1270
+ this._tapOperationApply();
1271
+ this._tapModelMethods();
1272
+ this._tapModelSelection();
1273
+ this._tapComponentFactory();
1274
+ this._tapViewDocumentEvents();
1275
+ }
1276
+ /**
1277
+ * Returns all recorded action entries.
1278
+ */
1279
+ getEntries() {
1280
+ return this._entries.slice();
1281
+ }
1282
+ /**
1283
+ * Flushes all recorded entries.
1284
+ */
1285
+ flushEntries() {
1286
+ this._entries = [];
1287
+ }
1288
+ /**
1289
+ * Creates a new action frame and adds it to the recording stack.
1290
+ *
1291
+ * @param action The name/type of the action being recorded.
1292
+ * @param params Optional parameters associated with the event.
1293
+ * @returns The created call frame object.
1294
+ */
1295
+ _enterFrame(action, params) {
1296
+ const callFrame = {
1297
+ timeStamp: (/* @__PURE__ */ new Date()).toISOString(),
1298
+ ...this._frameStack.length && { parentEntry: this._frameStack.at(-1) },
1299
+ action,
1300
+ params: params?.map((param) => serializeValue(param)),
1301
+ before: this._buildStateSnapshot()
1302
+ };
1303
+ if (!this._filterCallback || this._filterCallback(callFrame, this._entries)) this._entries.push(callFrame);
1304
+ this._frameStack.push(callFrame);
1305
+ return callFrame;
1306
+ }
1307
+ /**
1308
+ * Closes an action frame and records its final state and results.
1309
+ *
1310
+ * @param callFrame The call frame to close.
1311
+ * @param result Optional result value from the action.
1312
+ * @param error Optional error that occurred during the action.
1313
+ */
1314
+ _leaveFrame(callFrame, result, error) {
1315
+ const topFrame = this._frameStack.pop();
1316
+ if (!topFrame || topFrame !== callFrame) return;
1317
+ if (result !== void 0) topFrame.result = serializeValue(result);
1318
+ if (error) topFrame.error = serializeValue(error);
1319
+ topFrame.after = this._buildStateSnapshot();
1320
+ if (error) this._callErrorCallback(error);
1321
+ if (this._frameStack.length == 0) this._errors.clear();
1322
+ if (this._entries.length >= this._maxEntries) this._maxEntriesCallback();
1323
+ }
1324
+ /**
1325
+ * Builds a snapshot of the current editor state including document version,
1326
+ * read-only status, focus state, and model selection.
1327
+ *
1328
+ * @returns An object containing the current editor state snapshot.
1329
+ */
1330
+ _buildStateSnapshot() {
1331
+ const { model, isReadOnly, editing } = this.editor;
1332
+ return {
1333
+ documentVersion: model.document.version,
1334
+ editorReadOnly: isReadOnly,
1335
+ editorFocused: editing.view.document.isFocused,
1336
+ modelSelection: serializeValue(model.document.selection)
1337
+ };
1338
+ }
1339
+ /**
1340
+ * Sets up recording for all editor commands, both existing and future ones.
1341
+ * Taps into the command execution to track when commands are run.
1342
+ */
1343
+ _tapCommands() {
1344
+ for (const [commandName, command] of this.editor.commands) this._tapCommand(commandName, command);
1345
+ tapObjectMethod(this.editor.commands, "add", { before: (callContext, [commandName, command]) => {
1346
+ this._tapCommand(commandName, command);
1347
+ return false;
1348
+ } });
1349
+ }
1350
+ /**
1351
+ * Sets up recording for model operation applications.
1352
+ * Tracks when operations are applied to the model document.
1353
+ */
1354
+ _tapOperationApply() {
1355
+ tapObjectMethod(this.editor.model, "applyOperation", {
1356
+ before: (callContext, [operation]) => {
1357
+ if (operation.baseVersion === null) return false;
1358
+ callContext.callFrame = this._enterFrame("model.applyOperation", [operation]);
1359
+ return true;
1360
+ },
1361
+ after: (callContext) => {
1362
+ this._leaveFrame(callContext.callFrame);
1363
+ },
1364
+ error: (callContext, error) => {
1365
+ this._leaveFrame(callContext.callFrame, void 0, error);
1366
+ }
1367
+ });
1368
+ }
1369
+ /**
1370
+ * Sets up recording for key model methods like insertContent, insertObject, and deleteContent.
1371
+ * These methods represent high-level model manipulation operations.
1372
+ */
1373
+ _tapModelMethods() {
1374
+ for (const methodName of [
1375
+ "insertContent",
1376
+ "insertObject",
1377
+ "deleteContent"
1378
+ ]) tapObjectMethod(this.editor.model, methodName, {
1379
+ before: (callContext, ...params) => {
1380
+ callContext.callFrame = this._enterFrame(`model.${methodName}`, params);
1381
+ return true;
1382
+ },
1383
+ after: (callContext, result) => {
1384
+ this._leaveFrame(callContext.callFrame, result);
1385
+ },
1386
+ error: (callContext, error) => {
1387
+ this._leaveFrame(callContext.callFrame, void 0, error);
1388
+ }
1389
+ });
1390
+ }
1391
+ /**
1392
+ * Sets up recording for model selection changes.
1393
+ * Tracks when the selection range, attributes, or markers change.
1394
+ */
1395
+ _tapModelSelection() {
1396
+ this._tapFireMethod(this.editor.model.document.selection, [
1397
+ "change:range",
1398
+ "change:attribute",
1399
+ "change:marker"
1400
+ ], { eventSource: "model-selection" });
1401
+ }
1402
+ /**
1403
+ * Sets up recording for a specific command execution.
1404
+ *
1405
+ * @param commandName The name of the command to record.
1406
+ * @param command The command instance to tap into.
1407
+ */
1408
+ _tapCommand(commandName, command) {
1409
+ tapObjectMethod(command, "execute", {
1410
+ before: (callContext, params) => {
1411
+ callContext.callFrame = this._enterFrame(`commands.${commandName}:execute`, params);
1412
+ return true;
1413
+ },
1414
+ after: (callContext, result) => {
1415
+ this._leaveFrame(callContext.callFrame, result);
1416
+ },
1417
+ error: (callContext, error) => {
1418
+ this._leaveFrame(callContext.callFrame, void 0, error);
1419
+ }
1420
+ });
1421
+ }
1422
+ /**
1423
+ * Sets up recording for UI component factory creation and component interactions.
1424
+ * Tracks when components are created and their execute events.
1425
+ */
1426
+ _tapComponentFactory() {
1427
+ tapObjectMethod(this.editor.ui.componentFactory, "create", {
1428
+ before: (callContext, [componentName]) => {
1429
+ callContext.componentName = componentName;
1430
+ callContext.callFrame = this._enterFrame(`component-factory.create:${componentName}`);
1431
+ return true;
1432
+ },
1433
+ after: (callContext, componentInstance) => {
1434
+ const executeContext = {
1435
+ ...callContext,
1436
+ eventSource: `component.${callContext.componentName}`
1437
+ };
1438
+ if (typeof componentInstance.fire == "function") this._tapFireMethod(componentInstance, ["execute"], executeContext);
1439
+ if (typeof componentInstance.panelView?.fire == "function") this._tapFireMethod(componentInstance.panelView, ["execute"], executeContext);
1440
+ if (typeof componentInstance.buttonView?.actionView?.fire == "function") this._tapFireMethod(componentInstance.buttonView.actionView, ["execute"], executeContext);
1441
+ this._leaveFrame(callContext.callFrame);
1442
+ },
1443
+ error: (callContext, error) => {
1444
+ this._leaveFrame(callContext.callFrame, void 0, error);
1445
+ }
1446
+ });
1447
+ }
1448
+ /**
1449
+ * Sets up recording for view document events like clicks, keyboard input,
1450
+ * selection changes, and other user interactions.
1451
+ */
1452
+ _tapViewDocumentEvents() {
1453
+ this._tapFireMethod(this.editor.editing.view.document, [
1454
+ "click",
1455
+ "mousedown",
1456
+ "mouseup",
1457
+ "pointerdown",
1458
+ "pointerup",
1459
+ "focus",
1460
+ "blur",
1461
+ "keydown",
1462
+ "keyup",
1463
+ "selectionChange",
1464
+ "compositionstart",
1465
+ "compositionend",
1466
+ "beforeinput",
1467
+ "mutations",
1468
+ "enter",
1469
+ "delete",
1470
+ "insertText",
1471
+ "paste",
1472
+ "copy",
1473
+ "cut",
1474
+ "dragstart",
1475
+ "drop"
1476
+ ], { eventSource: "observers" });
1477
+ }
1478
+ /**
1479
+ * Sets up recording for specific events fired by an emitter object.
1480
+ *
1481
+ * @param emitter The object that fires events to be recorded.
1482
+ * @param eventNames Array of event names to record.
1483
+ * @param context Additional context to include with recorded events.
1484
+ */
1485
+ _tapFireMethod(emitter, eventNames, context = {}) {
1486
+ tapObjectMethod(emitter, "fire", {
1487
+ before: (callContext, [eventInfoOrName, ...params]) => {
1488
+ const eventName = typeof eventInfoOrName == "string" ? eventInfoOrName : eventInfoOrName.name;
1489
+ if (!eventNames.includes(eventName)) return false;
1490
+ callContext.callFrame = this._enterFrame(`${callContext.eventSource}:${eventName}`, params);
1491
+ return true;
1492
+ },
1493
+ after: (callContext, result) => {
1494
+ this._leaveFrame(callContext.callFrame, result);
1495
+ },
1496
+ error: (callContext, error) => {
1497
+ this._leaveFrame(callContext.callFrame, void 0, error);
1498
+ }
1499
+ }, context);
1500
+ }
1501
+ /**
1502
+ * Triggers error callback.
1503
+ */
1504
+ _callErrorCallback(error) {
1505
+ if (!this._errorCallback || this._errors.has(error)) return;
1506
+ this._errors.add(error);
1507
+ try {
1508
+ this._errorCallback(error, this.getEntries());
1509
+ } catch (observerError) {
1510
+ console.error("ActionsRecorder onError callback error:", observerError);
1511
+ }
1512
+ }
1513
+ /**
1514
+ * The default handler for maxEntries callback.
1515
+ */
1516
+ _maxEntriesDefaultHandler() {
1517
+ this._entries.shift();
1518
+ }
1519
+ };
1520
+ /**
1521
+ * Creates a wrapper around a method to record its calls, results, and errors.
1522
+ *
1523
+ * @internal
1524
+ *
1525
+ * @param object The object containing the method to tap.
1526
+ * @param methodName The name of the method to tap.
1527
+ * @param tap The tap configuration with before/after/error hooks.
1528
+ * @param context Additional context to include with the method calls.
1529
+ */
1530
+ function tapObjectMethod(object, methodName, tap, context = {}) {
1531
+ const originalMethod = object[methodName];
1532
+ if (originalMethod[Symbol.for("Tapped method")]) return;
1533
+ object[methodName] = (...args) => {
1534
+ const callContext = Object.assign({}, context);
1535
+ let shouldHandle;
1536
+ try {
1537
+ shouldHandle = tap.before?.(callContext, args);
1538
+ const result = originalMethod.apply(object, args);
1539
+ if (shouldHandle) tap.after?.(callContext, result);
1540
+ return result;
1541
+ } catch (error) {
1542
+ if (shouldHandle) tap.error?.(callContext, error);
1543
+ throw error;
1544
+ }
1545
+ };
1546
+ object[methodName][Symbol.for("Tapped method")] = originalMethod;
1724
1547
  }
1725
1548
  /**
1726
- * Serializes a value into a JSON-serializable format.
1727
- *
1728
- * @internal
1729
- *
1730
- * @param value The value to serialize.
1731
- * @param visited Set of already serialized objects to avoid circular references.
1732
- * @returns A JSON-serializable representation of the value.
1733
- */ function serializeValue(value, visited = new WeakSet()) {
1734
- if (!value || [
1735
- 'boolean',
1736
- 'number',
1737
- 'string'
1738
- ].includes(typeof value)) {
1739
- return value;
1740
- }
1741
- if (typeof value.toJSON == 'function') {
1742
- const jsonData = value.toJSON();
1743
- // Make sure that toJSON returns plain object, otherwise it could be just a clone with circular references.
1744
- if (isPlainObject$1(jsonData) || Array.isArray(jsonData) || [
1745
- 'string',
1746
- 'number',
1747
- 'boolean'
1748
- ].includes(typeof jsonData)) {
1749
- return serializeValue(jsonData, visited);
1750
- }
1751
- }
1752
- if (value instanceof Error) {
1753
- return {
1754
- name: value.name,
1755
- message: value.message,
1756
- stack: value.stack
1757
- };
1758
- }
1759
- // Most TypeCheckable should implement toJSON method so this is a fallback for other TypeCheckable objects.
1760
- if (isTypeCheckable(value) || typeof value != 'object') {
1761
- return {
1762
- type: typeof value,
1763
- constructor: value.constructor?.name || 'unknown',
1764
- string: String(value)
1765
- };
1766
- }
1767
- if (value instanceof File || value instanceof Blob || value instanceof FormData || value instanceof DataTransfer) {
1768
- return String(value);
1769
- }
1770
- if (visited.has(value)) {
1771
- return;
1772
- }
1773
- visited.add(value);
1774
- // Arrays.
1775
- if (Array.isArray(value)) {
1776
- return value.length ? value.map((item)=>serializeValue(item, visited)) : undefined;
1777
- }
1778
- // Other objects (plain, instances of classes, or events).
1779
- const result = {};
1780
- const ignoreFields = [];
1781
- // DOM event additional fields.
1782
- if (value.domEvent) {
1783
- ignoreFields.push('domEvent', 'domTarget', 'view', 'document');
1784
- result.domEvent = serializeDomEvent(value.domEvent);
1785
- result.target = serializeValue(value.target);
1786
- if (value.dataTransfer) {
1787
- result.dataTransfer = {
1788
- types: value.dataTransfer.types,
1789
- htmlData: value.dataTransfer.getData('text/html'),
1790
- files: serializeValue(value.dataTransfer.files)
1791
- };
1792
- }
1793
- }
1794
- // Other object types.
1795
- for (const [key, val] of Object.entries(value)){
1796
- // Ignore private fields, DOM events serialized above, and decorated methods.
1797
- if (key.startsWith('_') || ignoreFields.includes(key) || typeof val == 'function') {
1798
- continue;
1799
- }
1800
- const serializedValue = serializeValue(val, visited);
1801
- if (serializedValue !== undefined) {
1802
- result[key] = serializedValue;
1803
- }
1804
- }
1805
- if (Symbol.iterator in value) {
1806
- const items = Array.from(value[Symbol.iterator]()).map((item)=>serializeValue(item, visited));
1807
- if (items.length) {
1808
- result._items = items;
1809
- }
1810
- }
1811
- return Object.keys(result).length ? result : undefined;
1549
+ * Serializes a value into a JSON-serializable format.
1550
+ *
1551
+ * @internal
1552
+ *
1553
+ * @param value The value to serialize.
1554
+ * @param visited Set of already serialized objects to avoid circular references.
1555
+ * @returns A JSON-serializable representation of the value.
1556
+ */
1557
+ function serializeValue(value, visited = /* @__PURE__ */ new WeakSet()) {
1558
+ if (!value || [
1559
+ "boolean",
1560
+ "number",
1561
+ "string"
1562
+ ].includes(typeof value)) return value;
1563
+ if (typeof value.toJSON == "function") {
1564
+ const jsonData = value.toJSON();
1565
+ if (isPlainObject(jsonData) || Array.isArray(jsonData) || [
1566
+ "string",
1567
+ "number",
1568
+ "boolean"
1569
+ ].includes(typeof jsonData)) return serializeValue(jsonData, visited);
1570
+ }
1571
+ if (value instanceof Error) return {
1572
+ name: value.name,
1573
+ message: value.message,
1574
+ stack: value.stack
1575
+ };
1576
+ if (isTypeCheckable(value) || typeof value != "object") return {
1577
+ type: typeof value,
1578
+ constructor: value.constructor?.name || "unknown",
1579
+ string: String(value)
1580
+ };
1581
+ if (value instanceof File || value instanceof Blob || value instanceof FormData || value instanceof DataTransfer) return String(value);
1582
+ if (visited.has(value)) return;
1583
+ visited.add(value);
1584
+ if (Array.isArray(value)) return value.length ? value.map((item) => serializeValue(item, visited)) : void 0;
1585
+ const result = {};
1586
+ const ignoreFields = [];
1587
+ if (value.domEvent) {
1588
+ ignoreFields.push("domEvent", "domTarget", "view", "document");
1589
+ result.domEvent = serializeDomEvent(value.domEvent);
1590
+ result.target = serializeValue(value.target);
1591
+ if (value.dataTransfer) result.dataTransfer = {
1592
+ types: value.dataTransfer.types,
1593
+ htmlData: value.dataTransfer.getData("text/html"),
1594
+ files: serializeValue(value.dataTransfer.files)
1595
+ };
1596
+ }
1597
+ for (const [key, val] of Object.entries(value)) {
1598
+ if (key.startsWith("_") || ignoreFields.includes(key) || typeof val == "function") continue;
1599
+ const serializedValue = serializeValue(val, visited);
1600
+ if (serializedValue !== void 0) result[key] = serializedValue;
1601
+ }
1602
+ if (Symbol.iterator in value) {
1603
+ const items = Array.from(value[Symbol.iterator]()).map((item) => serializeValue(item, visited));
1604
+ if (items.length) result._items = items;
1605
+ }
1606
+ return Object.keys(result).length ? result : void 0;
1812
1607
  }
1813
1608
  /**
1814
- * Serializes a DOM event into a plain object representation.
1815
- *
1816
- * Extracts common properties from DOM events such as type, target information,
1817
- * coordinates, key codes, and other relevant event data for debugging purposes.
1818
- *
1819
- * @param event The DOM event to serialize.
1820
- * @returns A serialized object containing the event's key properties.
1821
- */ function serializeDomEvent(event) {
1822
- let serialized = {
1823
- type: event.type,
1824
- target: serializeDOMTarget(event.target)
1825
- };
1826
- // Add mouse event properties.
1827
- if (event instanceof MouseEvent) {
1828
- serialized = {
1829
- ...serialized,
1830
- button: event.button,
1831
- buttons: event.buttons,
1832
- ctrlKey: event.ctrlKey,
1833
- shiftKey: event.shiftKey,
1834
- altKey: event.altKey,
1835
- metaKey: event.metaKey
1836
- };
1837
- }
1838
- // Add keyboard event properties.
1839
- if (event instanceof KeyboardEvent) {
1840
- serialized = {
1841
- ...serialized,
1842
- key: event.key,
1843
- code: event.code,
1844
- keyCode: event.keyCode,
1845
- ctrlKey: event.ctrlKey,
1846
- shiftKey: event.shiftKey,
1847
- altKey: event.altKey,
1848
- metaKey: event.metaKey,
1849
- repeat: event.repeat
1850
- };
1851
- }
1852
- // Add input event properties.
1853
- if (event instanceof InputEvent) {
1854
- serialized = {
1855
- ...serialized,
1856
- data: event.data,
1857
- inputType: event.inputType,
1858
- isComposing: event.isComposing
1859
- };
1860
- }
1861
- // Add pointer event properties.
1862
- if (event instanceof PointerEvent) {
1863
- serialized = {
1864
- ...serialized,
1865
- isPrimary: event.isPrimary
1866
- };
1867
- }
1868
- /**
1869
- * Serializes a DOM event target into a plain object representation.
1870
- *
1871
- * @param target The DOM event target to serialize.
1872
- * @returns A serialized object containing the target's information.
1873
- */ function serializeDOMTarget(target) {
1874
- if (!target) {
1875
- return null;
1876
- }
1877
- if (target instanceof Element) {
1878
- return {
1879
- tagName: target.tagName,
1880
- className: target.className,
1881
- id: target.id
1882
- };
1883
- }
1884
- if (target instanceof Window || target instanceof Document) {
1885
- return {
1886
- type: target.constructor.name
1887
- };
1888
- }
1889
- return {};
1890
- }
1891
- return serialized;
1609
+ * Serializes a DOM event into a plain object representation.
1610
+ *
1611
+ * Extracts common properties from DOM events such as type, target information,
1612
+ * coordinates, key codes, and other relevant event data for debugging purposes.
1613
+ *
1614
+ * @param event The DOM event to serialize.
1615
+ * @returns A serialized object containing the event's key properties.
1616
+ */
1617
+ function serializeDomEvent(event) {
1618
+ let serialized = {
1619
+ type: event.type,
1620
+ target: serializeDOMTarget(event.target)
1621
+ };
1622
+ if (event instanceof MouseEvent) serialized = {
1623
+ ...serialized,
1624
+ button: event.button,
1625
+ buttons: event.buttons,
1626
+ ctrlKey: event.ctrlKey,
1627
+ shiftKey: event.shiftKey,
1628
+ altKey: event.altKey,
1629
+ metaKey: event.metaKey
1630
+ };
1631
+ if (event instanceof KeyboardEvent) serialized = {
1632
+ ...serialized,
1633
+ key: event.key,
1634
+ code: event.code,
1635
+ keyCode: event.keyCode,
1636
+ ctrlKey: event.ctrlKey,
1637
+ shiftKey: event.shiftKey,
1638
+ altKey: event.altKey,
1639
+ metaKey: event.metaKey,
1640
+ repeat: event.repeat
1641
+ };
1642
+ if (event instanceof InputEvent) serialized = {
1643
+ ...serialized,
1644
+ data: event.data,
1645
+ inputType: event.inputType,
1646
+ isComposing: event.isComposing
1647
+ };
1648
+ if (event instanceof PointerEvent) serialized = {
1649
+ ...serialized,
1650
+ isPrimary: event.isPrimary
1651
+ };
1652
+ /**
1653
+ * Serializes a DOM event target into a plain object representation.
1654
+ *
1655
+ * @param target The DOM event target to serialize.
1656
+ * @returns A serialized object containing the target's information.
1657
+ */
1658
+ function serializeDOMTarget(target) {
1659
+ if (!target) return null;
1660
+ if (target instanceof Element) return {
1661
+ tagName: target.tagName,
1662
+ className: target.className,
1663
+ id: target.id
1664
+ };
1665
+ if (target instanceof Window || target instanceof Document) return { type: target.constructor.name };
1666
+ return {};
1667
+ }
1668
+ return serialized;
1892
1669
  }
1893
1670
  /**
1894
- * Checks if a value is type-checkable, meaning it has an `is` method.
1895
- */ function isTypeCheckable(value) {
1896
- return value && typeof value.is === 'function';
1671
+ * Checks if a value is type-checkable, meaning it has an `is` method.
1672
+ */
1673
+ function isTypeCheckable(value) {
1674
+ return value && typeof value.is === "function";
1897
1675
  }
1676
+ /* v8 ignore stop -- @preserve */
1677
+
1678
+ /**
1679
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
1680
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
1681
+ */
1898
1682
 
1899
1683
  export { ActionsRecorder, ContextWatchdog, EditorWatchdog, Watchdog };
1900
- //# sourceMappingURL=index.js.map
1684
+ //# sourceMappingURL=index.js.map