ckeditor5-livewire 1.9.0 → 1.11.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/hooks/editable.d.ts +3 -5
- package/dist/hooks/editable.d.ts.map +1 -1
- package/dist/hooks/editor/editor.d.ts +0 -4
- package/dist/hooks/editor/editor.d.ts.map +1 -1
- package/dist/hooks/editor/plugins/livewire-sync.d.ts.map +1 -1
- package/dist/hooks/editor/utils/cleanup-orphan-editor-elements.d.ts +8 -0
- package/dist/hooks/editor/utils/cleanup-orphan-editor-elements.d.ts.map +1 -0
- package/dist/hooks/editor/utils/create-editor-in-context.d.ts +6 -1
- package/dist/hooks/editor/utils/create-editor-in-context.d.ts.map +1 -1
- package/dist/hooks/editor/utils/index.d.ts +2 -0
- package/dist/hooks/editor/utils/index.d.ts.map +1 -1
- package/dist/hooks/editor/utils/is-multiroot-editor-instance.d.ts +6 -0
- package/dist/hooks/editor/utils/is-multiroot-editor-instance.d.ts.map +1 -0
- package/dist/hooks/editor/utils/wrap-with-watchdog.d.ts +7 -16
- package/dist/hooks/editor/utils/wrap-with-watchdog.d.ts.map +1 -1
- package/dist/hooks/ui-part.d.ts +2 -6
- package/dist/hooks/ui-part.d.ts.map +1 -1
- package/dist/index.cjs +2 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +297 -220
- package/dist/index.mjs.map +1 -1
- package/dist/shared/are-maps-equal.d.ts +11 -0
- package/dist/shared/are-maps-equal.d.ts.map +1 -0
- package/dist/shared/async-registry.d.ts +43 -10
- package/dist/shared/async-registry.d.ts.map +1 -1
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/hooks/context/context.test.ts +3 -1
- package/src/hooks/editable.ts +73 -46
- package/src/hooks/editor/editor.test.ts +44 -9
- package/src/hooks/editor/editor.ts +159 -149
- package/src/hooks/editor/plugins/livewire-sync.ts +17 -8
- package/src/hooks/editor/utils/cleanup-orphan-editor-elements.test.ts +285 -0
- package/src/hooks/editor/utils/cleanup-orphan-editor-elements.ts +60 -0
- package/src/hooks/editor/utils/create-editor-in-context.ts +6 -2
- package/src/hooks/editor/utils/index.ts +2 -0
- package/src/hooks/editor/utils/is-multiroot-editor-instance.ts +8 -0
- package/src/hooks/editor/utils/wrap-with-watchdog.test.ts +34 -14
- package/src/hooks/editor/utils/wrap-with-watchdog.ts +16 -26
- package/src/hooks/ui-part.ts +10 -16
- package/src/shared/are-maps-equal.test.ts +56 -0
- package/src/shared/are-maps-equal.ts +22 -0
- package/src/shared/async-registry.test.ts +212 -31
- package/src/shared/async-registry.ts +178 -61
- package/src/shared/index.ts +1 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compares two Map structures for equality based on their contents.
|
|
3
|
+
* The function checks if the maps have the same size, contain the exact same keys,
|
|
4
|
+
* and have strictly equal values (using shallow comparison).
|
|
5
|
+
*
|
|
6
|
+
* @param map1 - The first map to compare (can be null).
|
|
7
|
+
* @param map2 - The second map to compare.
|
|
8
|
+
* @returns Returns `true` if the maps are identical in terms of keys and values, otherwise `false`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function areMapsEqual(map1: Map<any, any> | null, map2: Map<any, any>): boolean;
|
|
11
|
+
//# sourceMappingURL=are-maps-equal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"are-maps-equal.d.ts","sourceRoot":"","sources":["../../../src/shared/are-maps-equal.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,IAAI,EAAE,IAAI,EAAE,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,OAAO,CAYrF"}
|
|
@@ -19,6 +19,15 @@ export declare class AsyncRegistry<T extends Destructible> {
|
|
|
19
19
|
* Set of watchers that observe changes to the registry.
|
|
20
20
|
*/
|
|
21
21
|
private readonly watchers;
|
|
22
|
+
/**
|
|
23
|
+
* Batch nesting depth. When > 0, watcher notifications are deferred.
|
|
24
|
+
*/
|
|
25
|
+
private batchDepth;
|
|
26
|
+
/**
|
|
27
|
+
* Snapshot of the last state dispatched to watchers, used for change detection.
|
|
28
|
+
*/
|
|
29
|
+
private lastNotifiedItems;
|
|
30
|
+
private lastNotifiedErrors;
|
|
22
31
|
/**
|
|
23
32
|
* Executes a function on an item.
|
|
24
33
|
* If the item is not yet registered, it will wait for it to be registered.
|
|
@@ -29,6 +38,14 @@ export declare class AsyncRegistry<T extends Destructible> {
|
|
|
29
38
|
* @returns A promise that resolves with the result of the function.
|
|
30
39
|
*/
|
|
31
40
|
execute<R, E extends T = T>(id: RegistryId | null, onSuccess: (item: E) => R, onError?: (error: any) => void): Promise<Awaited<R>>;
|
|
41
|
+
/**
|
|
42
|
+
* Reactively binds a mount/unmount lifecycle to a single registry item.
|
|
43
|
+
*
|
|
44
|
+
* @param id The ID of the item to observe.
|
|
45
|
+
* @param onMount Function executed when the item mounts.
|
|
46
|
+
* @returns A function that stops observing and immediately runs any pending cleanup.
|
|
47
|
+
*/
|
|
48
|
+
mountEffect<E extends T = T>(id: RegistryId | null, onMount: (item: E) => (() => void) | void): () => void;
|
|
32
49
|
/**
|
|
33
50
|
* Registers an item.
|
|
34
51
|
*
|
|
@@ -53,14 +70,21 @@ export declare class AsyncRegistry<T extends Destructible> {
|
|
|
53
70
|
* Un-registers an item.
|
|
54
71
|
*
|
|
55
72
|
* @param id The ID of the item.
|
|
73
|
+
* @param resetPendingCallbacks If true resets pending callbacks.
|
|
56
74
|
*/
|
|
57
|
-
unregister(id: RegistryId | null): void;
|
|
75
|
+
unregister(id: RegistryId | null, resetPendingCallbacks?: boolean): void;
|
|
58
76
|
/**
|
|
59
77
|
* Gets all registered items.
|
|
60
78
|
*
|
|
61
79
|
* @returns An array of all registered items.
|
|
62
80
|
*/
|
|
63
81
|
getItems(): T[];
|
|
82
|
+
/**
|
|
83
|
+
* Returns single registered item.
|
|
84
|
+
*
|
|
85
|
+
* @returns Registered item.
|
|
86
|
+
*/
|
|
87
|
+
getItem(id: RegistryId | null): T | undefined;
|
|
64
88
|
/**
|
|
65
89
|
* Checks if an item with the given ID is registered.
|
|
66
90
|
*
|
|
@@ -81,6 +105,22 @@ export declare class AsyncRegistry<T extends Destructible> {
|
|
|
81
105
|
* This will call the `destroy` method on each item.
|
|
82
106
|
*/
|
|
83
107
|
destroyAll(): Promise<void>;
|
|
108
|
+
/**
|
|
109
|
+
* Destroys all registered editors and removes all watchers.
|
|
110
|
+
*/
|
|
111
|
+
reset(): Promise<void>;
|
|
112
|
+
/**
|
|
113
|
+
* Executes a callback while deferring all watcher notifications.
|
|
114
|
+
* A single notification is fired synchronously after the callback returns,
|
|
115
|
+
* but only if the registry actually changed.
|
|
116
|
+
*
|
|
117
|
+
* Batches can be nested — watchers are notified only when the outermost
|
|
118
|
+
* batch completes.
|
|
119
|
+
*
|
|
120
|
+
* @param fn The callback to execute.
|
|
121
|
+
* @returns The return value of the callback.
|
|
122
|
+
*/
|
|
123
|
+
batch<R>(fn: () => R): R;
|
|
84
124
|
/**
|
|
85
125
|
* Registers a watcher that will be called whenever the registry changes.
|
|
86
126
|
*
|
|
@@ -95,9 +135,9 @@ export declare class AsyncRegistry<T extends Destructible> {
|
|
|
95
135
|
*/
|
|
96
136
|
unwatch(watcher: RegistryWatcher<T>): void;
|
|
97
137
|
/**
|
|
98
|
-
*
|
|
138
|
+
* Immediately dispatches the current state to all watchers if it changed.
|
|
99
139
|
*/
|
|
100
|
-
private
|
|
140
|
+
private flushWatchers;
|
|
101
141
|
/**
|
|
102
142
|
* Gets or creates pending callbacks for a specific ID.
|
|
103
143
|
*
|
|
@@ -105,13 +145,6 @@ export declare class AsyncRegistry<T extends Destructible> {
|
|
|
105
145
|
* @returns The pending callbacks structure.
|
|
106
146
|
*/
|
|
107
147
|
private getPendingCallbacks;
|
|
108
|
-
/**
|
|
109
|
-
* Registers an item as the default (null ID) item if it's the first one.
|
|
110
|
-
*
|
|
111
|
-
* @param id The ID of the item being registered.
|
|
112
|
-
* @param item The item instance.
|
|
113
|
-
*/
|
|
114
|
-
private registerAsDefault;
|
|
115
148
|
}
|
|
116
149
|
/**
|
|
117
150
|
* Interface for objects that can be destroyed.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"async-registry.d.ts","sourceRoot":"","sources":["../../../src/shared/async-registry.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"async-registry.d.ts","sourceRoot":"","sources":["../../../src/shared/async-registry.ts"],"names":[],"mappings":"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"}
|
package/dist/shared/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/shared/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC;AAC3B,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC;AACvC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,QAAQ,CAAC;AACvB,cAAc,iBAAiB,CAAC;AAChC,cAAc,WAAW,CAAC;AAC1B,cAAc,OAAO,CAAC;AACtB,cAAc,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/shared/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC;AAC3B,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC;AACvC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,QAAQ,CAAC;AACvB,cAAc,iBAAiB,CAAC;AAChC,cAAc,WAAW,CAAC;AAC1B,cAAc,OAAO,CAAC;AACtB,cAAc,YAAY,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ckeditor5-livewire",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.0",
|
|
4
4
|
"description": "CKEditor 5 integration for Laravel Livewire",
|
|
5
5
|
"author": "Mateusz Bagiński <cziken58@gmail.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -32,9 +32,9 @@
|
|
|
32
32
|
"@vitest/coverage-v8": "^4.1.0",
|
|
33
33
|
"ckeditor5": "^47.6.0",
|
|
34
34
|
"ckeditor5-premium-features": "^47.6.0",
|
|
35
|
-
"happy-dom": "^20.
|
|
35
|
+
"happy-dom": "^20.8.9",
|
|
36
36
|
"typescript": "^5.5.4",
|
|
37
|
-
"vite": "^8.0.
|
|
37
|
+
"vite": "^8.0.5",
|
|
38
38
|
"vite-plugin-dts": "^4.5.4",
|
|
39
39
|
"vitest": "^4.1.0"
|
|
40
40
|
},
|
|
@@ -342,7 +342,9 @@ describe('context component', () => {
|
|
|
342
342
|
|
|
343
343
|
await livewireStub.$internal.unmountComponent(editorId);
|
|
344
344
|
|
|
345
|
-
|
|
345
|
+
await vi.waitFor(() => {
|
|
346
|
+
expect(context?.editors.first).toBeNull();
|
|
347
|
+
});
|
|
346
348
|
});
|
|
347
349
|
|
|
348
350
|
it('should remove the context instance from the registry on destroy', async () => {
|
package/src/hooks/editable.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import type { MultiRootEditor } from 'ckeditor5';
|
|
1
|
+
import type { DecoupledEditor, Editor, MultiRootEditor } from 'ckeditor5';
|
|
2
2
|
|
|
3
3
|
import type { RootAttributesUpdater } from './utils';
|
|
4
4
|
|
|
5
5
|
import { debounce } from '../shared';
|
|
6
6
|
import { EditorsRegistry } from './editor/editors-registry';
|
|
7
|
+
import { isMultirootEditorInstance } from './editor/utils';
|
|
7
8
|
import { ClassHook } from './hook';
|
|
8
9
|
import { createRootAttributesUpdater, isWireModelConnected } from './utils';
|
|
9
10
|
|
|
@@ -11,11 +12,6 @@ import { createRootAttributesUpdater, isWireModelConnected } from './utils';
|
|
|
11
12
|
* Editable hook for Livewire. It allows you to create editables for multi-root editors.
|
|
12
13
|
*/
|
|
13
14
|
export class EditableComponentHook extends ClassHook<Snapshot> {
|
|
14
|
-
/**
|
|
15
|
-
* The promise that resolves when the editable is mounted.
|
|
16
|
-
*/
|
|
17
|
-
private editorPromise: Promise<MultiRootEditor | null> | null = null;
|
|
18
|
-
|
|
19
15
|
/**
|
|
20
16
|
* The root attributes updater for the editable's root.
|
|
21
17
|
*/
|
|
@@ -32,16 +28,15 @@ export class EditableComponentHook extends ClassHook<Snapshot> {
|
|
|
32
28
|
override mounted() {
|
|
33
29
|
const { editorId, rootName, content } = this.canonical;
|
|
34
30
|
|
|
35
|
-
|
|
36
|
-
this.editorPromise = EditorsRegistry.the.execute(editorId, (editor: MultiRootEditor) => {
|
|
31
|
+
const unmountEffect = EditorsRegistry.the.mountEffect(editorId, (editor: MultiRootEditor | DecoupledEditor) => {
|
|
37
32
|
/* v8 ignore next if -- @preserve */
|
|
38
33
|
if (this.isBeingDestroyed()) {
|
|
39
|
-
return
|
|
34
|
+
return;
|
|
40
35
|
}
|
|
41
36
|
|
|
42
|
-
const
|
|
37
|
+
const root = editor.model.document.getRoot(rootName);
|
|
43
38
|
|
|
44
|
-
if (
|
|
39
|
+
if (root?.isAttached()) {
|
|
45
40
|
// If the newly added root already exists, but the newly added editable has content,
|
|
46
41
|
// we need to update the root data with the editable content.
|
|
47
42
|
if (content !== null) {
|
|
@@ -54,7 +49,11 @@ export class EditableComponentHook extends ClassHook<Snapshot> {
|
|
|
54
49
|
}
|
|
55
50
|
}
|
|
56
51
|
}
|
|
57
|
-
|
|
52
|
+
|
|
53
|
+
/* v8 ignore next else -- @preserve */
|
|
54
|
+
if (!root && isMultirootEditorInstance(editor)) {
|
|
55
|
+
const { ui, editing } = editor;
|
|
56
|
+
|
|
58
57
|
editor.addRoot(rootName, {
|
|
59
58
|
isUndoable: false,
|
|
60
59
|
...content !== null && {
|
|
@@ -70,19 +69,56 @@ export class EditableComponentHook extends ClassHook<Snapshot> {
|
|
|
70
69
|
}
|
|
71
70
|
|
|
72
71
|
// Add livewire sync.
|
|
73
|
-
this.syncTypingContentPush(editor);
|
|
74
|
-
this.setupPendingReceivedContentHandlers(editor);
|
|
72
|
+
const cleanupSync = this.syncTypingContentPush(editor);
|
|
73
|
+
const cleanupPending = this.setupPendingReceivedContentHandlers(editor);
|
|
74
|
+
|
|
75
75
|
this.applyRootAttributes(editor);
|
|
76
76
|
|
|
77
|
-
return
|
|
77
|
+
return () => {
|
|
78
|
+
cleanupSync();
|
|
79
|
+
cleanupPending();
|
|
80
|
+
|
|
81
|
+
// Remove root attributes we may have set on this root.
|
|
82
|
+
this.rootAttributesUpdater?.(null);
|
|
83
|
+
|
|
84
|
+
// Unmount root from the editor if editor is still registered.
|
|
85
|
+
/* v8 ignore next else -- @preserve */
|
|
86
|
+
if (editor.state !== 'destroyed') {
|
|
87
|
+
const root = editor.model.document.getRoot(rootName);
|
|
88
|
+
|
|
89
|
+
/* v8 ignore next if -- @preserve */
|
|
90
|
+
if (root && isMultirootEditorInstance(editor)) {
|
|
91
|
+
// Detaching editables seem to be buggy when something removed DOM element of the editable (e.g. Livewire re-render) before
|
|
92
|
+
// the editable is unmounted. To prevent errors in such cases, we will try to detach the editable if it exists, but ignore errors.
|
|
93
|
+
try {
|
|
94
|
+
/* v8 ignore else -- @preserve */
|
|
95
|
+
if (editor.ui.view.editables[rootName]) {
|
|
96
|
+
editor.detachEditable(root);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
// Ignore errors when detaching editable.
|
|
101
|
+
/* v8 ignore next -- @preserve */
|
|
102
|
+
console.error('Unable unmount editable from root:', err);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (root.isAttached()) {
|
|
106
|
+
editor.detachRoot(rootName, false);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
};
|
|
78
111
|
});
|
|
112
|
+
|
|
113
|
+
this.onBeforeDestroy(unmountEffect);
|
|
79
114
|
}
|
|
80
115
|
|
|
81
116
|
/**
|
|
82
117
|
* Called when the component is updated by Livewire.
|
|
83
118
|
*/
|
|
84
119
|
override async afterCommitSynced(): Promise<void> {
|
|
85
|
-
const
|
|
120
|
+
const { editorId } = this.canonical;
|
|
121
|
+
const editor = await EditorsRegistry.the.waitFor(editorId);
|
|
86
122
|
|
|
87
123
|
this.applyCanonicalContentToEditor(editor);
|
|
88
124
|
this.applyRootAttributes(editor);
|
|
@@ -91,35 +127,17 @@ export class EditableComponentHook extends ClassHook<Snapshot> {
|
|
|
91
127
|
/**
|
|
92
128
|
* Destroys the editable component. Unmounts root from the editor.
|
|
93
129
|
*/
|
|
94
|
-
override
|
|
95
|
-
const { rootName } = this.canonical;
|
|
96
|
-
|
|
130
|
+
override destroyed() {
|
|
97
131
|
// Let's hide the element during destruction to prevent flickering.
|
|
132
|
+
// Root detachment and attribute cleanup are handled by the mountEffect cleanup function.
|
|
98
133
|
this.element.style.display = 'none';
|
|
99
|
-
|
|
100
|
-
// Let's wait for the mounted promise to resolve before proceeding with destruction.
|
|
101
|
-
const editor = await this.editorPromise;
|
|
102
|
-
this.editorPromise = null;
|
|
103
|
-
|
|
104
|
-
// Remove root attributes we may have set on this root.
|
|
105
|
-
this.rootAttributesUpdater?.(null);
|
|
106
|
-
|
|
107
|
-
// Unmount root from the editor if editor is still registered.
|
|
108
|
-
if (editor && editor.state !== 'destroyed') {
|
|
109
|
-
const root = editor.model.document.getRoot(rootName);
|
|
110
|
-
|
|
111
|
-
/* v8 ignore next if -- @preserve */
|
|
112
|
-
if (root && 'detachEditable' in editor) {
|
|
113
|
-
editor.detachEditable(root);
|
|
114
|
-
editor.detachRoot(rootName, false);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
134
|
}
|
|
118
135
|
|
|
119
136
|
/**
|
|
120
137
|
* Setups the content sync from the editor to Livewire on user input with debounce.
|
|
138
|
+
* Returns a cleanup function that unregisters all event listeners.
|
|
121
139
|
*/
|
|
122
|
-
private syncTypingContentPush(editor: MultiRootEditor) {
|
|
140
|
+
private syncTypingContentPush(editor: MultiRootEditor | DecoupledEditor): () => void {
|
|
123
141
|
const { rootName, saveDebounceMs } = this.canonical;
|
|
124
142
|
|
|
125
143
|
const input = this.element.querySelector<HTMLInputElement>('input');
|
|
@@ -130,6 +148,12 @@ export class EditableComponentHook extends ClassHook<Snapshot> {
|
|
|
130
148
|
return;
|
|
131
149
|
}
|
|
132
150
|
|
|
151
|
+
const root = editor.model.document.getRoot(rootName);
|
|
152
|
+
|
|
153
|
+
if (!root?.isAttached()) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
133
157
|
const html = editor.getData({ rootName });
|
|
134
158
|
|
|
135
159
|
if (input) {
|
|
@@ -152,17 +176,18 @@ export class EditableComponentHook extends ClassHook<Snapshot> {
|
|
|
152
176
|
editor.model.document.on('change:data', onChangeData);
|
|
153
177
|
sync();
|
|
154
178
|
|
|
155
|
-
|
|
179
|
+
return () => {
|
|
156
180
|
isDestroyed = true;
|
|
157
181
|
editor.model.document.off('change:data', onChangeData);
|
|
158
|
-
}
|
|
182
|
+
};
|
|
159
183
|
}
|
|
160
184
|
|
|
161
185
|
/**
|
|
162
186
|
* Sets up handlers that manage pending incoming content (clears pending
|
|
163
187
|
* content on user edits and applies pending content on blur).
|
|
188
|
+
* Returns a cleanup function that unregisters all event listeners.
|
|
164
189
|
*/
|
|
165
|
-
private setupPendingReceivedContentHandlers(editor:
|
|
190
|
+
private setupPendingReceivedContentHandlers(editor: Editor): () => void {
|
|
166
191
|
const { ui, model } = editor;
|
|
167
192
|
const { focusTracker } = ui;
|
|
168
193
|
const { rootName } = this.canonical;
|
|
@@ -184,16 +209,16 @@ export class EditableComponentHook extends ClassHook<Snapshot> {
|
|
|
184
209
|
model.document.on('change:data', onDataChange);
|
|
185
210
|
focusTracker.on('change:isFocused', onFocusChange);
|
|
186
211
|
|
|
187
|
-
|
|
212
|
+
return () => {
|
|
188
213
|
model.document.off('change:data', onDataChange);
|
|
189
214
|
focusTracker.off('change:isFocused', onFocusChange);
|
|
190
|
-
}
|
|
215
|
+
};
|
|
191
216
|
}
|
|
192
217
|
|
|
193
218
|
/**
|
|
194
219
|
* Applies canonical content to the editor while respecting focus/pending state.
|
|
195
220
|
*/
|
|
196
|
-
private applyCanonicalContentToEditor(editor:
|
|
221
|
+
private applyCanonicalContentToEditor(editor: Editor): void {
|
|
197
222
|
if (!isWireModelConnected(this.element)) {
|
|
198
223
|
return;
|
|
199
224
|
}
|
|
@@ -212,13 +237,15 @@ export class EditableComponentHook extends ClassHook<Snapshot> {
|
|
|
212
237
|
return;
|
|
213
238
|
}
|
|
214
239
|
|
|
215
|
-
editor.setData({
|
|
240
|
+
editor.setData({
|
|
241
|
+
[rootName]: content ?? '',
|
|
242
|
+
});
|
|
216
243
|
}
|
|
217
244
|
|
|
218
245
|
/**
|
|
219
246
|
* Applies root attributes from the Livewire snapshot to the editor root.
|
|
220
247
|
*/
|
|
221
|
-
private applyRootAttributes(editor:
|
|
248
|
+
private applyRootAttributes(editor: Editor): void {
|
|
222
249
|
const { rootName, rootAttributes } = this.canonical;
|
|
223
250
|
|
|
224
251
|
this.rootAttributesUpdater ??= createRootAttributesUpdater(editor, rootName);
|
|
@@ -488,11 +488,11 @@ describe('editor component', () => {
|
|
|
488
488
|
await vi.advanceTimersByTimeAsync(399);
|
|
489
489
|
expect($wire.set).not.toHaveBeenCalled();
|
|
490
490
|
|
|
491
|
-
await vi.advanceTimersByTimeAsync(
|
|
491
|
+
await vi.advanceTimersByTimeAsync(20);
|
|
492
492
|
expect($wire.set).toHaveBeenCalledExactlyOnceWith('content', { main: '<p>Debounce test</p>' });
|
|
493
493
|
});
|
|
494
494
|
|
|
495
|
-
it('should immediately send `$wire.set` when editor is not focused', async () => {
|
|
495
|
+
it('should almost immediately send `$wire.set` when editor is not focused', async () => {
|
|
496
496
|
const { $wire } = livewireStub.$internal.appendComponentToDOM<EditorSnapshot>({
|
|
497
497
|
name: 'ckeditor5',
|
|
498
498
|
el: createEditorHtmlElement(),
|
|
@@ -507,6 +507,8 @@ describe('editor component', () => {
|
|
|
507
507
|
vi.mocked($wire.set).mockClear();
|
|
508
508
|
editor.setData('<p>Debounce test</p>');
|
|
509
509
|
|
|
510
|
+
await vi.advanceTimersByTimeAsync(20);
|
|
511
|
+
|
|
510
512
|
expect($wire.set).toHaveBeenCalledExactlyOnceWith('content', { main: '<p>Debounce test</p>' });
|
|
511
513
|
});
|
|
512
514
|
|
|
@@ -531,7 +533,7 @@ describe('editor component', () => {
|
|
|
531
533
|
await vi.advanceTimersByTimeAsync(399);
|
|
532
534
|
expect(input.value).to.be.equal('<p>Initial content</p>');
|
|
533
535
|
|
|
534
|
-
await vi.advanceTimersByTimeAsync(
|
|
536
|
+
await vi.advanceTimersByTimeAsync(20);
|
|
535
537
|
expect(input.value).to.be.equal('<p>Debounce test</p>');
|
|
536
538
|
});
|
|
537
539
|
});
|
|
@@ -667,6 +669,11 @@ describe('editor component', () => {
|
|
|
667
669
|
const originalEditor = await waitForTestEditor('editor-with-watchdog');
|
|
668
670
|
const watchdog = unwrapEditorWatchdog(originalEditor)!;
|
|
669
671
|
|
|
672
|
+
(watchdog as any)._fire('error', {
|
|
673
|
+
error: new Error('Mock'),
|
|
674
|
+
causesRestart: true,
|
|
675
|
+
});
|
|
676
|
+
|
|
670
677
|
(watchdog as any)._restart();
|
|
671
678
|
|
|
672
679
|
await vi.waitFor(async () => {
|
|
@@ -675,6 +682,34 @@ describe('editor component', () => {
|
|
|
675
682
|
expect(newEditor).not.equals(originalEditor);
|
|
676
683
|
});
|
|
677
684
|
});
|
|
685
|
+
|
|
686
|
+
it('should not restart editor if only `causesRestart: false` error occurs', async () => {
|
|
687
|
+
livewireStub.$internal.appendComponentToDOM<EditorSnapshot>({
|
|
688
|
+
name: 'ckeditor5',
|
|
689
|
+
el: createEditorHtmlElement({
|
|
690
|
+
id: 'editor-with-watchdog',
|
|
691
|
+
}),
|
|
692
|
+
canonical: {
|
|
693
|
+
...createEditorSnapshot(),
|
|
694
|
+
editorId: 'editor-with-watchdog',
|
|
695
|
+
watchdog: true,
|
|
696
|
+
},
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
const originalEditor = await waitForTestEditor('editor-with-watchdog');
|
|
700
|
+
const watchdog = unwrapEditorWatchdog(originalEditor)!;
|
|
701
|
+
|
|
702
|
+
(watchdog as any)._fire('error', {
|
|
703
|
+
error: new Error('Mock'),
|
|
704
|
+
causesRestart: false,
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
await vi.waitFor(async () => {
|
|
708
|
+
const newEditor = await waitForTestEditor('editor-with-watchdog');
|
|
709
|
+
|
|
710
|
+
expect(newEditor).to.be.equal(originalEditor);
|
|
711
|
+
});
|
|
712
|
+
});
|
|
678
713
|
});
|
|
679
714
|
|
|
680
715
|
describe('livewire <> editor synchronization', () => {
|
|
@@ -718,7 +753,7 @@ describe('editor component', () => {
|
|
|
718
753
|
vi.mocked($wire.set).mockClear();
|
|
719
754
|
editor.setData('<p>New content</p>');
|
|
720
755
|
|
|
721
|
-
await vi.advanceTimersByTimeAsync(
|
|
756
|
+
await vi.advanceTimersByTimeAsync(20);
|
|
722
757
|
|
|
723
758
|
expect($wire.set).toHaveBeenCalledWith('content', { main: '<p>New content</p>' });
|
|
724
759
|
vi.useRealTimers();
|
|
@@ -743,7 +778,7 @@ describe('editor component', () => {
|
|
|
743
778
|
editor.setData('<p>Updated content</p>');
|
|
744
779
|
focusTracker.isFocused = true;
|
|
745
780
|
|
|
746
|
-
await vi.advanceTimersByTimeAsync(
|
|
781
|
+
await vi.advanceTimersByTimeAsync(20);
|
|
747
782
|
|
|
748
783
|
expect($wire.set).toHaveBeenCalledWith('content', { main: '<p>Updated content</p>' });
|
|
749
784
|
expect($wire.set).toHaveBeenCalledWith('focused', true);
|
|
@@ -769,7 +804,7 @@ describe('editor component', () => {
|
|
|
769
804
|
vi.mocked($wire.set).mockClear();
|
|
770
805
|
editor.setData('<p>New content</p>');
|
|
771
806
|
|
|
772
|
-
await vi.advanceTimersByTimeAsync(
|
|
807
|
+
await vi.advanceTimersByTimeAsync(20);
|
|
773
808
|
|
|
774
809
|
expect($wire.set).toHaveBeenCalledWith('content', { main: '<p>New content</p>' });
|
|
775
810
|
vi.useRealTimers();
|
|
@@ -830,7 +865,7 @@ describe('editor component', () => {
|
|
|
830
865
|
vi.mocked($wire.dispatch).mockClear();
|
|
831
866
|
editor.setData('<p>Content change event test</p>');
|
|
832
867
|
|
|
833
|
-
await vi.advanceTimersByTimeAsync(
|
|
868
|
+
await vi.advanceTimersByTimeAsync(20);
|
|
834
869
|
|
|
835
870
|
expect($wire.dispatch).toHaveBeenCalledExactlyOnceWith('editor-content-changed', {
|
|
836
871
|
editorId: DEFAULT_TEST_EDITOR_ID,
|
|
@@ -1071,7 +1106,7 @@ describe('editor component', () => {
|
|
|
1071
1106
|
const input = getTestEditorInput();
|
|
1072
1107
|
|
|
1073
1108
|
editor.setData('<p>Form integration test</p>');
|
|
1074
|
-
await vi.advanceTimersByTimeAsync(
|
|
1109
|
+
await vi.advanceTimersByTimeAsync(20);
|
|
1075
1110
|
|
|
1076
1111
|
expect(input.value).to.be.equal('<p>Form integration test</p>');
|
|
1077
1112
|
});
|
|
@@ -1123,7 +1158,7 @@ describe('editor component', () => {
|
|
|
1123
1158
|
|
|
1124
1159
|
// Submit the form.
|
|
1125
1160
|
input.closest('form')!.dispatchEvent(new Event('submit', { bubbles: true }));
|
|
1126
|
-
await vi.advanceTimersByTimeAsync(
|
|
1161
|
+
await vi.advanceTimersByTimeAsync(20);
|
|
1127
1162
|
|
|
1128
1163
|
// Value should be synced immediately on form submit.
|
|
1129
1164
|
expect(input.value).to.be.equal('<p>Form integration test</p>');
|