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.
Files changed (167) hide show
  1. package/dist/hooks/context/context.d.ts +39 -0
  2. package/dist/hooks/context/context.d.ts.map +1 -0
  3. package/dist/hooks/context/contexts-registry.d.ts +9 -0
  4. package/dist/hooks/context/contexts-registry.d.ts.map +1 -0
  5. package/dist/hooks/context/index.d.ts +4 -0
  6. package/dist/hooks/context/index.d.ts.map +1 -0
  7. package/dist/hooks/context/typings.d.ts +34 -0
  8. package/dist/hooks/context/typings.d.ts.map +1 -0
  9. package/dist/hooks/editable.d.ts +40 -0
  10. package/dist/hooks/editable.d.ts.map +1 -0
  11. package/dist/hooks/editor/custom-editor-plugins.d.ts +54 -0
  12. package/dist/hooks/editor/custom-editor-plugins.d.ts.map +1 -0
  13. package/dist/hooks/editor/editor.d.ts +69 -0
  14. package/dist/hooks/editor/editor.d.ts.map +1 -0
  15. package/dist/hooks/editor/editors-registry.d.ts +9 -0
  16. package/dist/hooks/editor/editors-registry.d.ts.map +1 -0
  17. package/dist/hooks/editor/index.d.ts +3 -0
  18. package/dist/hooks/editor/index.d.ts.map +1 -0
  19. package/dist/hooks/editor/plugins/index.d.ts +3 -0
  20. package/dist/hooks/editor/plugins/index.d.ts.map +1 -0
  21. package/dist/hooks/editor/plugins/livewire-sync.d.ts +19 -0
  22. package/dist/hooks/editor/plugins/livewire-sync.d.ts.map +1 -0
  23. package/dist/hooks/editor/plugins/sync-editor-with-input.d.ts +6 -0
  24. package/dist/hooks/editor/plugins/sync-editor-with-input.d.ts.map +1 -0
  25. package/dist/hooks/editor/typings.d.ts +99 -0
  26. package/dist/hooks/editor/typings.d.ts.map +1 -0
  27. package/dist/hooks/editor/utils/create-editor-in-context.d.ts +44 -0
  28. package/dist/hooks/editor/utils/create-editor-in-context.d.ts.map +1 -0
  29. package/dist/hooks/editor/utils/get-editor-roots-values.d.ts +9 -0
  30. package/dist/hooks/editor/utils/get-editor-roots-values.d.ts.map +1 -0
  31. package/dist/hooks/editor/utils/index.d.ts +12 -0
  32. package/dist/hooks/editor/utils/index.d.ts.map +1 -0
  33. package/dist/hooks/editor/utils/is-single-editing-like-editor.d.ts +9 -0
  34. package/dist/hooks/editor/utils/is-single-editing-like-editor.d.ts.map +1 -0
  35. package/dist/hooks/editor/utils/load-editor-constructor.d.ts +9 -0
  36. package/dist/hooks/editor/utils/load-editor-constructor.d.ts.map +1 -0
  37. package/dist/hooks/editor/utils/load-editor-plugins.d.ts +20 -0
  38. package/dist/hooks/editor/utils/load-editor-plugins.d.ts.map +1 -0
  39. package/dist/hooks/editor/utils/load-editor-translations.d.ts +14 -0
  40. package/dist/hooks/editor/utils/load-editor-translations.d.ts.map +1 -0
  41. package/dist/hooks/editor/utils/normalize-custom-translations.d.ts +11 -0
  42. package/dist/hooks/editor/utils/normalize-custom-translations.d.ts.map +1 -0
  43. package/dist/hooks/editor/utils/query-editor-editables.d.ts +34 -0
  44. package/dist/hooks/editor/utils/query-editor-editables.d.ts.map +1 -0
  45. package/dist/hooks/editor/utils/resolve-editor-config-elements-references.d.ts +9 -0
  46. package/dist/hooks/editor/utils/resolve-editor-config-elements-references.d.ts.map +1 -0
  47. package/dist/hooks/editor/utils/set-editor-editable-height.d.ts +9 -0
  48. package/dist/hooks/editor/utils/set-editor-editable-height.d.ts.map +1 -0
  49. package/dist/hooks/editor/utils/wrap-with-watchdog.d.ts +24 -0
  50. package/dist/hooks/editor/utils/wrap-with-watchdog.d.ts.map +1 -0
  51. package/dist/hooks/hook.d.ts +58 -0
  52. package/dist/hooks/hook.d.ts.map +1 -0
  53. package/dist/hooks/index.d.ts +5 -0
  54. package/dist/hooks/index.d.ts.map +1 -0
  55. package/dist/hooks/ui-part.d.ts +32 -0
  56. package/dist/hooks/ui-part.d.ts.map +1 -0
  57. package/dist/index.cjs +5 -0
  58. package/dist/index.cjs.map +1 -0
  59. package/dist/index.d.ts +1 -0
  60. package/dist/index.d.ts.map +1 -0
  61. package/dist/index.mjs +1146 -0
  62. package/dist/index.mjs.map +1 -0
  63. package/dist/shared/async-registry.d.ts +131 -0
  64. package/dist/shared/async-registry.d.ts.map +1 -0
  65. package/dist/shared/camel-case.d.ts +8 -0
  66. package/dist/shared/camel-case.d.ts.map +1 -0
  67. package/dist/shared/debounce.d.ts +2 -0
  68. package/dist/shared/debounce.d.ts.map +1 -0
  69. package/dist/shared/deep-camel-case-keys.d.ts +8 -0
  70. package/dist/shared/deep-camel-case-keys.d.ts.map +1 -0
  71. package/dist/shared/filter-object-values.d.ts +9 -0
  72. package/dist/shared/filter-object-values.d.ts.map +1 -0
  73. package/dist/shared/index.d.ts +13 -0
  74. package/dist/shared/index.d.ts.map +1 -0
  75. package/dist/shared/is-empty-object.d.ts +2 -0
  76. package/dist/shared/is-empty-object.d.ts.map +1 -0
  77. package/dist/shared/is-plain-object.d.ts +8 -0
  78. package/dist/shared/is-plain-object.d.ts.map +1 -0
  79. package/dist/shared/map-object-values.d.ts +11 -0
  80. package/dist/shared/map-object-values.d.ts.map +1 -0
  81. package/dist/shared/once.d.ts +2 -0
  82. package/dist/shared/once.d.ts.map +1 -0
  83. package/dist/shared/timeout.d.ts +8 -0
  84. package/dist/shared/timeout.d.ts.map +1 -0
  85. package/dist/shared/uid.d.ts +7 -0
  86. package/dist/shared/uid.d.ts.map +1 -0
  87. package/dist/shared/wait-for.d.ts +20 -0
  88. package/dist/shared/wait-for.d.ts.map +1 -0
  89. package/dist/types/can-be-promise.type.d.ts +2 -0
  90. package/dist/types/can-be-promise.type.d.ts.map +1 -0
  91. package/dist/types/index.d.ts +3 -0
  92. package/dist/types/index.d.ts.map +1 -0
  93. package/dist/types/required-by.type.d.ts +2 -0
  94. package/dist/types/required-by.type.d.ts.map +1 -0
  95. package/package.json +40 -0
  96. package/src/hooks/context/context.test.ts +394 -0
  97. package/src/hooks/context/context.ts +116 -0
  98. package/src/hooks/context/contexts-registry.test.ts +10 -0
  99. package/src/hooks/context/contexts-registry.ts +10 -0
  100. package/src/hooks/context/index.ts +3 -0
  101. package/src/hooks/context/typings.ts +39 -0
  102. package/src/hooks/editable.test.ts +276 -0
  103. package/src/hooks/editable.ts +122 -0
  104. package/src/hooks/editor/custom-editor-plugins.test.ts +103 -0
  105. package/src/hooks/editor/custom-editor-plugins.ts +84 -0
  106. package/src/hooks/editor/editor.test.ts +782 -0
  107. package/src/hooks/editor/editor.ts +357 -0
  108. package/src/hooks/editor/editors-registry.test.ts +10 -0
  109. package/src/hooks/editor/editors-registry.ts +10 -0
  110. package/src/hooks/editor/index.ts +2 -0
  111. package/src/hooks/editor/plugins/index.ts +2 -0
  112. package/src/hooks/editor/plugins/livewire-sync.ts +85 -0
  113. package/src/hooks/editor/plugins/sync-editor-with-input.ts +76 -0
  114. package/src/hooks/editor/typings.ts +114 -0
  115. package/src/hooks/editor/utils/create-editor-in-context.ts +90 -0
  116. package/src/hooks/editor/utils/get-editor-roots-values.ts +16 -0
  117. package/src/hooks/editor/utils/index.ts +11 -0
  118. package/src/hooks/editor/utils/is-single-editing-like-editor.test.ts +40 -0
  119. package/src/hooks/editor/utils/is-single-editing-like-editor.ts +11 -0
  120. package/src/hooks/editor/utils/load-editor-constructor.test.ts +62 -0
  121. package/src/hooks/editor/utils/load-editor-constructor.ts +27 -0
  122. package/src/hooks/editor/utils/load-editor-plugins.test.ts +100 -0
  123. package/src/hooks/editor/utils/load-editor-plugins.ts +71 -0
  124. package/src/hooks/editor/utils/load-editor-translations.ts +233 -0
  125. package/src/hooks/editor/utils/normalize-custom-translations.test.ts +152 -0
  126. package/src/hooks/editor/utils/normalize-custom-translations.ts +18 -0
  127. package/src/hooks/editor/utils/query-editor-editables.ts +102 -0
  128. package/src/hooks/editor/utils/resolve-editor-config-elements-references.test.ts +93 -0
  129. package/src/hooks/editor/utils/resolve-editor-config-elements-references.ts +36 -0
  130. package/src/hooks/editor/utils/set-editor-editable-height.test.ts +131 -0
  131. package/src/hooks/editor/utils/set-editor-editable-height.ts +15 -0
  132. package/src/hooks/editor/utils/wrap-with-watchdog.test.ts +45 -0
  133. package/src/hooks/editor/utils/wrap-with-watchdog.ts +51 -0
  134. package/src/hooks/hook.ts +87 -0
  135. package/src/hooks/index.ts +21 -0
  136. package/src/hooks/ui-part.test.ts +161 -0
  137. package/src/hooks/ui-part.ts +80 -0
  138. package/src/index.ts +5 -0
  139. package/src/livewire.d.ts +42 -0
  140. package/src/shared/async-registry.test.ts +658 -0
  141. package/src/shared/async-registry.ts +308 -0
  142. package/src/shared/camel-case.test.ts +35 -0
  143. package/src/shared/camel-case.ts +11 -0
  144. package/src/shared/debounce.test.ts +72 -0
  145. package/src/shared/debounce.ts +16 -0
  146. package/src/shared/deep-camel-case-keys.test.ts +34 -0
  147. package/src/shared/deep-camel-case-keys.ts +26 -0
  148. package/src/shared/filter-object-values.test.ts +25 -0
  149. package/src/shared/filter-object-values.ts +17 -0
  150. package/src/shared/index.ts +12 -0
  151. package/src/shared/is-empty-object.test.ts +78 -0
  152. package/src/shared/is-empty-object.ts +3 -0
  153. package/src/shared/is-plain-object.test.ts +38 -0
  154. package/src/shared/is-plain-object.ts +15 -0
  155. package/src/shared/map-object-values.test.ts +29 -0
  156. package/src/shared/map-object-values.ts +19 -0
  157. package/src/shared/once.test.ts +116 -0
  158. package/src/shared/once.ts +12 -0
  159. package/src/shared/timeout.test.ts +65 -0
  160. package/src/shared/timeout.ts +13 -0
  161. package/src/shared/uid.test.ts +25 -0
  162. package/src/shared/uid.ts +8 -0
  163. package/src/shared/wait-for.test.ts +24 -0
  164. package/src/shared/wait-for.ts +56 -0
  165. package/src/types/can-be-promise.type.ts +1 -0
  166. package/src/types/index.ts +2 -0
  167. package/src/types/required-by.type.ts +1 -0
@@ -0,0 +1,357 @@
1
+ import type { Editor } from 'ckeditor5';
2
+
3
+ import type { EditorId, EditorLanguage, EditorPreset, EditorType } from './typings';
4
+ import type { EditorCreator } from './utils';
5
+
6
+ import { ContextsRegistry } from '../../hooks/context';
7
+ import { isEmptyObject, waitFor } from '../../shared';
8
+ import { ClassHook } from '../hook';
9
+ import { EditorsRegistry } from './editors-registry';
10
+ import {
11
+ createLivewireSyncPlugin,
12
+ createSyncEditorWithInputPlugin,
13
+ } from './plugins';
14
+ import {
15
+ createEditorInContext,
16
+ isSingleEditingLikeEditor,
17
+ loadAllEditorTranslations,
18
+ loadEditorConstructor,
19
+ loadEditorPlugins,
20
+ normalizeCustomTranslations,
21
+ queryEditablesElements,
22
+ queryEditablesSnapshotContent,
23
+ resolveEditorConfigElementReferences,
24
+ setEditorEditableHeight,
25
+ unwrapEditorContext,
26
+ unwrapEditorWatchdog,
27
+ wrapWithWatchdog,
28
+ } from './utils';
29
+
30
+ /**
31
+ * The Livewire hook that manages the lifecycle of CKEditor5 instances.
32
+ */
33
+ export class EditorComponentHook extends ClassHook<Snapshot> {
34
+ /**
35
+ * The promise that resolves to the editor instance.
36
+ */
37
+ private editorPromise: Promise<Editor> | null = null;
38
+
39
+ /**
40
+ * @inheritdoc
41
+ */
42
+ override async mounted(): Promise<void> {
43
+ const { editorId } = this.ephemeral;
44
+
45
+ EditorsRegistry.the.resetErrors(editorId);
46
+
47
+ try {
48
+ this.editorPromise = this.createEditor();
49
+
50
+ const editor = await this.editorPromise;
51
+
52
+ // Do not even try to broadcast about the registration of the editor
53
+ // if hook was immediately destroyed.
54
+ if (!this.isBeingDestroyed()) {
55
+ EditorsRegistry.the.register(editorId, editor);
56
+
57
+ editor.once('destroy', () => {
58
+ if (EditorsRegistry.the.hasItem(editorId)) {
59
+ EditorsRegistry.the.unregister(editorId);
60
+ }
61
+ });
62
+ }
63
+ }
64
+ catch (error: any) {
65
+ this.editorPromise = null;
66
+ EditorsRegistry.the.error(editorId, error);
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Destroys the editor instance when the component is destroyed.
72
+ * This is important to prevent memory leaks and ensure that the editor is properly cleaned up.
73
+ */
74
+ override async destroyed() {
75
+ // Let's hide the element during destruction to prevent flickering.
76
+ this.element.style.display = 'none';
77
+
78
+ // Let's wait for the mounted promise to resolve before proceeding with destruction.
79
+ try {
80
+ const editor = await this.editorPromise;
81
+
82
+ if (!editor) {
83
+ return;
84
+ }
85
+
86
+ const editorContext = unwrapEditorContext(editor);
87
+ const watchdog = unwrapEditorWatchdog(editor);
88
+
89
+ if (editorContext) {
90
+ // If context is present, make sure it's not in unmounting phase, as it'll kill the editors.
91
+ // If it's being destroyed, don't do anything, as the context will take care of it.
92
+ if (editorContext.state !== 'unavailable') {
93
+ await editorContext.context.remove(editorContext.editorContextId);
94
+ }
95
+ }
96
+ else if (watchdog) {
97
+ await watchdog.destroy();
98
+ }
99
+ else {
100
+ await editor.destroy();
101
+ }
102
+ }
103
+ finally {
104
+ this.editorPromise = null;
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Creates the CKEditor instance.
110
+ */
111
+ private async createEditor() {
112
+ const {
113
+ preset,
114
+ editorId,
115
+ contextId,
116
+ editableHeight,
117
+ emit,
118
+ saveDebounceMs,
119
+ language,
120
+ watchdog,
121
+ content,
122
+ } = this.ephemeral;
123
+
124
+ const {
125
+ customTranslations,
126
+ editorType,
127
+ licenseKey,
128
+ config: { plugins, ...config },
129
+ } = preset;
130
+
131
+ // Wrap editor creator with watchdog if needed.
132
+ let Constructor: EditorCreator = await loadEditorConstructor(editorType);
133
+ const context = await (
134
+ contextId
135
+ ? ContextsRegistry.the.waitFor(contextId)
136
+ : null
137
+ );
138
+
139
+ // Do not use editor specific watchdog if context is attached, as the context is by default protected.
140
+ if (watchdog && !context) {
141
+ const wrapped = await wrapWithWatchdog(Constructor);
142
+
143
+ ({ Constructor } = wrapped);
144
+ wrapped.watchdog.on('restart', () => {
145
+ const newInstance = wrapped.watchdog.editor!;
146
+
147
+ this.editorPromise = Promise.resolve(newInstance);
148
+
149
+ EditorsRegistry.the.register(editorId, newInstance);
150
+ });
151
+ }
152
+
153
+ const { loadedPlugins, hasPremium } = await loadEditorPlugins(plugins);
154
+
155
+ // Add integration specific plugins.
156
+ loadedPlugins.push(
157
+ await createLivewireSyncPlugin(
158
+ {
159
+ emit,
160
+ saveDebounceMs,
161
+ $wire: this.$wire,
162
+ },
163
+ ),
164
+ );
165
+
166
+ if (isSingleEditingLikeEditor(editorType)) {
167
+ loadedPlugins.push(
168
+ await createSyncEditorWithInputPlugin(saveDebounceMs),
169
+ );
170
+ }
171
+
172
+ // Mix custom translations with loaded translations.
173
+ const loadedTranslations = await loadAllEditorTranslations(language, hasPremium);
174
+ const mixedTranslations = [
175
+ ...loadedTranslations,
176
+ normalizeCustomTranslations(customTranslations || {}),
177
+ ]
178
+ .filter(translations => !isEmptyObject(translations));
179
+
180
+ // Let's query all elements, and create basic configuration.
181
+ let initialData: string | Record<string, string> = {
182
+ ...content,
183
+ ...queryEditablesSnapshotContent(editorId, editorType),
184
+ };
185
+
186
+ if (isSingleEditingLikeEditor(editorType)) {
187
+ initialData = initialData['main'] || '';
188
+ }
189
+
190
+ // Depending of the editor type, and parent lookup for nearest context or initialize it without it.
191
+ const editor = await (async () => {
192
+ let sourceElementOrData = queryEditablesElements(editorId, editorType);
193
+
194
+ // Handle special case when user specified `initialData` of several root elements, but editable components
195
+ // are not yet present in the DOM. In other words - editor is initialized before attaching root elements.
196
+ if (shouldWaitForRoots(sourceElementOrData, editorType)) {
197
+ const requiredRoots = Object.keys(initialData as Record<string, string>);
198
+
199
+ if (!checkIfAllRootsArePresent(sourceElementOrData, requiredRoots)) {
200
+ sourceElementOrData = await waitForAllRootsToBePresent(editorId, editorType, requiredRoots);
201
+ initialData = {
202
+ ...content,
203
+ ...queryEditablesSnapshotContent(editorId, editorType),
204
+ };
205
+ }
206
+ }
207
+
208
+ // Construct parsed config.
209
+ const parsedConfig = {
210
+ ...resolveEditorConfigElementReferences(config),
211
+ initialData,
212
+ licenseKey,
213
+ plugins: loadedPlugins,
214
+ language,
215
+ ...mixedTranslations.length && {
216
+ translations: mixedTranslations,
217
+ },
218
+ };
219
+
220
+ if (!context || !(sourceElementOrData instanceof HTMLElement)) {
221
+ return Constructor.create(sourceElementOrData as any, parsedConfig);
222
+ }
223
+
224
+ const result = await createEditorInContext({
225
+ context,
226
+ element: sourceElementOrData,
227
+ creator: Constructor,
228
+ config: parsedConfig,
229
+ });
230
+
231
+ return result.editor;
232
+ })();
233
+
234
+ if (isSingleEditingLikeEditor(editorType) && editableHeight) {
235
+ setEditorEditableHeight(editor, editableHeight);
236
+ }
237
+
238
+ return editor;
239
+ };
240
+ }
241
+
242
+ /**
243
+ * Checks if all required root elements are present in the elements object.
244
+ *
245
+ * @param elements The elements object mapping root IDs to HTMLElements.
246
+ * @param requiredRoots The list of required root IDs.
247
+ * @returns True if all required roots are present, false otherwise.
248
+ */
249
+ function checkIfAllRootsArePresent(elements: Record<string, HTMLElement>, requiredRoots: string[]): boolean {
250
+ return requiredRoots.every(rootId => elements[rootId]);
251
+ }
252
+
253
+ /**
254
+ * Waits for all required root elements to be present in the DOM.
255
+ *
256
+ * @param editorId The editor's ID.
257
+ * @param editorType The type of the editor.
258
+ * @param requiredRoots The list of required root IDs.
259
+ * @returns A promise that resolves to the record of root elements.
260
+ */
261
+ async function waitForAllRootsToBePresent(
262
+ editorId: EditorId,
263
+ editorType: EditorType,
264
+ requiredRoots: string[],
265
+ ): Promise<Record<string, HTMLElement>> {
266
+ await waitFor(
267
+ () => {
268
+ const elements = queryEditablesElements(editorId, editorType) as unknown as Record<string, HTMLElement>;
269
+
270
+ if (!checkIfAllRootsArePresent(elements, requiredRoots)) {
271
+ throw new Error(
272
+ 'It looks like not all required root elements are present yet.\n'
273
+ + '* If you want to wait for them, ensure they are registered before editor initialization.\n'
274
+ + '* If you want lazy initialize roots, consider removing root values from the `initialData` config '
275
+ + 'and assign initial data in editable components.\n'
276
+ + `Missing roots: ${requiredRoots.filter(rootId => !elements[rootId]).join(', ')}.`,
277
+ );
278
+ }
279
+
280
+ return true;
281
+ },
282
+ { timeOutAfter: 2000, retryAfter: 100 },
283
+ );
284
+
285
+ return queryEditablesElements(editorId, editorType) as unknown as Record<string, HTMLElement>;
286
+ }
287
+
288
+ /**
289
+ * Type guard to check if we should wait for multiple root elements.
290
+ *
291
+ * @param elements The elements retrieved for the editor.
292
+ * @param editorType The type of the editor.
293
+ * @returns True if we should wait for multiple root elements, false otherwise.
294
+ */
295
+ function shouldWaitForRoots(
296
+ elements: HTMLElement | Record<string, HTMLElement>,
297
+ editorType: EditorType,
298
+ ): elements is Record<string, HTMLElement> {
299
+ return (
300
+ !isSingleEditingLikeEditor(editorType)
301
+ && typeof elements === 'object'
302
+ && !(elements instanceof HTMLElement)
303
+ );
304
+ }
305
+
306
+ /**
307
+ * A snapshot of the Livewire component's state relevant to the CKEditor5 hook.
308
+ */
309
+ export type Snapshot = {
310
+ /**
311
+ * The unique identifier for the CKEditor5 instance.
312
+ */
313
+ editorId: string;
314
+
315
+ /**
316
+ * Whether to use a watchdog for the CKEditor5 instance.
317
+ */
318
+ watchdog: boolean;
319
+
320
+ /**
321
+ * The identifier of the CKEditor context.
322
+ */
323
+ contextId: string | null;
324
+
325
+ /**
326
+ * The debounce time in milliseconds for saving content changes.
327
+ */
328
+ saveDebounceMs: number;
329
+
330
+ /**
331
+ * The preset configuration for the CKEditor5 instance.
332
+ */
333
+ preset: EditorPreset;
334
+
335
+ /**
336
+ * The content of the editor, mapped by ID of root elements.
337
+ */
338
+ content: Record<string, string>;
339
+
340
+ /**
341
+ * The height of the editable area, if specified.
342
+ */
343
+ editableHeight: number | null;
344
+
345
+ /**
346
+ * The language of the editor UI and content.
347
+ */
348
+ language: EditorLanguage;
349
+
350
+ /**
351
+ * The global events of the editor to forward to Livewire.
352
+ */
353
+ emit: {
354
+ change: boolean;
355
+ focus: boolean;
356
+ };
357
+ };
@@ -0,0 +1,10 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { AsyncRegistry } from '../../shared/async-registry';
4
+ import { EditorsRegistry } from './editors-registry';
5
+
6
+ describe('editors registry', () => {
7
+ it('should be singleton of async registry', () => {
8
+ expect(EditorsRegistry.the).toBeInstanceOf(AsyncRegistry);
9
+ });
10
+ });
@@ -0,0 +1,10 @@
1
+ import type { Editor } from 'ckeditor5';
2
+
3
+ import { AsyncRegistry } from '../../shared/async-registry';
4
+
5
+ /**
6
+ * It provides a way to register editors and execute callbacks on them when they are available.
7
+ */
8
+ export class EditorsRegistry extends AsyncRegistry<Editor> {
9
+ static readonly the = new EditorsRegistry();
10
+ }
@@ -0,0 +1,2 @@
1
+ export * from './editor';
2
+ export * from './typings';
@@ -0,0 +1,2 @@
1
+ export * from './livewire-sync';
2
+ export * from './sync-editor-with-input';
@@ -0,0 +1,85 @@
1
+ import type { PluginConstructor } from 'ckeditor5';
2
+
3
+ import type { Wire } from '../../../livewire';
4
+
5
+ import { debounce } from '../../../shared';
6
+ import { getEditorRootsValues } from '../utils';
7
+
8
+ /**
9
+ * Creates a LivewireSync plugin class.
10
+ */
11
+ export async function createLivewireSyncPlugin(
12
+ {
13
+ emit,
14
+ saveDebounceMs,
15
+ $wire,
16
+ }: Attrs,
17
+ ): Promise<PluginConstructor> {
18
+ const { Plugin } = await import('ckeditor5');
19
+
20
+ return class LivewireSync extends Plugin {
21
+ /**
22
+ * The name of the plugin.
23
+ */
24
+ static get pluginName() {
25
+ return 'LivewireSync' as const;
26
+ }
27
+
28
+ /**
29
+ * Initializes the plugin.
30
+ */
31
+ public init(): void {
32
+ if (emit.change) {
33
+ this.setupTypingContentPush();
34
+ }
35
+
36
+ if (emit.focus) {
37
+ this.setupFocusableEventPush();
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Setups the content push event for the editor.
43
+ */
44
+ private setupTypingContentPush() {
45
+ const { model } = this.editor;
46
+
47
+ const syncContentChange = () => {
48
+ $wire.set('content', this.getEditorRootsValues());
49
+ };
50
+
51
+ model.document.on('change:data', debounce(saveDebounceMs, syncContentChange));
52
+ syncContentChange();
53
+ }
54
+
55
+ /**
56
+ * Setups the event push for the editor.
57
+ */
58
+ private setupFocusableEventPush() {
59
+ const { ui } = this.editor;
60
+
61
+ const pushEvent = () => {
62
+ $wire.set('focused', ui.focusTracker.isFocused);
63
+ $wire.set('content', this.getEditorRootsValues());
64
+ };
65
+
66
+ ui.focusTracker.on('change:isFocused', pushEvent);
67
+ }
68
+
69
+ /**
70
+ * Gets the current values of all editor roots.
71
+ */
72
+ private getEditorRootsValues(): Record<string, string> {
73
+ return getEditorRootsValues(this.editor);
74
+ }
75
+ };
76
+ }
77
+
78
+ /**
79
+ * The attributes required to create the LivewireSync plugin.
80
+ */
81
+ type Attrs = {
82
+ emit: { change?: boolean; focus?: boolean; };
83
+ saveDebounceMs: number;
84
+ $wire: Wire;
85
+ };
@@ -0,0 +1,76 @@
1
+ import type { ClassicEditor, PluginConstructor } from 'ckeditor5';
2
+
3
+ import { debounce } from '../../../shared';
4
+
5
+ /**
6
+ * Creates a SyncEditorWithInput plugin class.
7
+ */
8
+ export async function createSyncEditorWithInputPlugin(saveDebounceMs: number): Promise<PluginConstructor> {
9
+ const { Plugin } = await import('ckeditor5');
10
+
11
+ return class SyncEditorWithInput extends Plugin {
12
+ /**
13
+ * The input element to synchronize with.
14
+ */
15
+ private input: HTMLInputElement | null = null;
16
+
17
+ /**
18
+ * The form element reference for cleanup.
19
+ */
20
+ private form: HTMLFormElement | null = null;
21
+
22
+ /**
23
+ * The name of the plugin.
24
+ */
25
+ static get pluginName() {
26
+ return 'SyncEditorWithInput' as const;
27
+ }
28
+
29
+ /**
30
+ * Initializes the plugin.
31
+ */
32
+ public afterInit(): void {
33
+ const { editor } = this;
34
+ const editorElement = (editor as ClassicEditor).sourceElement as HTMLElement;
35
+
36
+ // Try to find the associated input field.
37
+ const editorId = editorElement.id.replace(/_editor$/, '');
38
+
39
+ this.input = document.getElementById(`${editorId}_input`) as HTMLInputElement | null;
40
+
41
+ if (!this.input) {
42
+ return;
43
+ }
44
+
45
+ // Setup handlers.
46
+ editor.model.document.on('change:data', debounce(saveDebounceMs, () => this.sync()));
47
+ editor.once('ready', this.sync);
48
+
49
+ // Setup form integration.
50
+ this.form = this.input.closest('form');
51
+ this.form?.addEventListener('submit', this.sync);
52
+ }
53
+
54
+ /**
55
+ * Synchronizes the editor's content with the input field.
56
+ */
57
+ private sync = (): void => {
58
+ const newValue = this.editor.getData();
59
+
60
+ this.input!.value = newValue;
61
+ this.input!.dispatchEvent(new Event('input', { bubbles: true }));
62
+ };
63
+
64
+ /**
65
+ * Destroys the plugin.
66
+ */
67
+ public override destroy(): void {
68
+ if (this.form) {
69
+ this.form.removeEventListener('submit', this.sync);
70
+ }
71
+
72
+ this.input = null;
73
+ this.form = null;
74
+ }
75
+ };
76
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Represents a unique identifier for a CKEditor5 editor instance.
3
+ * This is typically the ID of the HTML element that the editor is attached to.
4
+ */
5
+ export type EditorId = string;
6
+
7
+ /**
8
+ * Defines editor type supported by CKEditor5.
9
+ */
10
+ export type EditorType = 'inline' | 'classic' | 'balloon' | 'decoupled' | 'multiroot';
11
+
12
+ /**
13
+ * Represents a CKEditor5 plugin as a string identifier.
14
+ */
15
+ export type EditorPlugin = string;
16
+
17
+ /**
18
+ * Configuration object for CKEditor5 editor instance.
19
+ */
20
+ export type EditorConfig = {
21
+ /**
22
+ * Array of plugin identifiers to be loaded by the editor.
23
+ */
24
+ plugins: EditorPlugin[];
25
+
26
+ /**
27
+ * Other configuration options are flexible and can be any key-value pairs.
28
+ */
29
+ [key: string]: any;
30
+ };
31
+
32
+ /**
33
+ * Configuration object for CKEditor5 cloud services.
34
+ */
35
+ export type EditorCloudConfig = {
36
+ /**
37
+ * The version of CKEditor5 being used.
38
+ */
39
+ editorVersion: string;
40
+
41
+ /**
42
+ * Indicates whether the CKEditor5 instance is a premium version.
43
+ */
44
+ premium: boolean;
45
+
46
+ /**
47
+ * List of language codes for translations available in the CKEditor5 instance.
48
+ */
49
+ translations: string[];
50
+
51
+ /**
52
+ * Configuration for CKEditor5's upload adapter.
53
+ */
54
+ ckbox: {
55
+ version: string;
56
+ theme: string | null;
57
+ } | null;
58
+ };
59
+
60
+ /**
61
+ * Configuration object for the CKEditor5 hook.
62
+ */
63
+ export type EditorPreset = {
64
+ /**
65
+ * The configuration of the cloud.
66
+ */
67
+ cloud: EditorCloudConfig | null;
68
+
69
+ /**
70
+ * The type of CKEditor5 editor to use.
71
+ * Must be one of the predefined types: 'inline', 'classic', 'balloon', 'decoupled', or 'multiroot'.
72
+ */
73
+ editorType: EditorType;
74
+
75
+ /**
76
+ * The configuration object for the CKEditor5 editor.
77
+ * This should match the configuration expected by CKEditor5.
78
+ */
79
+ config: EditorConfig;
80
+
81
+ /**
82
+ * The license key for CKEditor5.
83
+ * This is required for using CKEditor5 with a valid license.
84
+ */
85
+ licenseKey: string;
86
+
87
+ /**
88
+ * Optional watchdog configuration for error recovery.
89
+ */
90
+ watchdogConfig?: Record<string, any> | null;
91
+
92
+ /**
93
+ * Optional custom translations for the editor.
94
+ * This allows for localization of the editor interface.
95
+ */
96
+ customTranslations?: EditorCustomTranslationsDictionary | null;
97
+ };
98
+
99
+ /**
100
+ * Represents the language settings for the CKEditor5 editor.
101
+ */
102
+ export type EditorLanguage = {
103
+ ui: string;
104
+ content: string;
105
+ };
106
+
107
+ /**
108
+ * Represents custom translations for the editor.
109
+ */
110
+ export type EditorCustomTranslationsDictionary = {
111
+ [language: string]: {
112
+ [key: string]: string | ReadonlyArray<string>;
113
+ };
114
+ };