ckeditor5-blazor 1.9.0 → 1.10.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.
Files changed (46) hide show
  1. package/dist/elements/editable.d.ts +3 -11
  2. package/dist/elements/editable.d.ts.map +1 -1
  3. package/dist/elements/editor/editor.d.ts.map +1 -1
  4. package/dist/elements/editor/typings.d.ts +2 -1
  5. package/dist/elements/editor/typings.d.ts.map +1 -1
  6. package/dist/elements/editor/utils/cleanup-orphan-editor-elements.d.ts +8 -0
  7. package/dist/elements/editor/utils/cleanup-orphan-editor-elements.d.ts.map +1 -0
  8. package/dist/elements/editor/utils/create-editor-in-context.d.ts +6 -1
  9. package/dist/elements/editor/utils/create-editor-in-context.d.ts.map +1 -1
  10. package/dist/elements/editor/utils/index.d.ts +1 -0
  11. package/dist/elements/editor/utils/index.d.ts.map +1 -1
  12. package/dist/elements/editor/utils/wrap-with-watchdog.d.ts +7 -16
  13. package/dist/elements/editor/utils/wrap-with-watchdog.d.ts.map +1 -1
  14. package/dist/elements/ui-part.d.ts +3 -3
  15. package/dist/elements/ui-part.d.ts.map +1 -1
  16. package/dist/index.cjs +2 -2
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.mjs +459 -394
  19. package/dist/index.mjs.map +1 -1
  20. package/dist/interop/create-editable-blazor-interop.d.ts.map +1 -1
  21. package/dist/interop/create-editor-blazor-interop.d.ts.map +1 -1
  22. package/dist/shared/are-maps-equal.d.ts +11 -0
  23. package/dist/shared/are-maps-equal.d.ts.map +1 -0
  24. package/dist/shared/async-registry.d.ts +44 -16
  25. package/dist/shared/async-registry.d.ts.map +1 -1
  26. package/dist/shared/index.d.ts +1 -0
  27. package/dist/shared/index.d.ts.map +1 -1
  28. package/package.json +3 -3
  29. package/src/elements/editable.ts +38 -58
  30. package/src/elements/editor/editor.ts +122 -101
  31. package/src/elements/editor/typings.ts +3 -1
  32. package/src/elements/editor/utils/cleanup-orphan-editor-elements.test.ts +285 -0
  33. package/src/elements/editor/utils/cleanup-orphan-editor-elements.ts +60 -0
  34. package/src/elements/editor/utils/create-editor-in-context.ts +8 -2
  35. package/src/elements/editor/utils/index.ts +1 -0
  36. package/src/elements/editor/utils/wrap-with-watchdog.test.ts +34 -14
  37. package/src/elements/editor/utils/wrap-with-watchdog.ts +15 -25
  38. package/src/elements/ui-part.test.ts +1 -1
  39. package/src/elements/ui-part.ts +12 -11
  40. package/src/interop/create-editable-blazor-interop.ts +19 -16
  41. package/src/interop/create-editor-blazor-interop.ts +15 -18
  42. package/src/shared/are-maps-equal.test.ts +56 -0
  43. package/src/shared/are-maps-equal.ts +22 -0
  44. package/src/shared/async-registry.test.ts +190 -88
  45. package/src/shared/async-registry.ts +179 -107
  46. package/src/shared/index.ts +1 -0
@@ -1,3 +1,5 @@
1
+ import { areMapsEqual } from './are-maps-equal';
2
+
1
3
  /**
2
4
  * Generic async registry for objects with an async destroy method.
3
5
  * Provides a way to register, unregister, and execute callbacks on objects by ID.
@@ -23,6 +25,18 @@ export class AsyncRegistry<T extends Destructible> {
23
25
  */
24
26
  private readonly watchers = new Set<RegistryWatcher<T>>();
25
27
 
28
+ /**
29
+ * Batch nesting depth. When > 0, watcher notifications are deferred.
30
+ */
31
+ private batchDepth = 0;
32
+
33
+ /**
34
+ * Snapshot of the last state dispatched to watchers, used for change detection.
35
+ */
36
+ private lastNotifiedItems: Map<any, any> | null = null;
37
+
38
+ private lastNotifiedErrors: Map<any, any> | null = null;
39
+
26
40
  /**
27
41
  * Executes a function on an item.
28
42
  * If the item is not yet registered, it will wait for it to be registered.
@@ -68,6 +82,66 @@ export class AsyncRegistry<T extends Destructible> {
68
82
  });
69
83
  }
70
84
 
85
+ /**
86
+ * Reactively binds a mount/unmount lifecycle to a single registry item.
87
+ *
88
+ * @param id The ID of the item to observe.
89
+ * @param onMount Function executed when the item mounts.
90
+ * @returns A function that stops observing and immediately runs any pending cleanup.
91
+ */
92
+ mountEffect<E extends T = T>(
93
+ id: RegistryId | null,
94
+ onMount: (item: E) => (() => void) | void,
95
+ ): () => void {
96
+ let cleanup: VoidFunction | void;
97
+ let mountedItem: T | undefined;
98
+ let unmounted = false;
99
+
100
+ const unwatch = this.watch((items) => {
101
+ const item = items.get(id);
102
+
103
+ if (item === mountedItem) {
104
+ return;
105
+ }
106
+
107
+ cleanup?.();
108
+ cleanup = undefined;
109
+ mountedItem = item;
110
+
111
+ if (!item) {
112
+ return;
113
+ }
114
+
115
+ try {
116
+ const newCleanup = onMount(item as E);
117
+
118
+ if (unmounted) {
119
+ newCleanup?.();
120
+ unwatch();
121
+ }
122
+ else {
123
+ cleanup = newCleanup;
124
+ }
125
+ }
126
+ catch (err) {
127
+ /* v8 ignore start -- @preserve */
128
+ console.error(err);
129
+ throw err;
130
+ /* v8 ignore end */
131
+ }
132
+ });
133
+
134
+ return () => {
135
+ unmounted = true;
136
+
137
+ if (mountedItem) {
138
+ unwatch();
139
+ cleanup?.();
140
+ cleanup = undefined;
141
+ }
142
+ };
143
+ }
144
+
71
145
  /**
72
146
  * Registers an item.
73
147
  *
@@ -75,24 +149,27 @@ export class AsyncRegistry<T extends Destructible> {
75
149
  * @param item The item instance.
76
150
  */
77
151
  register(id: RegistryId | null, item: T): void {
78
- if (this.items.has(id)) {
79
- throw new Error(`Item with ID "${id}" is already registered.`);
80
- }
152
+ this.batch(() => {
153
+ if (this.items.has(id)) {
154
+ throw new Error(`Item with ID "${id}" is already registered.`);
155
+ }
81
156
 
82
- this.resetErrors(id);
83
- this.items.set(id, item);
157
+ this.resetErrors(id);
158
+ this.items.set(id, item);
84
159
 
85
- // Execute all pending callbacks for this item (synchronously).
86
- const pending = this.pendingCallbacks.get(id);
160
+ // Execute all pending callbacks for this item (synchronously).
161
+ const pending = this.pendingCallbacks.get(id);
87
162
 
88
- if (pending) {
89
- pending.success.forEach(callback => callback(item));
90
- this.pendingCallbacks.delete(id);
91
- }
163
+ if (pending) {
164
+ pending.success.forEach(callback => callback(item));
165
+ this.pendingCallbacks.delete(id);
166
+ }
92
167
 
93
- // Register the first item as the default item (null ID).
94
- this.registerAsDefault(id, item);
95
- this.notifyWatchers();
168
+ // Register the first item as the default item (null ID).
169
+ if (this.items.size === 1 && id !== null) {
170
+ this.register(null, item);
171
+ }
172
+ });
96
173
  }
97
174
 
98
175
  /**
@@ -102,24 +179,23 @@ export class AsyncRegistry<T extends Destructible> {
102
179
  * @param error The error to register.
103
180
  */
104
181
  error(id: RegistryId | null, error: any): void {
105
- this.items.delete(id);
106
- this.initializationErrors.set(id, error);
182
+ this.batch(() => {
183
+ this.items.delete(id);
184
+ this.initializationErrors.set(id, error);
107
185
 
108
- // Execute all pending error callbacks for this item.
109
- const pending = this.pendingCallbacks.get(id);
186
+ // Execute all pending error callbacks for this item.
187
+ const pending = this.pendingCallbacks.get(id);
110
188
 
111
- if (pending) {
112
- pending.error.forEach(callback => callback(error));
113
- this.pendingCallbacks.delete(id);
114
- }
115
-
116
- // Set as default error if this is the first error and no items exist.
117
- if (this.initializationErrors.size === 1 && !this.items.size) {
118
- this.error(null, error);
119
- }
189
+ if (pending) {
190
+ pending.error.forEach(callback => callback(error));
191
+ this.pendingCallbacks.delete(id);
192
+ }
120
193
 
121
- // Notify watchers about the error state.
122
- this.notifyWatchers();
194
+ // Set as default error if this is the first error and no items exist.
195
+ if (this.initializationErrors.size === 1 && !this.items.size) {
196
+ this.error(null, error);
197
+ }
198
+ });
123
199
  }
124
200
 
125
201
  /**
@@ -142,21 +218,23 @@ export class AsyncRegistry<T extends Destructible> {
142
218
  * Un-registers an item.
143
219
  *
144
220
  * @param id The ID of the item.
221
+ * @param resetPendingCallbacks If true resets pending callbacks.
145
222
  */
146
- unregister(id: RegistryId | null): void {
147
- if (!this.items.has(id)) {
148
- throw new Error(`Item with ID "${id}" is not registered.`);
149
- }
223
+ unregister(id: RegistryId | null, resetPendingCallbacks: boolean = true): void {
224
+ this.batch(() => {
225
+ // If unregistering the default item, clear it.
226
+ if (id && this.items.get(null) === this.items.get(id)) {
227
+ this.unregister(null, false);
228
+ }
150
229
 
151
- // If unregistering the default item, clear it.
152
- if (id && this.items.get(null) === this.items.get(id)) {
153
- this.unregister(null);
154
- }
230
+ this.items.delete(id);
155
231
 
156
- this.items.delete(id);
157
- this.pendingCallbacks.delete(id);
232
+ if (resetPendingCallbacks) {
233
+ this.pendingCallbacks.delete(id);
234
+ }
158
235
 
159
- this.notifyWatchers();
236
+ this.resetErrors(id);
237
+ });
160
238
  }
161
239
 
162
240
  /**
@@ -168,6 +246,15 @@ export class AsyncRegistry<T extends Destructible> {
168
246
  return Array.from(this.items.values());
169
247
  }
170
248
 
249
+ /**
250
+ * Returns single registered item.
251
+ *
252
+ * @returns Registered item.
253
+ */
254
+ getItem(id: RegistryId | null): T | undefined {
255
+ return this.items.get(id);
256
+ }
257
+
171
258
  /**
172
259
  * Checks if an item with the given ID is registered.
173
260
  *
@@ -183,46 +270,11 @@ export class AsyncRegistry<T extends Destructible> {
183
270
  * If the item is not registered yet, it will wait for it to be registered.
184
271
  *
185
272
  * @param id The ID of the item.
186
- * @param timeout Optional timeout in milliseconds.
187
273
  * @returns A promise that resolves with the item instance.
188
274
  */
189
- waitFor<E extends T = T>(id: RegistryId | null, timeout?: number): Promise<E> {
275
+ waitFor<E extends T = T>(id: RegistryId | null): Promise<E> {
190
276
  return new Promise<E>((resolve, reject) => {
191
- let exceedTimeout = false;
192
- let timer: ReturnType<typeof setTimeout> | null = null;
193
-
194
- void this.execute(
195
- id,
196
- (value: E) => {
197
- if (exceedTimeout) {
198
- return;
199
- }
200
-
201
- if (timer !== null) {
202
- clearTimeout(timer!);
203
- }
204
-
205
- (resolve as (value: E) => void)(value);
206
- },
207
- (error: any) => {
208
- if (exceedTimeout) {
209
- return;
210
- }
211
-
212
- if (timer !== null) {
213
- clearTimeout(timer!);
214
- }
215
-
216
- reject(error);
217
- },
218
- );
219
-
220
- if (timeout) {
221
- timer = setTimeout(() => {
222
- exceedTimeout = true;
223
- reject(new Error(`Timeout waiting for item with ID "${id}" to be registered.`));
224
- }, timeout);
225
- }
277
+ void this.execute(id, resolve as (value: E) => void, reject);
226
278
  });
227
279
  }
228
280
 
@@ -242,7 +294,41 @@ export class AsyncRegistry<T extends Destructible> {
242
294
 
243
295
  await Promise.all(promises);
244
296
 
245
- this.notifyWatchers();
297
+ this.flushWatchers();
298
+ }
299
+
300
+ /**
301
+ * Destroys all registered editors and removes all watchers.
302
+ */
303
+ async reset() {
304
+ await this.destroyAll();
305
+ this.watchers.clear();
306
+ }
307
+
308
+ /**
309
+ * Executes a callback while deferring all watcher notifications.
310
+ * A single notification is fired synchronously after the callback returns,
311
+ * but only if the registry actually changed.
312
+ *
313
+ * Batches can be nested — watchers are notified only when the outermost
314
+ * batch completes.
315
+ *
316
+ * @param fn The callback to execute.
317
+ * @returns The return value of the callback.
318
+ */
319
+ batch<R>(fn: () => R): R {
320
+ this.batchDepth++;
321
+
322
+ try {
323
+ return fn();
324
+ }
325
+ finally {
326
+ this.batchDepth--;
327
+
328
+ if (this.batchDepth === 0) {
329
+ this.flushWatchers();
330
+ }
331
+ }
246
332
  }
247
333
 
248
334
  /**
@@ -273,25 +359,23 @@ export class AsyncRegistry<T extends Destructible> {
273
359
  }
274
360
 
275
361
  /**
276
- * Resets the registry by clearing all items, errors, and pending callbacks.
362
+ * Immediately dispatches the current state to all watchers if it changed.
277
363
  */
278
- reset(): void {
279
- this.items.clear();
280
- this.initializationErrors.clear();
281
- this.pendingCallbacks.clear();
282
- this.notifyWatchers();
283
- }
364
+ private flushWatchers(): void {
365
+ if (
366
+ areMapsEqual(this.lastNotifiedItems, this.items)
367
+ && areMapsEqual(this.lastNotifiedErrors, this.initializationErrors)
368
+ ) {
369
+ return;
370
+ }
284
371
 
285
- /**
286
- * Notifies all watchers about changes to the registry.
287
- */
288
- private notifyWatchers(): void {
289
- this.watchers.forEach(
290
- watcher => watcher(
291
- new Map(this.items),
292
- new Map(this.initializationErrors),
293
- ),
294
- );
372
+ this.lastNotifiedItems = new Map(this.items);
373
+ this.lastNotifiedErrors = new Map(this.initializationErrors);
374
+
375
+ this.watchers.forEach(watcher => watcher(
376
+ new Map(this.items),
377
+ new Map(this.initializationErrors),
378
+ ));
295
379
  }
296
380
 
297
381
  /**
@@ -310,18 +394,6 @@ export class AsyncRegistry<T extends Destructible> {
310
394
 
311
395
  return pending;
312
396
  }
313
-
314
- /**
315
- * Registers an item as the default (null ID) item if it's the first one.
316
- *
317
- * @param id The ID of the item being registered.
318
- * @param item The item instance.
319
- */
320
- private registerAsDefault(id: RegistryId | null, item: T): void {
321
- if (this.items.size === 1 && id !== null) {
322
- this.register(null, item);
323
- }
324
- }
325
397
  }
326
398
 
327
399
  /**
@@ -1,3 +1,4 @@
1
+ export * from './are-maps-equal';
1
2
  export * from './async-registry';
2
3
  export * from './camel-case';
3
4
  export * from './debounce';