ckeditor5-livewire 1.2.3 → 1.2.5
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/editor/editor.d.ts.map +1 -1
- package/dist/hooks/editor/utils/index.d.ts +1 -1
- package/dist/hooks/editor/utils/index.d.ts.map +1 -1
- package/dist/hooks/editor/utils/{is-single-editing-like-editor.d.ts → is-single-root-editor.d.ts} +2 -2
- package/dist/hooks/editor/utils/is-single-root-editor.d.ts.map +1 -0
- package/dist/hooks/editor/utils/query-editor-editables.d.ts +8 -9
- package/dist/hooks/editor/utils/query-editor-editables.d.ts.map +1 -1
- package/dist/index.cjs +2 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +153 -172
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/hooks/editor/editor.test.ts +44 -1
- package/src/hooks/editor/editor.ts +18 -27
- package/src/hooks/editor/plugins/livewire-sync.ts +2 -2
- package/src/hooks/editor/utils/index.ts +1 -1
- package/src/hooks/editor/utils/{is-single-editing-like-editor.test.ts → is-single-root-editor.test.ts} +9 -9
- package/src/hooks/editor/utils/{is-single-editing-like-editor.ts → is-single-root-editor.ts} +1 -1
- package/src/hooks/editor/utils/query-editor-editables.ts +19 -56
- package/dist/hooks/editor/utils/is-single-editing-like-editor.d.ts.map +0 -1
|
@@ -213,7 +213,7 @@ describe('editor component', () => {
|
|
|
213
213
|
});
|
|
214
214
|
|
|
215
215
|
await expect(waitForTestEditor()).rejects.toThrowError(
|
|
216
|
-
|
|
216
|
+
/It looks like not all required root elements are present yet/,
|
|
217
217
|
);
|
|
218
218
|
});
|
|
219
219
|
});
|
|
@@ -627,6 +627,49 @@ describe('editor component', () => {
|
|
|
627
627
|
|
|
628
628
|
vi.useRealTimers();
|
|
629
629
|
});
|
|
630
|
+
|
|
631
|
+
it('should not crash if content is null in snapshot', async () => {
|
|
632
|
+
vi.useFakeTimers();
|
|
633
|
+
|
|
634
|
+
const { $wire } = livewireStub.$internal.appendComponentToDOM<EditorSnapshot>({
|
|
635
|
+
name: 'ckeditor5',
|
|
636
|
+
el: createEditorHtmlElement(),
|
|
637
|
+
canonical: {
|
|
638
|
+
...createEditorSnapshot(),
|
|
639
|
+
content: null as any,
|
|
640
|
+
saveDebounceMs: 0,
|
|
641
|
+
},
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
const editor = await waitForTestEditor();
|
|
645
|
+
|
|
646
|
+
$wire.set.mockClear();
|
|
647
|
+
editor.setData('<p>New content</p>');
|
|
648
|
+
|
|
649
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
650
|
+
|
|
651
|
+
expect($wire.set).toHaveBeenCalledWith('content', { main: '<p>New content</p>' });
|
|
652
|
+
vi.useRealTimers();
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
it('should not crash on focus change if content is null in snapshot', async () => {
|
|
656
|
+
const { $wire } = livewireStub.$internal.appendComponentToDOM<EditorSnapshot>({
|
|
657
|
+
name: 'ckeditor5',
|
|
658
|
+
el: createEditorHtmlElement(),
|
|
659
|
+
canonical: {
|
|
660
|
+
...createEditorSnapshot(),
|
|
661
|
+
content: null as any,
|
|
662
|
+
},
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
const { ui: { focusTracker } } = await waitForTestEditor();
|
|
666
|
+
|
|
667
|
+
// Focus the editor.
|
|
668
|
+
$wire.set.mockClear();
|
|
669
|
+
focusTracker.isFocused = true;
|
|
670
|
+
|
|
671
|
+
expect($wire.set).toHaveBeenCalledWith('focused', true);
|
|
672
|
+
});
|
|
630
673
|
});
|
|
631
674
|
|
|
632
675
|
describe('dispatch / receive events', () => {
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from './plugins';
|
|
14
14
|
import {
|
|
15
15
|
createEditorInContext,
|
|
16
|
-
|
|
16
|
+
isSingleRootEditor,
|
|
17
17
|
loadAllEditorTranslations,
|
|
18
18
|
loadEditorConstructor,
|
|
19
19
|
loadEditorPlugins,
|
|
@@ -171,7 +171,7 @@ export class EditorComponentHook extends ClassHook<Snapshot> {
|
|
|
171
171
|
),
|
|
172
172
|
);
|
|
173
173
|
|
|
174
|
-
if (
|
|
174
|
+
if (isSingleRootEditor(editorType)) {
|
|
175
175
|
loadedPlugins.push(
|
|
176
176
|
await createSyncEditorWithInputPlugin(saveDebounceMs),
|
|
177
177
|
);
|
|
@@ -188,31 +188,40 @@ export class EditorComponentHook extends ClassHook<Snapshot> {
|
|
|
188
188
|
// Let's query all elements, and create basic configuration.
|
|
189
189
|
let initialData: string | Record<string, string> = {
|
|
190
190
|
...content,
|
|
191
|
-
...queryEditablesSnapshotContent(editorId
|
|
191
|
+
...queryEditablesSnapshotContent(editorId),
|
|
192
192
|
};
|
|
193
193
|
|
|
194
|
-
if (
|
|
194
|
+
if (isSingleRootEditor(editorType)) {
|
|
195
195
|
initialData = initialData['main'] || '';
|
|
196
196
|
}
|
|
197
197
|
|
|
198
198
|
// Depending of the editor type, and parent lookup for nearest context or initialize it without it.
|
|
199
199
|
const editor = await (async () => {
|
|
200
|
-
let sourceElementOrData = queryEditablesElements(editorId, editorType);
|
|
200
|
+
let sourceElementOrData: HTMLElement | Record<string, HTMLElement> = queryEditablesElements(editorId, editorType);
|
|
201
201
|
|
|
202
202
|
// Handle special case when user specified `initialData` of several root elements, but editable components
|
|
203
203
|
// are not yet present in the DOM. In other words - editor is initialized before attaching root elements.
|
|
204
|
-
if (
|
|
205
|
-
const requiredRoots =
|
|
204
|
+
if (!(sourceElementOrData instanceof HTMLElement) && !('main' in sourceElementOrData)) {
|
|
205
|
+
const requiredRoots = (
|
|
206
|
+
editorType === 'decoupled'
|
|
207
|
+
? ['main']
|
|
208
|
+
: Object.keys(initialData as Record<string, string>)
|
|
209
|
+
);
|
|
206
210
|
|
|
207
211
|
if (!checkIfAllRootsArePresent(sourceElementOrData, requiredRoots)) {
|
|
208
212
|
sourceElementOrData = await waitForAllRootsToBePresent(editorId, editorType, requiredRoots);
|
|
209
213
|
initialData = {
|
|
210
214
|
...content,
|
|
211
|
-
...queryEditablesSnapshotContent(editorId
|
|
215
|
+
...queryEditablesSnapshotContent(editorId),
|
|
212
216
|
};
|
|
213
217
|
}
|
|
214
218
|
}
|
|
215
219
|
|
|
220
|
+
// If single root editor, unwrap the element from the object.
|
|
221
|
+
if (isSingleRootEditor(editorType) && 'main' in sourceElementOrData) {
|
|
222
|
+
sourceElementOrData = sourceElementOrData['main'];
|
|
223
|
+
}
|
|
224
|
+
|
|
216
225
|
// Construct parsed config.
|
|
217
226
|
const parsedConfig = {
|
|
218
227
|
...resolveEditorConfigElementReferences(config),
|
|
@@ -239,7 +248,7 @@ export class EditorComponentHook extends ClassHook<Snapshot> {
|
|
|
239
248
|
return result.editor;
|
|
240
249
|
})();
|
|
241
250
|
|
|
242
|
-
if (
|
|
251
|
+
if (isSingleRootEditor(editorType) && editableHeight) {
|
|
243
252
|
setEditorEditableHeight(editor, editableHeight);
|
|
244
253
|
}
|
|
245
254
|
|
|
@@ -293,24 +302,6 @@ async function waitForAllRootsToBePresent(
|
|
|
293
302
|
return queryEditablesElements(editorId, editorType) as unknown as Record<string, HTMLElement>;
|
|
294
303
|
}
|
|
295
304
|
|
|
296
|
-
/**
|
|
297
|
-
* Type guard to check if we should wait for multiple root elements.
|
|
298
|
-
*
|
|
299
|
-
* @param elements The elements retrieved for the editor.
|
|
300
|
-
* @param editorType The type of the editor.
|
|
301
|
-
* @returns True if we should wait for multiple root elements, false otherwise.
|
|
302
|
-
*/
|
|
303
|
-
function shouldWaitForRoots(
|
|
304
|
-
elements: HTMLElement | Record<string, HTMLElement>,
|
|
305
|
-
editorType: EditorType,
|
|
306
|
-
): elements is Record<string, HTMLElement> {
|
|
307
|
-
return (
|
|
308
|
-
!isSingleEditingLikeEditor(editorType)
|
|
309
|
-
&& typeof elements === 'object'
|
|
310
|
-
&& !(elements instanceof HTMLElement)
|
|
311
|
-
);
|
|
312
|
-
}
|
|
313
|
-
|
|
314
305
|
/**
|
|
315
306
|
* A snapshot of the Livewire component's state relevant to the CKEditor5 hook.
|
|
316
307
|
*/
|
|
@@ -101,7 +101,7 @@ export async function createLivewireSyncPlugin(
|
|
|
101
101
|
const values = this.getEditorRootsValues();
|
|
102
102
|
|
|
103
103
|
// Prevent looping when editor changed content from Livewire.
|
|
104
|
-
if (!shallowEqual(values, component.canonical.content)) {
|
|
104
|
+
if (!shallowEqual(values, component.canonical.content ?? {})) {
|
|
105
105
|
$wire.set('content', values);
|
|
106
106
|
$wire.dispatch('editor-content-changed', {
|
|
107
107
|
editorId: component.canonical.editorId,
|
|
@@ -127,7 +127,7 @@ export async function createLivewireSyncPlugin(
|
|
|
127
127
|
$wire.set('focused', ui.focusTracker.isFocused);
|
|
128
128
|
|
|
129
129
|
// Only push content if it has changed compared to canonical
|
|
130
|
-
if (!shallowEqual(values, component.canonical.content)) {
|
|
130
|
+
if (!shallowEqual(values, component.canonical.content ?? {})) {
|
|
131
131
|
$wire.set('content', values);
|
|
132
132
|
}
|
|
133
133
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export * from './create-editor-in-context';
|
|
2
2
|
export * from './get-editor-roots-values';
|
|
3
|
-
export * from './is-single-
|
|
3
|
+
export * from './is-single-root-editor';
|
|
4
4
|
export * from './is-wire-model-connected';
|
|
5
5
|
export * from './load-editor-constructor';
|
|
6
6
|
export * from './load-editor-plugins';
|
|
@@ -2,27 +2,27 @@ import { describe, expect, it } from 'vitest';
|
|
|
2
2
|
|
|
3
3
|
import type { EditorType } from '../typings';
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { isSingleRootEditor } from './is-single-root-editor';
|
|
6
6
|
|
|
7
|
-
describe('
|
|
7
|
+
describe('isSingleRootEditor', () => {
|
|
8
8
|
it('should return true for inline editor', () => {
|
|
9
|
-
expect(
|
|
9
|
+
expect(isSingleRootEditor('inline')).toBe(true);
|
|
10
10
|
});
|
|
11
11
|
|
|
12
12
|
it('should return true for classic editor', () => {
|
|
13
|
-
expect(
|
|
13
|
+
expect(isSingleRootEditor('classic')).toBe(true);
|
|
14
14
|
});
|
|
15
15
|
|
|
16
16
|
it('should return true for balloon editor', () => {
|
|
17
|
-
expect(
|
|
17
|
+
expect(isSingleRootEditor('balloon')).toBe(true);
|
|
18
18
|
});
|
|
19
19
|
|
|
20
20
|
it('should return false for decoupled editor', () => {
|
|
21
|
-
expect(
|
|
21
|
+
expect(isSingleRootEditor('decoupled')).toBe(true);
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
it('should return false for multiroot editor', () => {
|
|
25
|
-
expect(
|
|
25
|
+
expect(isSingleRootEditor('multiroot')).toBe(false);
|
|
26
26
|
});
|
|
27
27
|
|
|
28
28
|
it('should handle all valid editor types', () => {
|
|
@@ -30,11 +30,11 @@ describe('isSingleEditingLikeEditor', () => {
|
|
|
30
30
|
const multiEditingTypes: EditorType[] = ['multiroot'];
|
|
31
31
|
|
|
32
32
|
singleEditingTypes.forEach((type) => {
|
|
33
|
-
expect(
|
|
33
|
+
expect(isSingleRootEditor(type)).toBe(true);
|
|
34
34
|
});
|
|
35
35
|
|
|
36
36
|
multiEditingTypes.forEach((type) => {
|
|
37
|
-
expect(
|
|
37
|
+
expect(isSingleRootEditor(type)).toBe(false);
|
|
38
38
|
});
|
|
39
39
|
});
|
|
40
40
|
});
|
package/src/hooks/editor/utils/{is-single-editing-like-editor.ts → is-single-root-editor.ts}
RENAMED
|
@@ -6,6 +6,6 @@ import type { EditorType } from '../typings';
|
|
|
6
6
|
* @param editorType - The type of the editor to check.
|
|
7
7
|
* @returns `true` if the editor type is 'inline', 'classic', or 'balloon', otherwise `false`.
|
|
8
8
|
*/
|
|
9
|
-
export function
|
|
9
|
+
export function isSingleRootEditor(editorType: EditorType): boolean {
|
|
10
10
|
return ['inline', 'classic', 'balloon', 'decoupled'].includes(editorType);
|
|
11
11
|
}
|
|
@@ -1,7 +1,24 @@
|
|
|
1
1
|
import type { EditorId, EditorType } from '../typings';
|
|
2
2
|
|
|
3
3
|
import { filterObjectValues, mapObjectValues } from '../../../shared';
|
|
4
|
-
import {
|
|
4
|
+
import { isSingleRootEditor } from './is-single-root-editor';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Gets the initial root elements for the editor based on its type.
|
|
8
|
+
*
|
|
9
|
+
* @param editorId The editor's ID.
|
|
10
|
+
* @param type The type of the editor.
|
|
11
|
+
* @returns The root element(s) for the editor.
|
|
12
|
+
*/
|
|
13
|
+
export function queryEditablesElements(editorId: EditorId, type: EditorType) {
|
|
14
|
+
if (isSingleRootEditor(type) && type !== 'decoupled') {
|
|
15
|
+
return document.getElementById(`${editorId}_editor`)!;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const editables = queryAllEditorEditables(editorId);
|
|
19
|
+
|
|
20
|
+
return mapObjectValues(editables, ({ element }) => element);
|
|
21
|
+
}
|
|
5
22
|
|
|
6
23
|
/**
|
|
7
24
|
* Queries all editable elements within a specific editor instance.
|
|
@@ -24,75 +41,21 @@ export function queryAllEditorEditables(editorId: EditorId): Record<string, Edit
|
|
|
24
41
|
);
|
|
25
42
|
}
|
|
26
43
|
|
|
27
|
-
/**
|
|
28
|
-
* Gets the initial root elements for the editor based on its type.
|
|
29
|
-
*
|
|
30
|
-
* @param editorId The editor's ID.
|
|
31
|
-
* @param type The type of the editor.
|
|
32
|
-
* @returns The root element(s) for the editor.
|
|
33
|
-
*/
|
|
34
|
-
export function queryEditablesElements(editorId: EditorId, type: EditorType) {
|
|
35
|
-
// While the `decoupled` editor is a single editing-like editor, it has a different structure
|
|
36
|
-
// and requires special handling to get the main editable.
|
|
37
|
-
if (type === 'decoupled') {
|
|
38
|
-
const { element } = queryDecoupledMainEditableOrThrow(editorId);
|
|
39
|
-
|
|
40
|
-
return element;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (isSingleEditingLikeEditor(type)) {
|
|
44
|
-
return document.getElementById(`${editorId}_editor`)!;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const editables = queryAllEditorEditables(editorId);
|
|
48
|
-
|
|
49
|
-
return mapObjectValues(editables, ({ element }) => element);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
44
|
/**
|
|
53
45
|
* Gets the initial data for the roots of the editor. If the editor is a single editing-like editor,
|
|
54
46
|
* it retrieves the initial value from the element's attribute. Otherwise, it returns an object mapping
|
|
55
47
|
* editable names to their initial values.
|
|
56
48
|
*
|
|
57
49
|
* @param editorId The editor's ID.
|
|
58
|
-
* @param type The type of the editor.
|
|
59
50
|
* @returns The initial values for the editor's roots.
|
|
60
51
|
*/
|
|
61
|
-
export function queryEditablesSnapshotContent(editorId: EditorId
|
|
62
|
-
// While the `decoupled` editor is a single editing-like editor, it has a different structure
|
|
63
|
-
// and requires special handling to get the main editable.
|
|
64
|
-
if (type === 'decoupled') {
|
|
65
|
-
const { content } = queryDecoupledMainEditableOrThrow(editorId);
|
|
66
|
-
|
|
67
|
-
// If initial value is not set, then pick it from the editor element.
|
|
68
|
-
if (typeof content === 'string') {
|
|
69
|
-
return {
|
|
70
|
-
main: content,
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
52
|
+
export function queryEditablesSnapshotContent(editorId: EditorId) {
|
|
75
53
|
const editables = queryAllEditorEditables(editorId);
|
|
76
54
|
const values = mapObjectValues(editables, ({ content }) => content);
|
|
77
55
|
|
|
78
56
|
return filterObjectValues(values, value => typeof value === 'string') as Record<string, string>;
|
|
79
57
|
}
|
|
80
58
|
|
|
81
|
-
/**
|
|
82
|
-
* Queries the main editable for a decoupled editor and throws an error if not found.
|
|
83
|
-
*
|
|
84
|
-
* @param editorId The ID of the editor to query.
|
|
85
|
-
*/
|
|
86
|
-
function queryDecoupledMainEditableOrThrow(editorId: EditorId) {
|
|
87
|
-
const mainEditable = queryAllEditorEditables(editorId)['main'];
|
|
88
|
-
|
|
89
|
-
if (!mainEditable) {
|
|
90
|
-
throw new Error(`No "main" editable found for editor with ID "${editorId}".`);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return mainEditable;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
59
|
/**
|
|
97
60
|
* Type representing an editable item within an editor.
|
|
98
61
|
*/
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"is-single-editing-like-editor.d.ts","sourceRoot":"","sources":["../../../../../src/hooks/editor/utils/is-single-editing-like-editor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,UAAU,EAAE,UAAU,GAAG,OAAO,CAEzE"}
|