ckeditor5-livewire 0.0.1
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/context/context.d.ts +39 -0
- package/dist/hooks/context/context.d.ts.map +1 -0
- package/dist/hooks/context/contexts-registry.d.ts +9 -0
- package/dist/hooks/context/contexts-registry.d.ts.map +1 -0
- package/dist/hooks/context/index.d.ts +4 -0
- package/dist/hooks/context/index.d.ts.map +1 -0
- package/dist/hooks/context/typings.d.ts +34 -0
- package/dist/hooks/context/typings.d.ts.map +1 -0
- package/dist/hooks/editable.d.ts +40 -0
- package/dist/hooks/editable.d.ts.map +1 -0
- package/dist/hooks/editor/custom-editor-plugins.d.ts +54 -0
- package/dist/hooks/editor/custom-editor-plugins.d.ts.map +1 -0
- package/dist/hooks/editor/editor.d.ts +69 -0
- package/dist/hooks/editor/editor.d.ts.map +1 -0
- package/dist/hooks/editor/editors-registry.d.ts +9 -0
- package/dist/hooks/editor/editors-registry.d.ts.map +1 -0
- package/dist/hooks/editor/index.d.ts +3 -0
- package/dist/hooks/editor/index.d.ts.map +1 -0
- package/dist/hooks/editor/plugins/index.d.ts +3 -0
- package/dist/hooks/editor/plugins/index.d.ts.map +1 -0
- package/dist/hooks/editor/plugins/livewire-sync.d.ts +19 -0
- package/dist/hooks/editor/plugins/livewire-sync.d.ts.map +1 -0
- package/dist/hooks/editor/plugins/sync-editor-with-input.d.ts +6 -0
- package/dist/hooks/editor/plugins/sync-editor-with-input.d.ts.map +1 -0
- package/dist/hooks/editor/typings.d.ts +99 -0
- package/dist/hooks/editor/typings.d.ts.map +1 -0
- package/dist/hooks/editor/utils/create-editor-in-context.d.ts +44 -0
- package/dist/hooks/editor/utils/create-editor-in-context.d.ts.map +1 -0
- package/dist/hooks/editor/utils/get-editor-roots-values.d.ts +9 -0
- package/dist/hooks/editor/utils/get-editor-roots-values.d.ts.map +1 -0
- package/dist/hooks/editor/utils/index.d.ts +12 -0
- package/dist/hooks/editor/utils/index.d.ts.map +1 -0
- package/dist/hooks/editor/utils/is-single-editing-like-editor.d.ts +9 -0
- package/dist/hooks/editor/utils/is-single-editing-like-editor.d.ts.map +1 -0
- package/dist/hooks/editor/utils/load-editor-constructor.d.ts +9 -0
- package/dist/hooks/editor/utils/load-editor-constructor.d.ts.map +1 -0
- package/dist/hooks/editor/utils/load-editor-plugins.d.ts +20 -0
- package/dist/hooks/editor/utils/load-editor-plugins.d.ts.map +1 -0
- package/dist/hooks/editor/utils/load-editor-translations.d.ts +14 -0
- package/dist/hooks/editor/utils/load-editor-translations.d.ts.map +1 -0
- package/dist/hooks/editor/utils/normalize-custom-translations.d.ts +11 -0
- package/dist/hooks/editor/utils/normalize-custom-translations.d.ts.map +1 -0
- package/dist/hooks/editor/utils/query-editor-editables.d.ts +34 -0
- package/dist/hooks/editor/utils/query-editor-editables.d.ts.map +1 -0
- package/dist/hooks/editor/utils/resolve-editor-config-elements-references.d.ts +9 -0
- package/dist/hooks/editor/utils/resolve-editor-config-elements-references.d.ts.map +1 -0
- package/dist/hooks/editor/utils/set-editor-editable-height.d.ts +9 -0
- package/dist/hooks/editor/utils/set-editor-editable-height.d.ts.map +1 -0
- package/dist/hooks/editor/utils/wrap-with-watchdog.d.ts +24 -0
- package/dist/hooks/editor/utils/wrap-with-watchdog.d.ts.map +1 -0
- package/dist/hooks/hook.d.ts +58 -0
- package/dist/hooks/hook.d.ts.map +1 -0
- package/dist/hooks/index.d.ts +5 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/ui-part.d.ts +32 -0
- package/dist/hooks/ui-part.d.ts.map +1 -0
- package/dist/index.cjs +5 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +1146 -0
- package/dist/index.mjs.map +1 -0
- package/dist/shared/async-registry.d.ts +131 -0
- package/dist/shared/async-registry.d.ts.map +1 -0
- package/dist/shared/camel-case.d.ts +8 -0
- package/dist/shared/camel-case.d.ts.map +1 -0
- package/dist/shared/debounce.d.ts +2 -0
- package/dist/shared/debounce.d.ts.map +1 -0
- package/dist/shared/deep-camel-case-keys.d.ts +8 -0
- package/dist/shared/deep-camel-case-keys.d.ts.map +1 -0
- package/dist/shared/filter-object-values.d.ts +9 -0
- package/dist/shared/filter-object-values.d.ts.map +1 -0
- package/dist/shared/index.d.ts +13 -0
- package/dist/shared/index.d.ts.map +1 -0
- package/dist/shared/is-empty-object.d.ts +2 -0
- package/dist/shared/is-empty-object.d.ts.map +1 -0
- package/dist/shared/is-plain-object.d.ts +8 -0
- package/dist/shared/is-plain-object.d.ts.map +1 -0
- package/dist/shared/map-object-values.d.ts +11 -0
- package/dist/shared/map-object-values.d.ts.map +1 -0
- package/dist/shared/once.d.ts +2 -0
- package/dist/shared/once.d.ts.map +1 -0
- package/dist/shared/timeout.d.ts +8 -0
- package/dist/shared/timeout.d.ts.map +1 -0
- package/dist/shared/uid.d.ts +7 -0
- package/dist/shared/uid.d.ts.map +1 -0
- package/dist/shared/wait-for.d.ts +20 -0
- package/dist/shared/wait-for.d.ts.map +1 -0
- package/dist/types/can-be-promise.type.d.ts +2 -0
- package/dist/types/can-be-promise.type.d.ts.map +1 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/required-by.type.d.ts +2 -0
- package/dist/types/required-by.type.d.ts.map +1 -0
- package/package.json +40 -0
- package/src/hooks/context/context.test.ts +394 -0
- package/src/hooks/context/context.ts +116 -0
- package/src/hooks/context/contexts-registry.test.ts +10 -0
- package/src/hooks/context/contexts-registry.ts +10 -0
- package/src/hooks/context/index.ts +3 -0
- package/src/hooks/context/typings.ts +39 -0
- package/src/hooks/editable.test.ts +276 -0
- package/src/hooks/editable.ts +122 -0
- package/src/hooks/editor/custom-editor-plugins.test.ts +103 -0
- package/src/hooks/editor/custom-editor-plugins.ts +84 -0
- package/src/hooks/editor/editor.test.ts +782 -0
- package/src/hooks/editor/editor.ts +357 -0
- package/src/hooks/editor/editors-registry.test.ts +10 -0
- package/src/hooks/editor/editors-registry.ts +10 -0
- package/src/hooks/editor/index.ts +2 -0
- package/src/hooks/editor/plugins/index.ts +2 -0
- package/src/hooks/editor/plugins/livewire-sync.ts +85 -0
- package/src/hooks/editor/plugins/sync-editor-with-input.ts +76 -0
- package/src/hooks/editor/typings.ts +114 -0
- package/src/hooks/editor/utils/create-editor-in-context.ts +90 -0
- package/src/hooks/editor/utils/get-editor-roots-values.ts +16 -0
- package/src/hooks/editor/utils/index.ts +11 -0
- package/src/hooks/editor/utils/is-single-editing-like-editor.test.ts +40 -0
- package/src/hooks/editor/utils/is-single-editing-like-editor.ts +11 -0
- package/src/hooks/editor/utils/load-editor-constructor.test.ts +62 -0
- package/src/hooks/editor/utils/load-editor-constructor.ts +27 -0
- package/src/hooks/editor/utils/load-editor-plugins.test.ts +100 -0
- package/src/hooks/editor/utils/load-editor-plugins.ts +71 -0
- package/src/hooks/editor/utils/load-editor-translations.ts +233 -0
- package/src/hooks/editor/utils/normalize-custom-translations.test.ts +152 -0
- package/src/hooks/editor/utils/normalize-custom-translations.ts +18 -0
- package/src/hooks/editor/utils/query-editor-editables.ts +102 -0
- package/src/hooks/editor/utils/resolve-editor-config-elements-references.test.ts +93 -0
- package/src/hooks/editor/utils/resolve-editor-config-elements-references.ts +36 -0
- package/src/hooks/editor/utils/set-editor-editable-height.test.ts +131 -0
- package/src/hooks/editor/utils/set-editor-editable-height.ts +15 -0
- package/src/hooks/editor/utils/wrap-with-watchdog.test.ts +45 -0
- package/src/hooks/editor/utils/wrap-with-watchdog.ts +51 -0
- package/src/hooks/hook.ts +87 -0
- package/src/hooks/index.ts +21 -0
- package/src/hooks/ui-part.test.ts +161 -0
- package/src/hooks/ui-part.ts +80 -0
- package/src/index.ts +5 -0
- package/src/livewire.d.ts +42 -0
- package/src/shared/async-registry.test.ts +658 -0
- package/src/shared/async-registry.ts +308 -0
- package/src/shared/camel-case.test.ts +35 -0
- package/src/shared/camel-case.ts +11 -0
- package/src/shared/debounce.test.ts +72 -0
- package/src/shared/debounce.ts +16 -0
- package/src/shared/deep-camel-case-keys.test.ts +34 -0
- package/src/shared/deep-camel-case-keys.ts +26 -0
- package/src/shared/filter-object-values.test.ts +25 -0
- package/src/shared/filter-object-values.ts +17 -0
- package/src/shared/index.ts +12 -0
- package/src/shared/is-empty-object.test.ts +78 -0
- package/src/shared/is-empty-object.ts +3 -0
- package/src/shared/is-plain-object.test.ts +38 -0
- package/src/shared/is-plain-object.ts +15 -0
- package/src/shared/map-object-values.test.ts +29 -0
- package/src/shared/map-object-values.ts +19 -0
- package/src/shared/once.test.ts +116 -0
- package/src/shared/once.ts +12 -0
- package/src/shared/timeout.test.ts +65 -0
- package/src/shared/timeout.ts +13 -0
- package/src/shared/uid.test.ts +25 -0
- package/src/shared/uid.ts +8 -0
- package/src/shared/wait-for.test.ts +24 -0
- package/src/shared/wait-for.ts +56 -0
- package/src/types/can-be-promise.type.ts +1 -0
- package/src/types/index.ts +2 -0
- package/src/types/required-by.type.ts +1 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import type { EditorCustomTranslationsDictionary } from '../typings';
|
|
4
|
+
|
|
5
|
+
import { normalizeCustomTranslations } from './normalize-custom-translations';
|
|
6
|
+
|
|
7
|
+
describe('normalizeCustomTranslations', () => {
|
|
8
|
+
it('should normalize empty translations object', () => {
|
|
9
|
+
const input: EditorCustomTranslationsDictionary = {};
|
|
10
|
+
const result = normalizeCustomTranslations(input);
|
|
11
|
+
|
|
12
|
+
expect(result).toEqual({});
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should normalize single language translation', () => {
|
|
16
|
+
const input: EditorCustomTranslationsDictionary = {
|
|
17
|
+
en: {
|
|
18
|
+
Bold: 'Bold Text',
|
|
19
|
+
Italic: 'Italic Text',
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const result = normalizeCustomTranslations(input);
|
|
24
|
+
|
|
25
|
+
expect(result).toEqual({
|
|
26
|
+
en: {
|
|
27
|
+
dictionary: {
|
|
28
|
+
Bold: 'Bold Text',
|
|
29
|
+
Italic: 'Italic Text',
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should normalize multiple language translations', () => {
|
|
36
|
+
const input: EditorCustomTranslationsDictionary = {
|
|
37
|
+
en: {
|
|
38
|
+
Bold: 'Bold Text',
|
|
39
|
+
Italic: 'Italic Text',
|
|
40
|
+
},
|
|
41
|
+
pl: {
|
|
42
|
+
Bold: 'Pogrubienie',
|
|
43
|
+
Italic: 'Kursywa',
|
|
44
|
+
},
|
|
45
|
+
de: {
|
|
46
|
+
Bold: 'Fett',
|
|
47
|
+
Italic: 'Kursiv',
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const result = normalizeCustomTranslations(input);
|
|
52
|
+
|
|
53
|
+
expect(result).toEqual({
|
|
54
|
+
en: {
|
|
55
|
+
dictionary: {
|
|
56
|
+
Bold: 'Bold Text',
|
|
57
|
+
Italic: 'Italic Text',
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
pl: {
|
|
61
|
+
dictionary: {
|
|
62
|
+
Bold: 'Pogrubienie',
|
|
63
|
+
Italic: 'Kursywa',
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
de: {
|
|
67
|
+
dictionary: {
|
|
68
|
+
Bold: 'Fett',
|
|
69
|
+
Italic: 'Kursiv',
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should handle empty dictionary for a language', () => {
|
|
76
|
+
const input: EditorCustomTranslationsDictionary = {
|
|
77
|
+
en: {},
|
|
78
|
+
pl: {
|
|
79
|
+
Bold: 'Pogrubienie',
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const result = normalizeCustomTranslations(input);
|
|
84
|
+
|
|
85
|
+
expect(result).toEqual({
|
|
86
|
+
en: {
|
|
87
|
+
dictionary: {},
|
|
88
|
+
},
|
|
89
|
+
pl: {
|
|
90
|
+
dictionary: {
|
|
91
|
+
Bold: 'Pogrubienie',
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should preserve special characters and unicode in translations', () => {
|
|
98
|
+
const input: EditorCustomTranslationsDictionary = {
|
|
99
|
+
zh: {
|
|
100
|
+
粗体: '加粗文本',
|
|
101
|
+
斜体: '斜体文本',
|
|
102
|
+
},
|
|
103
|
+
ar: {
|
|
104
|
+
Bold: 'عريض',
|
|
105
|
+
Italic: 'مائل',
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const result = normalizeCustomTranslations(input);
|
|
110
|
+
|
|
111
|
+
expect(result).toEqual({
|
|
112
|
+
zh: {
|
|
113
|
+
dictionary: {
|
|
114
|
+
粗体: '加粗文本',
|
|
115
|
+
斜体: '斜体文本',
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
ar: {
|
|
119
|
+
dictionary: {
|
|
120
|
+
Bold: 'عريض',
|
|
121
|
+
Italic: 'مائل',
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should handle language codes with regions', () => {
|
|
128
|
+
const input: EditorCustomTranslationsDictionary = {
|
|
129
|
+
'en-US': {
|
|
130
|
+
Color: 'Color',
|
|
131
|
+
},
|
|
132
|
+
'en-GB': {
|
|
133
|
+
Color: 'Colour',
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const result = normalizeCustomTranslations(input);
|
|
138
|
+
|
|
139
|
+
expect(result).toEqual({
|
|
140
|
+
'en-US': {
|
|
141
|
+
dictionary: {
|
|
142
|
+
Color: 'Color',
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
'en-GB': {
|
|
146
|
+
dictionary: {
|
|
147
|
+
Color: 'Colour',
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Translations } from 'ckeditor5';
|
|
2
|
+
|
|
3
|
+
import type { EditorCustomTranslationsDictionary } from '../typings';
|
|
4
|
+
|
|
5
|
+
import { mapObjectValues } from '../../../shared';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* This function takes a custom translations object and maps it to the format expected by CKEditor5.
|
|
9
|
+
* Each translation dictionary is wrapped in an object with a `dictionary` key.
|
|
10
|
+
*
|
|
11
|
+
* @param translations - The custom translations to normalize.
|
|
12
|
+
* @returns A normalized translations object suitable for CKEditor5.
|
|
13
|
+
*/
|
|
14
|
+
export function normalizeCustomTranslations(translations: EditorCustomTranslationsDictionary): Translations {
|
|
15
|
+
return mapObjectValues(translations, dictionary => ({
|
|
16
|
+
dictionary,
|
|
17
|
+
}));
|
|
18
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { EditorId, EditorType } from '../typings';
|
|
2
|
+
|
|
3
|
+
import { filterObjectValues, mapObjectValues } from '../../../shared';
|
|
4
|
+
import { isSingleEditingLikeEditor } from './is-single-editing-like-editor';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Queries all editable elements within a specific editor instance.
|
|
8
|
+
*
|
|
9
|
+
* @param editorId The ID of the editor to query.
|
|
10
|
+
* @returns An object mapping editable names to their corresponding elements and initial values.
|
|
11
|
+
*/
|
|
12
|
+
export function queryAllEditorEditables(editorId: EditorId): Record<string, EditableItem> {
|
|
13
|
+
return (
|
|
14
|
+
window.Livewire
|
|
15
|
+
.all()
|
|
16
|
+
.filter(({ name, ephemeral }) => name === 'ckeditor5-editable' && ephemeral['editorId'] === editorId)
|
|
17
|
+
.reduce<Record<string, EditableItem>>((acc, { ephemeral, el }) => ({
|
|
18
|
+
...acc,
|
|
19
|
+
[ephemeral['rootName'] as string]: {
|
|
20
|
+
element: el.querySelector('[data-cke-editable-content]')!,
|
|
21
|
+
content: ephemeral['content'],
|
|
22
|
+
},
|
|
23
|
+
}), Object.create({}))
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
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
|
+
/**
|
|
53
|
+
* Gets the initial data for the roots of the editor. If the editor is a single editing-like editor,
|
|
54
|
+
* it retrieves the initial value from the element's attribute. Otherwise, it returns an object mapping
|
|
55
|
+
* editable names to their initial values.
|
|
56
|
+
*
|
|
57
|
+
* @param editorId The editor's ID.
|
|
58
|
+
* @param type The type of the editor.
|
|
59
|
+
* @returns The initial values for the editor's roots.
|
|
60
|
+
*/
|
|
61
|
+
export function queryEditablesSnapshotContent(editorId: EditorId, type: EditorType) {
|
|
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
|
+
|
|
75
|
+
const editables = queryAllEditorEditables(editorId);
|
|
76
|
+
const values = mapObjectValues(editables, ({ content }) => content);
|
|
77
|
+
|
|
78
|
+
return filterObjectValues(values, value => typeof value === 'string') as Record<string, string>;
|
|
79
|
+
}
|
|
80
|
+
|
|
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
|
+
/**
|
|
97
|
+
* Type representing an editable item within an editor.
|
|
98
|
+
*/
|
|
99
|
+
export type EditableItem = {
|
|
100
|
+
element: HTMLElement;
|
|
101
|
+
content: string | null;
|
|
102
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { resolveEditorConfigElementReferences } from './resolve-editor-config-elements-references';
|
|
4
|
+
|
|
5
|
+
describe('resolveEditorConfigElementReferences', () => {
|
|
6
|
+
afterEach(() => {
|
|
7
|
+
document.body.innerHTML = '';
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('resolves a single element reference', () => {
|
|
11
|
+
const div = document.createElement('div');
|
|
12
|
+
div.id = 'test-div';
|
|
13
|
+
document.body.appendChild(div);
|
|
14
|
+
|
|
15
|
+
const config = {
|
|
16
|
+
foo: { $element: '#test-div' },
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const result = resolveEditorConfigElementReferences(config);
|
|
20
|
+
expect(result.foo).toBe(div);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('returns null if element not found', () => {
|
|
24
|
+
const config = {
|
|
25
|
+
foo: { $element: '#not-exist' },
|
|
26
|
+
};
|
|
27
|
+
const result = resolveEditorConfigElementReferences(config);
|
|
28
|
+
|
|
29
|
+
expect(result.foo).toBeNull();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('recursively resolves nested element references', () => {
|
|
33
|
+
const span = document.createElement('span');
|
|
34
|
+
span.className = 'my-span';
|
|
35
|
+
document.body.appendChild(span);
|
|
36
|
+
|
|
37
|
+
const config = {
|
|
38
|
+
nested: {
|
|
39
|
+
bar: { $element: '.my-span' },
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const result = resolveEditorConfigElementReferences(config);
|
|
44
|
+
expect(result.nested.bar).toBe(span);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('resolves element references in arrays', () => {
|
|
48
|
+
const el1 = document.createElement('div');
|
|
49
|
+
el1.id = 'el1';
|
|
50
|
+
document.body.appendChild(el1);
|
|
51
|
+
|
|
52
|
+
const el2 = document.createElement('div');
|
|
53
|
+
el2.id = 'el2';
|
|
54
|
+
document.body.appendChild(el2);
|
|
55
|
+
|
|
56
|
+
const config = [
|
|
57
|
+
{ $element: '#el1' },
|
|
58
|
+
{ $element: '#el2' },
|
|
59
|
+
{ notElement: 123 },
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
const result = resolveEditorConfigElementReferences(config);
|
|
63
|
+
|
|
64
|
+
expect(result[0]).toBe(el1);
|
|
65
|
+
expect(result[1]).toBe(el2);
|
|
66
|
+
expect(result[2]).toEqual({ notElement: 123 });
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('returns primitives as is', () => {
|
|
70
|
+
expect(resolveEditorConfigElementReferences(42)).toBe(42);
|
|
71
|
+
expect(resolveEditorConfigElementReferences('foo')).toBe('foo');
|
|
72
|
+
expect(resolveEditorConfigElementReferences(null)).toBe(null);
|
|
73
|
+
expect(resolveEditorConfigElementReferences(undefined)).toBe(undefined);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('warns for invalid selector type', () => {
|
|
77
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
78
|
+
const config = { foo: { $element: '.foo' } };
|
|
79
|
+
|
|
80
|
+
resolveEditorConfigElementReferences(config);
|
|
81
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Element not found for selector: .foo'));
|
|
82
|
+
warnSpy.mockRestore();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('warns if element not found', () => {
|
|
86
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
87
|
+
const config = { foo: { $element: '#not-found' } };
|
|
88
|
+
|
|
89
|
+
resolveEditorConfigElementReferences(config);
|
|
90
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Element not found'));
|
|
91
|
+
warnSpy.mockRestore();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves element references in configuration object.
|
|
3
|
+
* Looks for objects with { $element: "selector" } format and replaces them with actual DOM elements.
|
|
4
|
+
*
|
|
5
|
+
* @param obj - Configuration object to process
|
|
6
|
+
* @returns Processed configuration object with resolved element references
|
|
7
|
+
*/
|
|
8
|
+
export function resolveEditorConfigElementReferences<T>(obj: T): T {
|
|
9
|
+
if (!obj || typeof obj !== 'object') {
|
|
10
|
+
return obj;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (Array.isArray(obj)) {
|
|
14
|
+
return obj.map(item => resolveEditorConfigElementReferences(item)) as T;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const anyObj = obj as any;
|
|
18
|
+
|
|
19
|
+
if (anyObj.$element && typeof anyObj.$element === 'string') {
|
|
20
|
+
const element = document.querySelector(anyObj.$element);
|
|
21
|
+
|
|
22
|
+
if (!element) {
|
|
23
|
+
console.warn(`Element not found for selector: ${anyObj.$element}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return (element || null) as T;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const result = Object.create(null);
|
|
30
|
+
|
|
31
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
32
|
+
result[key] = resolveEditorConfigElementReferences(value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return result as T;
|
|
36
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { setEditorEditableHeight } from './set-editor-editable-height';
|
|
4
|
+
|
|
5
|
+
// Mock CKEditor5 types and interfaces
|
|
6
|
+
const mockWriter = {
|
|
7
|
+
setStyle: vi.fn(),
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const mockRoot = {
|
|
11
|
+
// Mock root element
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const mockViewDocument = {
|
|
15
|
+
getRoot: vi.fn(() => mockRoot),
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const mockView = {
|
|
19
|
+
change: vi.fn(callback => callback(mockWriter)),
|
|
20
|
+
document: mockViewDocument,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const mockEditing = {
|
|
24
|
+
view: mockView,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const mockEditor = {
|
|
28
|
+
editing: mockEditing,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
describe('setEditorEditableHeight', () => {
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
vi.clearAllMocks();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should set height style on editor root element', () => {
|
|
37
|
+
const height = 300;
|
|
38
|
+
|
|
39
|
+
setEditorEditableHeight(mockEditor as any, height);
|
|
40
|
+
|
|
41
|
+
expect(mockView.change).toHaveBeenCalledWith(expect.any(Function));
|
|
42
|
+
expect(mockViewDocument.getRoot).toHaveBeenCalled();
|
|
43
|
+
expect(mockWriter.setStyle).toHaveBeenCalledWith('height', '300px', mockRoot);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should handle different height values', () => {
|
|
47
|
+
const heights = [100, 200, 500, 1000];
|
|
48
|
+
|
|
49
|
+
heights.forEach((height) => {
|
|
50
|
+
vi.clearAllMocks();
|
|
51
|
+
|
|
52
|
+
setEditorEditableHeight(mockEditor as any, height);
|
|
53
|
+
|
|
54
|
+
expect(mockWriter.setStyle).toHaveBeenCalledWith('height', `${height}px`, mockRoot);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should handle zero height', () => {
|
|
59
|
+
const height = 0;
|
|
60
|
+
|
|
61
|
+
setEditorEditableHeight(mockEditor as any, height);
|
|
62
|
+
|
|
63
|
+
expect(mockWriter.setStyle).toHaveBeenCalledWith('height', '0px', mockRoot);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should handle negative height values', () => {
|
|
67
|
+
const height = -100;
|
|
68
|
+
|
|
69
|
+
setEditorEditableHeight(mockEditor as any, height);
|
|
70
|
+
|
|
71
|
+
expect(mockWriter.setStyle).toHaveBeenCalledWith('height', '-100px', mockRoot);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should handle decimal height values', () => {
|
|
75
|
+
const height = 250.5;
|
|
76
|
+
|
|
77
|
+
setEditorEditableHeight(mockEditor as any, height);
|
|
78
|
+
|
|
79
|
+
expect(mockWriter.setStyle).toHaveBeenCalledWith('height', '250.5px', mockRoot);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should call view.change with correct callback', () => {
|
|
83
|
+
const height = 400;
|
|
84
|
+
|
|
85
|
+
setEditorEditableHeight(mockEditor as any, height);
|
|
86
|
+
|
|
87
|
+
expect(mockView.change).toHaveBeenCalledTimes(1);
|
|
88
|
+
expect(mockView.change).toHaveBeenCalledWith(expect.any(Function));
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should work with different editor instances', () => {
|
|
92
|
+
const anotherMockEditor = {
|
|
93
|
+
editing: {
|
|
94
|
+
view: {
|
|
95
|
+
change: vi.fn(callback => callback(mockWriter)),
|
|
96
|
+
document: {
|
|
97
|
+
getRoot: vi.fn(() => mockRoot),
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const height = 350;
|
|
104
|
+
|
|
105
|
+
setEditorEditableHeight(anotherMockEditor as any, height);
|
|
106
|
+
|
|
107
|
+
expect(anotherMockEditor.editing.view.change).toHaveBeenCalledWith(expect.any(Function));
|
|
108
|
+
expect(anotherMockEditor.editing.view.document.getRoot).toHaveBeenCalled();
|
|
109
|
+
expect(mockWriter.setStyle).toHaveBeenCalledWith('height', '350px', mockRoot);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should handle editor with null root gracefully', () => {
|
|
113
|
+
const mockEditorWithNullRoot = {
|
|
114
|
+
editing: {
|
|
115
|
+
view: {
|
|
116
|
+
change: vi.fn(callback => callback(mockWriter)),
|
|
117
|
+
document: {
|
|
118
|
+
getRoot: vi.fn(() => null),
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const height = 200;
|
|
125
|
+
|
|
126
|
+
// Should not throw error even with null root
|
|
127
|
+
expect(() => setEditorEditableHeight(mockEditorWithNullRoot as any, height)).not.toThrow();
|
|
128
|
+
|
|
129
|
+
expect(mockWriter.setStyle).toHaveBeenCalledWith('height', '200px', null);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Editor } from 'ckeditor5';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sets the height of the editable area in the CKEditor instance.
|
|
5
|
+
*
|
|
6
|
+
* @param instance - The CKEditor instance to modify.
|
|
7
|
+
* @param height - The height in pixels to set for the editable area.
|
|
8
|
+
*/
|
|
9
|
+
export function setEditorEditableHeight(instance: Editor, height: number): void {
|
|
10
|
+
const { editing } = instance;
|
|
11
|
+
|
|
12
|
+
editing.view.change((writer) => {
|
|
13
|
+
writer.setStyle('height', `${height}px`, editing.view.document.getRoot()!);
|
|
14
|
+
});
|
|
15
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { ClassicEditor, EditorWatchdog } from 'ckeditor5';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { unwrapEditorWatchdog, wrapWithWatchdog } from './wrap-with-watchdog';
|
|
5
|
+
|
|
6
|
+
describe('wrap with watchdog', () => {
|
|
7
|
+
let element: HTMLElement;
|
|
8
|
+
|
|
9
|
+
beforeEach(async () => {
|
|
10
|
+
element = document.createElement('div');
|
|
11
|
+
document.body.appendChild(element);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
element.remove();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('returns editor instance after calling Constructor.create', async () => {
|
|
19
|
+
const { Constructor } = await wrapWithWatchdog(ClassicEditor);
|
|
20
|
+
const editor = await Constructor.create(element, {
|
|
21
|
+
licenseKey: 'GPL',
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
expect(editor).toBeInstanceOf(ClassicEditor);
|
|
25
|
+
|
|
26
|
+
await editor.destroy();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('returns instance of watchdog', async () => {
|
|
30
|
+
const { watchdog } = await wrapWithWatchdog(ClassicEditor);
|
|
31
|
+
|
|
32
|
+
expect(watchdog).toBeInstanceOf(EditorWatchdog);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should be possible to unwrap watchdog from editor instance', async () => {
|
|
36
|
+
const { Constructor } = await wrapWithWatchdog(ClassicEditor);
|
|
37
|
+
const editor = await Constructor.create(element, {
|
|
38
|
+
licenseKey: 'GPL',
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
expect(unwrapEditorWatchdog(editor)).toBeInstanceOf(EditorWatchdog);
|
|
42
|
+
|
|
43
|
+
await editor.destroy();
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Editor, EditorWatchdog } from 'ckeditor5';
|
|
2
|
+
|
|
3
|
+
const EDITOR_WATCHDOG_SYMBOL = Symbol.for('elixir-editor-watchdog');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Wraps an Editor creator with a watchdog for automatic recovery.
|
|
7
|
+
*
|
|
8
|
+
* @param Editor - The Editor creator to wrap.
|
|
9
|
+
* @returns The Editor creator wrapped with a watchdog.
|
|
10
|
+
*/
|
|
11
|
+
export async function wrapWithWatchdog(Editor: EditorCreator) {
|
|
12
|
+
const { EditorWatchdog } = await import('ckeditor5');
|
|
13
|
+
const watchdog = new EditorWatchdog(Editor);
|
|
14
|
+
|
|
15
|
+
watchdog.setCreator(async (...args: Parameters<typeof Editor['create']>) => {
|
|
16
|
+
const editor = await Editor.create(...args);
|
|
17
|
+
|
|
18
|
+
(editor as any)[EDITOR_WATCHDOG_SYMBOL] = watchdog;
|
|
19
|
+
|
|
20
|
+
return editor;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
watchdog,
|
|
25
|
+
Constructor: {
|
|
26
|
+
create: async (...args: Parameters<typeof Editor['create']>) => {
|
|
27
|
+
await watchdog.create(...args);
|
|
28
|
+
|
|
29
|
+
return watchdog.editor!;
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Unwraps the EditorWatchdog from the editor instance.
|
|
37
|
+
*/
|
|
38
|
+
export function unwrapEditorWatchdog(editor: Editor): EditorWatchdog | null {
|
|
39
|
+
if (EDITOR_WATCHDOG_SYMBOL in editor) {
|
|
40
|
+
return (editor as any)[EDITOR_WATCHDOG_SYMBOL] as EditorWatchdog;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Type representing an Editor creator with a create method.
|
|
48
|
+
*/
|
|
49
|
+
export type EditorCreator = {
|
|
50
|
+
create: (...args: any) => Promise<Editor>;
|
|
51
|
+
};
|