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
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ckeditor5-livewire",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "CKEditor 5 integration for Laravel Livewire",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": {
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"import": "./dist/index.mjs",
|
|
9
|
+
"require": "./dist/index.cjs"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"main": "dist/index.cjs",
|
|
13
|
+
"module": "dist/index.mjs",
|
|
14
|
+
"types": "dist/index.d.ts",
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"src"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"clean": "rm -rf dist",
|
|
21
|
+
"prepare": "pnpm run build",
|
|
22
|
+
"build": "pnpm run clean && vite build && node ./scripts/check-dist-imports.mjs",
|
|
23
|
+
"watch": "vite build --watch",
|
|
24
|
+
"dev": "vite build --watch",
|
|
25
|
+
"typecheck": "tsc --noEmit --pretty",
|
|
26
|
+
"test": "vitest --coverage",
|
|
27
|
+
"test:watch": "vitest --watch --coverage"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
31
|
+
"ckeditor5": "^45.2.2",
|
|
32
|
+
"ckeditor5-premium-features": "^45.2.1",
|
|
33
|
+
"happy-dom": "^20.0.1",
|
|
34
|
+
"typescript": "^5.5.4",
|
|
35
|
+
"vite": "^7.1.11",
|
|
36
|
+
"vite-plugin-dts": "^4.5.4",
|
|
37
|
+
"vite-tsconfig-paths": "^5.1.4",
|
|
38
|
+
"vitest": "^3.2.3"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import { Context, ContextPlugin, ContextWatchdog } from 'ckeditor5';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
createContextHtmlElement,
|
|
6
|
+
createContextSnapshot,
|
|
7
|
+
createEditorHtmlElement,
|
|
8
|
+
createEditorSnapshot,
|
|
9
|
+
DEFAULT_TEST_CONTEXT_ID,
|
|
10
|
+
LivewireStub,
|
|
11
|
+
waitForTestContext,
|
|
12
|
+
waitForTestEditor,
|
|
13
|
+
} from '~/test-utils';
|
|
14
|
+
|
|
15
|
+
import type { Snapshot as EditorSnapshot } from '../editor';
|
|
16
|
+
|
|
17
|
+
import { timeout } from '../../shared';
|
|
18
|
+
import { EditorComponentHook } from '../editor';
|
|
19
|
+
import { CustomEditorPluginsRegistry } from '../editor/custom-editor-plugins';
|
|
20
|
+
import { EditorsRegistry } from '../editor/editors-registry';
|
|
21
|
+
import { registerLivewireComponentHook } from '../hook';
|
|
22
|
+
import { ContextComponentHook } from './context';
|
|
23
|
+
import { ContextsRegistry } from './contexts-registry';
|
|
24
|
+
|
|
25
|
+
describe('context component', () => {
|
|
26
|
+
let livewireStub: LivewireStub;
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
document.body.innerHTML = '';
|
|
30
|
+
livewireStub = window.Livewire = new LivewireStub();
|
|
31
|
+
|
|
32
|
+
registerLivewireComponentHook('ckeditor5', EditorComponentHook);
|
|
33
|
+
registerLivewireComponentHook('ckeditor5-context', ContextComponentHook);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
afterEach(async () => {
|
|
37
|
+
CustomEditorPluginsRegistry.the.unregisterAll();
|
|
38
|
+
await livewireStub.$internal.destroy();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('mount', () => {
|
|
42
|
+
it('should save the context instance in the registry with provided id', async () => {
|
|
43
|
+
livewireStub.$internal.appendComponentToDOM({
|
|
44
|
+
name: 'ckeditor5-context',
|
|
45
|
+
el: createContextHtmlElement(),
|
|
46
|
+
ephemeral: createContextSnapshot('my-context'),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const watchdog = await ContextsRegistry.the.waitFor('my-context');
|
|
50
|
+
|
|
51
|
+
expect(watchdog).toBeInstanceOf(ContextWatchdog);
|
|
52
|
+
expect(watchdog.context).toBeInstanceOf(Context);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should initialize context with empty creator config', async () => {
|
|
56
|
+
livewireStub.$internal.appendComponentToDOM({
|
|
57
|
+
name: 'ckeditor5-context',
|
|
58
|
+
el: createContextHtmlElement(),
|
|
59
|
+
ephemeral: createContextSnapshot(DEFAULT_TEST_CONTEXT_ID, {
|
|
60
|
+
config: {},
|
|
61
|
+
}),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
expect(await waitForTestContext()).toBeInstanceOf(ContextWatchdog);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should print console error if `itemError` event is fired', async () => {
|
|
68
|
+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
69
|
+
|
|
70
|
+
livewireStub.$internal.appendComponentToDOM({
|
|
71
|
+
name: 'ckeditor5-context',
|
|
72
|
+
el: createContextHtmlElement(),
|
|
73
|
+
ephemeral: createContextSnapshot(),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const watchdog = await waitForTestContext();
|
|
77
|
+
|
|
78
|
+
(watchdog as any)._fire('itemError', 'test-error', { some: 'data' });
|
|
79
|
+
|
|
80
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith('Context item error:', null, 'test-error', { some: 'data' });
|
|
81
|
+
|
|
82
|
+
consoleErrorSpy.mockRestore();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should initialize custom plugins passed to context', async () => {
|
|
86
|
+
class CustomPlugin extends ContextPlugin {
|
|
87
|
+
static get pluginName() {
|
|
88
|
+
return 'CustomPlugin';
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
CustomEditorPluginsRegistry.the.register('CustomPlugin', () => CustomPlugin);
|
|
93
|
+
|
|
94
|
+
livewireStub.$internal.appendComponentToDOM({
|
|
95
|
+
name: 'ckeditor5-context',
|
|
96
|
+
el: createContextHtmlElement(),
|
|
97
|
+
ephemeral: createContextSnapshot(DEFAULT_TEST_CONTEXT_ID, {
|
|
98
|
+
config: {
|
|
99
|
+
plugins: ['CustomPlugin'],
|
|
100
|
+
},
|
|
101
|
+
}),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const { context } = await waitForTestContext();
|
|
105
|
+
|
|
106
|
+
expect(context?.plugins.get('CustomPlugin')).toBeInstanceOf(CustomPlugin);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('registered plugins should support custom translations', async () => {
|
|
110
|
+
class CustomPlugin extends ContextPlugin {
|
|
111
|
+
static get pluginName() {
|
|
112
|
+
return 'CustomPlugin';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
getHelloTitle() {
|
|
116
|
+
return this.context.t('HELLO');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
CustomEditorPluginsRegistry.the.register('CustomPlugin', () => CustomPlugin);
|
|
121
|
+
|
|
122
|
+
livewireStub.$internal.appendComponentToDOM({
|
|
123
|
+
name: 'ckeditor5-context',
|
|
124
|
+
el: createContextHtmlElement(),
|
|
125
|
+
ephemeral: createContextSnapshot(DEFAULT_TEST_CONTEXT_ID, {
|
|
126
|
+
customTranslations: {
|
|
127
|
+
en: {
|
|
128
|
+
HELLO: 'Hello from CustomPlugin',
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
config: {
|
|
132
|
+
plugins: ['CustomPlugin'],
|
|
133
|
+
},
|
|
134
|
+
}),
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const { context } = await waitForTestContext();
|
|
138
|
+
const plugin = context?.plugins.get('CustomPlugin') as CustomPlugin;
|
|
139
|
+
|
|
140
|
+
expect(plugin.getHelloTitle()).toBe('Hello from CustomPlugin');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should support custom language for context translations', async () => {
|
|
144
|
+
class CustomPlugin extends ContextPlugin {
|
|
145
|
+
static get pluginName() {
|
|
146
|
+
return 'CustomPlugin';
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
getHelloTitle() {
|
|
150
|
+
return this.context.t('HELLO');
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
CustomEditorPluginsRegistry.the.register('CustomPlugin', () => CustomPlugin);
|
|
155
|
+
|
|
156
|
+
livewireStub.$internal.appendComponentToDOM({
|
|
157
|
+
name: 'ckeditor5-context',
|
|
158
|
+
el: createContextHtmlElement(),
|
|
159
|
+
ephemeral: createContextSnapshot(
|
|
160
|
+
DEFAULT_TEST_CONTEXT_ID,
|
|
161
|
+
{
|
|
162
|
+
customTranslations: {
|
|
163
|
+
en: {
|
|
164
|
+
HELLO: 'Hello from CustomPlugin',
|
|
165
|
+
},
|
|
166
|
+
pl: {
|
|
167
|
+
HELLO: 'Witaj z CustomPlugin',
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
config: {
|
|
171
|
+
plugins: ['CustomPlugin'],
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
ui: 'pl',
|
|
176
|
+
content: 'pl',
|
|
177
|
+
},
|
|
178
|
+
),
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const { context } = await waitForTestContext();
|
|
182
|
+
const plugin = context?.plugins.get('CustomPlugin') as CustomPlugin;
|
|
183
|
+
|
|
184
|
+
expect(plugin.getHelloTitle()).toBe('Witaj z CustomPlugin');
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe('attaching editor', () => {
|
|
189
|
+
it('should not attach editor to the context (editor has no specified context, context initialized)', async () => {
|
|
190
|
+
livewireStub.$internal.appendComponentToDOM({
|
|
191
|
+
name: 'ckeditor5-context',
|
|
192
|
+
el: createContextHtmlElement(),
|
|
193
|
+
ephemeral: createContextSnapshot(),
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const { context } = await waitForTestContext();
|
|
197
|
+
|
|
198
|
+
livewireStub.$internal.appendComponentToDOM<EditorSnapshot>({
|
|
199
|
+
name: 'ckeditor5',
|
|
200
|
+
el: createEditorHtmlElement(),
|
|
201
|
+
ephemeral: createEditorSnapshot(),
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
await waitForTestEditor();
|
|
205
|
+
|
|
206
|
+
expect(context?.editors.first).toBeNull();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('should attach editor to the context (editor has specified context, context initialized)', async () => {
|
|
210
|
+
livewireStub.$internal.appendComponentToDOM({
|
|
211
|
+
name: 'ckeditor5-context',
|
|
212
|
+
el: createContextHtmlElement(),
|
|
213
|
+
ephemeral: createContextSnapshot(),
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const { context } = await waitForTestContext();
|
|
217
|
+
|
|
218
|
+
livewireStub.$internal.appendComponentToDOM<EditorSnapshot>({
|
|
219
|
+
name: 'ckeditor5',
|
|
220
|
+
el: createEditorHtmlElement(),
|
|
221
|
+
ephemeral: {
|
|
222
|
+
...createEditorSnapshot(),
|
|
223
|
+
contextId: DEFAULT_TEST_CONTEXT_ID,
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const editor = await waitForTestEditor();
|
|
228
|
+
|
|
229
|
+
expect(context?.editors.first).toEqual(editor);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should pause editor initialization when context is not yet initialized', async () => {
|
|
233
|
+
livewireStub.$internal.appendComponentToDOM<EditorSnapshot>({
|
|
234
|
+
name: 'ckeditor5',
|
|
235
|
+
el: createEditorHtmlElement(),
|
|
236
|
+
ephemeral: {
|
|
237
|
+
...createEditorSnapshot(),
|
|
238
|
+
contextId: DEFAULT_TEST_CONTEXT_ID,
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
await timeout(50);
|
|
243
|
+
|
|
244
|
+
livewireStub.$internal.appendComponentToDOM({
|
|
245
|
+
name: 'ckeditor5-context',
|
|
246
|
+
el: createContextHtmlElement(),
|
|
247
|
+
ephemeral: createContextSnapshot(),
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const { context } = await waitForTestContext();
|
|
251
|
+
const editor = await waitForTestEditor();
|
|
252
|
+
|
|
253
|
+
expect(context?.editors.first).toEqual(editor);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('should be possible to attach multiple editors to the same context', async () => {
|
|
257
|
+
livewireStub.$internal.appendComponentToDOM({
|
|
258
|
+
name: 'ckeditor5-context',
|
|
259
|
+
el: createContextHtmlElement(),
|
|
260
|
+
ephemeral: createContextSnapshot(),
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const { context } = await waitForTestContext();
|
|
264
|
+
|
|
265
|
+
livewireStub.$internal.appendComponentToDOM<EditorSnapshot>({
|
|
266
|
+
name: 'ckeditor5',
|
|
267
|
+
el: createEditorHtmlElement({ id: 'editor-1' }),
|
|
268
|
+
ephemeral: {
|
|
269
|
+
...createEditorSnapshot(),
|
|
270
|
+
editorId: 'editor-1',
|
|
271
|
+
contextId: DEFAULT_TEST_CONTEXT_ID,
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
livewireStub.$internal.appendComponentToDOM<EditorSnapshot>({
|
|
276
|
+
name: 'ckeditor5',
|
|
277
|
+
el: createEditorHtmlElement({ id: 'editor-2' }),
|
|
278
|
+
ephemeral: {
|
|
279
|
+
...createEditorSnapshot(),
|
|
280
|
+
editorId: 'editor-2',
|
|
281
|
+
contextId: DEFAULT_TEST_CONTEXT_ID,
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
const editors = await Promise.all([
|
|
286
|
+
waitForTestEditor('editor-1'),
|
|
287
|
+
waitForTestEditor('editor-2'),
|
|
288
|
+
]);
|
|
289
|
+
|
|
290
|
+
expect(context?.editors.length).toBe(2);
|
|
291
|
+
expect([...context!.editors]).toEqual(editors);
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
describe('destroy', () => {
|
|
296
|
+
it('destroyed editor is removed from the context editors collection', async () => {
|
|
297
|
+
livewireStub.$internal.appendComponentToDOM({
|
|
298
|
+
name: 'ckeditor5-context',
|
|
299
|
+
el: createContextHtmlElement(),
|
|
300
|
+
ephemeral: createContextSnapshot(),
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const { context } = await waitForTestContext();
|
|
304
|
+
|
|
305
|
+
const { id: editorId } = livewireStub.$internal.appendComponentToDOM<EditorSnapshot>({
|
|
306
|
+
name: 'ckeditor5',
|
|
307
|
+
el: createEditorHtmlElement(),
|
|
308
|
+
ephemeral: {
|
|
309
|
+
...createEditorSnapshot(),
|
|
310
|
+
contextId: DEFAULT_TEST_CONTEXT_ID,
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const editor = await waitForTestEditor();
|
|
315
|
+
|
|
316
|
+
expect(context?.editors.first).toEqual(editor);
|
|
317
|
+
|
|
318
|
+
await livewireStub.$internal.unmountComponent(editorId);
|
|
319
|
+
|
|
320
|
+
expect(context?.editors.first).toBeNull();
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('should remove the context instance from the registry on destroy', async () => {
|
|
324
|
+
const { id } = livewireStub.$internal.appendComponentToDOM({
|
|
325
|
+
name: 'ckeditor5-context',
|
|
326
|
+
el: createContextHtmlElement(),
|
|
327
|
+
ephemeral: createContextSnapshot('my-context'),
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
const watchdog = await ContextsRegistry.the.waitFor('my-context');
|
|
331
|
+
|
|
332
|
+
expect(watchdog).toBeInstanceOf(ContextWatchdog);
|
|
333
|
+
|
|
334
|
+
await livewireStub.$internal.unmountComponent(id);
|
|
335
|
+
|
|
336
|
+
expect(ContextsRegistry.the.hasItem('my-context')).toBe(false);
|
|
337
|
+
expect(watchdog.state).toBe('destroyed');
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('should hide element during destruction', async () => {
|
|
341
|
+
const { id, el } = livewireStub.$internal.appendComponentToDOM({
|
|
342
|
+
name: 'ckeditor5-context',
|
|
343
|
+
el: createContextHtmlElement(),
|
|
344
|
+
ephemeral: createContextSnapshot(),
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
await livewireStub.$internal.unmountComponent(id);
|
|
348
|
+
|
|
349
|
+
expect(el.style.display).toBe('none');
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('should handle destruction when mounted promise is not resolved yet', async () => {
|
|
353
|
+
const { id, el } = livewireStub.$internal.appendComponentToDOM({
|
|
354
|
+
name: 'ckeditor5-context',
|
|
355
|
+
el: createContextHtmlElement(),
|
|
356
|
+
ephemeral: createContextSnapshot(),
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
await livewireStub.$internal.unmountComponent(id);
|
|
360
|
+
|
|
361
|
+
expect(el.style.display).toBe('none');
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('destroying context should destroy all attached editors', async () => {
|
|
365
|
+
const { id: contextId } = livewireStub.$internal.appendComponentToDOM({
|
|
366
|
+
name: 'ckeditor5-context',
|
|
367
|
+
el: createContextHtmlElement(),
|
|
368
|
+
ephemeral: createContextSnapshot(),
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
await waitForTestContext();
|
|
372
|
+
|
|
373
|
+
const { id: editorId } = livewireStub.$internal.appendComponentToDOM<EditorSnapshot>({
|
|
374
|
+
name: 'ckeditor5',
|
|
375
|
+
el: createEditorHtmlElement(),
|
|
376
|
+
ephemeral: {
|
|
377
|
+
...createEditorSnapshot(),
|
|
378
|
+
contextId: DEFAULT_TEST_CONTEXT_ID,
|
|
379
|
+
},
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
const editor = await waitForTestEditor();
|
|
383
|
+
|
|
384
|
+
expect(editor.state).toBe('ready');
|
|
385
|
+
|
|
386
|
+
await livewireStub.$internal.unmountComponent(contextId);
|
|
387
|
+
|
|
388
|
+
expect(editor.state).toBe('destroyed');
|
|
389
|
+
|
|
390
|
+
expect(ContextsRegistry.the.hasItem(DEFAULT_TEST_CONTEXT_ID)).toBe(false);
|
|
391
|
+
expect(EditorsRegistry.the.hasItem(editorId)).toBe(false);
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { Context, ContextWatchdog } from 'ckeditor5';
|
|
2
|
+
|
|
3
|
+
import type { EditorLanguage } from '../editor';
|
|
4
|
+
import type { ContextConfig } from './typings';
|
|
5
|
+
|
|
6
|
+
import { ClassHook } from '../../hooks/hook';
|
|
7
|
+
import { isEmptyObject } from '../../shared';
|
|
8
|
+
import {
|
|
9
|
+
loadAllEditorTranslations,
|
|
10
|
+
loadEditorPlugins,
|
|
11
|
+
normalizeCustomTranslations,
|
|
12
|
+
} from '../editor/utils';
|
|
13
|
+
import { ContextsRegistry } from './contexts-registry';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The Livewire hook that mounts CKEditor context instances.
|
|
17
|
+
*/
|
|
18
|
+
export class ContextComponentHook extends ClassHook<Snapshot> {
|
|
19
|
+
/**
|
|
20
|
+
* The promise that resolves to the context instance.
|
|
21
|
+
*/
|
|
22
|
+
private contextPromise: Promise<ContextWatchdog<Context>> | null = null;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Mounts the context component.
|
|
26
|
+
*/
|
|
27
|
+
override async mounted() {
|
|
28
|
+
const { contextId, language, context: contextConfig } = this.ephemeral;
|
|
29
|
+
const { customTranslations, watchdogConfig, config: { plugins, ...config } } = contextConfig;
|
|
30
|
+
|
|
31
|
+
const { loadedPlugins, hasPremium } = await loadEditorPlugins(plugins ?? []);
|
|
32
|
+
|
|
33
|
+
// Mix custom translations with loaded translations.
|
|
34
|
+
const loadedTranslations = await loadAllEditorTranslations(language, hasPremium);
|
|
35
|
+
const mixedTranslations = [
|
|
36
|
+
...loadedTranslations,
|
|
37
|
+
normalizeCustomTranslations(customTranslations || {}),
|
|
38
|
+
]
|
|
39
|
+
.filter(translations => !isEmptyObject(translations));
|
|
40
|
+
|
|
41
|
+
// Initialize context with watchdog.
|
|
42
|
+
this.contextPromise = (async () => {
|
|
43
|
+
const { ContextWatchdog, Context } = await import('ckeditor5');
|
|
44
|
+
|
|
45
|
+
const instance = new ContextWatchdog(Context, {
|
|
46
|
+
crashNumberLimit: 10,
|
|
47
|
+
...watchdogConfig,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
await instance.create({
|
|
51
|
+
...config,
|
|
52
|
+
language,
|
|
53
|
+
plugins: loadedPlugins,
|
|
54
|
+
...mixedTranslations.length && {
|
|
55
|
+
translations: mixedTranslations,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
instance.on('itemError', (...args) => {
|
|
60
|
+
console.error('Context item error:', ...args);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
return instance;
|
|
64
|
+
})();
|
|
65
|
+
|
|
66
|
+
const context = await this.contextPromise;
|
|
67
|
+
|
|
68
|
+
if (!this.isBeingDestroyed()) {
|
|
69
|
+
ContextsRegistry.the.register(contextId, context);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Destroys the context component. Unmounts root from the editor.
|
|
75
|
+
*/
|
|
76
|
+
override async destroyed() {
|
|
77
|
+
const { contextId } = this.ephemeral;
|
|
78
|
+
|
|
79
|
+
// Let's hide the element during destruction to prevent flickering.
|
|
80
|
+
this.element.style.display = 'none';
|
|
81
|
+
|
|
82
|
+
// Let's wait for the mounted promise to resolve before proceeding with destruction.
|
|
83
|
+
try {
|
|
84
|
+
const context = await this.contextPromise;
|
|
85
|
+
|
|
86
|
+
await context?.destroy();
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
this.contextPromise = null;
|
|
90
|
+
|
|
91
|
+
if (ContextsRegistry.the.hasItem(contextId)) {
|
|
92
|
+
ContextsRegistry.the.unregister(contextId);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The snapshot type stored in the Livewire Context hook.
|
|
100
|
+
*/
|
|
101
|
+
type Snapshot = {
|
|
102
|
+
/**
|
|
103
|
+
* The unique identifier for the context instance.
|
|
104
|
+
*/
|
|
105
|
+
contextId: string;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The context configuration for the context instance.
|
|
109
|
+
*/
|
|
110
|
+
context: ContextConfig;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The language of the context UI and content.
|
|
114
|
+
*/
|
|
115
|
+
language: EditorLanguage;
|
|
116
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { AsyncRegistry } from '../../shared/async-registry';
|
|
4
|
+
import { ContextsRegistry } from './contexts-registry';
|
|
5
|
+
|
|
6
|
+
describe('contexts registry', () => {
|
|
7
|
+
it('should be singleton of async registry', () => {
|
|
8
|
+
expect(ContextsRegistry.the).toBeInstanceOf(AsyncRegistry);
|
|
9
|
+
});
|
|
10
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Context, ContextWatchdog } from 'ckeditor5';
|
|
2
|
+
|
|
3
|
+
import { AsyncRegistry } from '../../shared';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* It provides a way to register contexts and execute callbacks on them when they are available.
|
|
7
|
+
*/
|
|
8
|
+
export class ContextsRegistry extends AsyncRegistry<ContextWatchdog<Context>> {
|
|
9
|
+
static readonly the = new ContextsRegistry();
|
|
10
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { WatchdogConfig } from 'ckeditor5';
|
|
2
|
+
|
|
3
|
+
import type { EditorCustomTranslationsDictionary, EditorPlugin } from '../editor';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Configuration object for CKEditor5 context instance.
|
|
7
|
+
*/
|
|
8
|
+
export type ContextConfig = {
|
|
9
|
+
/**
|
|
10
|
+
* Optional custom translations for the editor.
|
|
11
|
+
* This allows for localization of the editor interface.
|
|
12
|
+
*/
|
|
13
|
+
customTranslations?: EditorCustomTranslationsDictionary | null;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Watchdog configuration for the context.
|
|
17
|
+
*/
|
|
18
|
+
watchdogConfig: WatchdogConfig | null;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Configuration options for the context instance.
|
|
22
|
+
*/
|
|
23
|
+
config: ContextCreatorConfig;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Configuration options for the context instance.
|
|
28
|
+
*/
|
|
29
|
+
export type ContextCreatorConfig = {
|
|
30
|
+
/**
|
|
31
|
+
* Array of plugin identifiers to be loaded by the editor.
|
|
32
|
+
*/
|
|
33
|
+
plugins?: EditorPlugin[];
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Other configuration options are flexible and can be any key-value pairs.
|
|
37
|
+
*/
|
|
38
|
+
[key: string]: any;
|
|
39
|
+
};
|