ckeditor5-livewire 1.8.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/hooks/editable.d.ts +18 -6
- package/dist/hooks/editable.d.ts.map +1 -1
- package/dist/hooks/editor/editor.d.ts +14 -0
- package/dist/hooks/editor/editor.d.ts.map +1 -1
- package/dist/hooks/editor/plugins/livewire-sync.d.ts.map +1 -1
- package/dist/hooks/editor/utils/index.d.ts +0 -1
- package/dist/hooks/editor/utils/index.d.ts.map +1 -1
- package/dist/hooks/utils/index.d.ts +3 -0
- package/dist/hooks/utils/index.d.ts.map +1 -0
- package/dist/hooks/utils/is-wire-model-connected.d.ts +8 -0
- package/dist/hooks/utils/is-wire-model-connected.d.ts.map +1 -0
- package/dist/hooks/utils/root-attributes-updater.d.ts +16 -0
- package/dist/hooks/utils/root-attributes-updater.d.ts.map +1 -0
- package/dist/index.cjs +2 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +160 -122
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/hooks/editable.test.ts +103 -0
- package/src/hooks/editable.ts +74 -32
- package/src/hooks/editor/editor.test.ts +68 -0
- package/src/hooks/editor/editor.ts +31 -1
- package/src/hooks/editor/plugins/livewire-sync.ts +28 -4
- package/src/hooks/editor/utils/index.ts +0 -1
- package/src/hooks/utils/index.ts +2 -0
- package/src/hooks/{editor/utils → utils}/is-wire-model-connected.test.ts +2 -1
- package/src/hooks/{editor/utils → utils}/is-wire-model-connected.ts +6 -0
- package/src/hooks/utils/root-attributes-updater.ts +46 -0
- package/dist/hooks/editor/utils/is-wire-model-connected.d.ts +0 -2
- package/dist/hooks/editor/utils/is-wire-model-connected.d.ts.map +0 -1
|
@@ -50,6 +50,58 @@ describe('editable component', () => {
|
|
|
50
50
|
expect(editor.getData({ rootName: 'foo' })).toBe('<p>Initial foo component</p>');
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
+
it('should apply root attributes when mounting editable', async () => {
|
|
54
|
+
appendMultirootEditor();
|
|
55
|
+
|
|
56
|
+
const editor = await waitForTestEditor();
|
|
57
|
+
|
|
58
|
+
livewireStub.$internal.appendComponentToDOM({
|
|
59
|
+
name: 'ckeditor5-editable',
|
|
60
|
+
el: createEditableHtmlElement(),
|
|
61
|
+
canonical: {
|
|
62
|
+
...createEditableSnapshot('foo', '<p>Initial foo component</p>'),
|
|
63
|
+
rootAttributes: {
|
|
64
|
+
'data-test': 'initial',
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const root = editor.model.document.getRoot('foo')!;
|
|
70
|
+
|
|
71
|
+
expect(root.getAttribute('data-test')).toBe('initial');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should update root attributes after commit', async () => {
|
|
75
|
+
appendMultirootEditor();
|
|
76
|
+
|
|
77
|
+
const editor = await waitForTestEditor();
|
|
78
|
+
|
|
79
|
+
const { id } = livewireStub.$internal.appendComponentToDOM({
|
|
80
|
+
name: 'ckeditor5-editable',
|
|
81
|
+
el: createEditableHtmlElement(),
|
|
82
|
+
canonical: {
|
|
83
|
+
...createEditableSnapshot('foo', '<p>Initial foo component</p>'),
|
|
84
|
+
rootAttributes: {
|
|
85
|
+
'data-test': 'initial',
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const root = editor.model.document.getRoot('foo')!;
|
|
91
|
+
|
|
92
|
+
expect(root.getAttribute('data-test')).toBe('initial');
|
|
93
|
+
|
|
94
|
+
await livewireStub.$internal.dispatchComponentCommit(id, {
|
|
95
|
+
rootAttributes: {
|
|
96
|
+
'data-test': 'updated',
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
await vi.waitFor(() => {
|
|
101
|
+
expect(root.getAttribute('data-test')).toBe('updated');
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
53
105
|
it('should add editable root to the editor after mounting editor (non-empty editor, other editable defined before)', async () => {
|
|
54
106
|
livewireStub.$internal.appendComponentToDOM({
|
|
55
107
|
name: 'ckeditor5-editable',
|
|
@@ -200,6 +252,9 @@ describe('editable component', () => {
|
|
|
200
252
|
|
|
201
253
|
expect(input.value).toBe('<p>Initial foo component</p>');
|
|
202
254
|
|
|
255
|
+
// Debounce should be active only while the editor is focused.
|
|
256
|
+
editor.ui.focusTracker.isFocused = true;
|
|
257
|
+
|
|
203
258
|
editor.setData({
|
|
204
259
|
foo: '<p>Modified foo content</p>',
|
|
205
260
|
});
|
|
@@ -212,6 +267,28 @@ describe('editable component', () => {
|
|
|
212
267
|
|
|
213
268
|
expect(input.value).toBe('<p>Modified foo content</p>');
|
|
214
269
|
});
|
|
270
|
+
|
|
271
|
+
it('should synchronize input value immediately when editor is not focused', async () => {
|
|
272
|
+
livewireStub.$internal.appendComponentToDOM({
|
|
273
|
+
name: 'ckeditor5-editable',
|
|
274
|
+
el: createEditableHtmlElement({
|
|
275
|
+
id: 'editable-foo',
|
|
276
|
+
}),
|
|
277
|
+
canonical: createEditableSnapshot('foo', '<p>Initial foo component</p>'),
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
const input = queryEditableInput('editable-foo')!;
|
|
281
|
+
|
|
282
|
+
expect(input.value).toBe('<p>Initial foo component</p>');
|
|
283
|
+
|
|
284
|
+
editor.ui.focusTracker.isFocused = false;
|
|
285
|
+
|
|
286
|
+
editor.setData({
|
|
287
|
+
foo: '<p>Modified foo content</p>',
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
expect(input.value).toBe('<p>Modified foo content</p>');
|
|
291
|
+
});
|
|
215
292
|
});
|
|
216
293
|
|
|
217
294
|
describe('livewire <> editor synchronization', () => {
|
|
@@ -250,6 +327,9 @@ describe('editable component', () => {
|
|
|
250
327
|
|
|
251
328
|
expect($wire.set).toHaveBeenCalledWith('content', '<p>Initial foo component</p>');
|
|
252
329
|
|
|
330
|
+
// Debounce should be active only when the editor is focused.
|
|
331
|
+
editor.ui.focusTracker.isFocused = true;
|
|
332
|
+
|
|
253
333
|
editor.setData({
|
|
254
334
|
foo: '<p>Modified foo content</p>',
|
|
255
335
|
});
|
|
@@ -262,6 +342,29 @@ describe('editable component', () => {
|
|
|
262
342
|
|
|
263
343
|
expect($wire.set).toHaveBeenCalledWith('content', '<p>Modified foo content</p>');
|
|
264
344
|
});
|
|
345
|
+
|
|
346
|
+
it('should synchronize socket content immediately when editor is not focused', async () => {
|
|
347
|
+
const { $wire } = livewireStub.$internal.appendComponentToDOM({
|
|
348
|
+
name: 'ckeditor5-editable',
|
|
349
|
+
el: createEditableHtmlElement(),
|
|
350
|
+
canonical: {
|
|
351
|
+
...createEditableSnapshot('foo', '<p>Initial foo component</p>'),
|
|
352
|
+
saveDebounceMs: 500,
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
expect($wire.set).toHaveBeenCalledWith('content', '<p>Initial foo component</p>');
|
|
357
|
+
|
|
358
|
+
vi.mocked($wire.set).mockClear();
|
|
359
|
+
|
|
360
|
+
editor.ui.focusTracker.isFocused = false;
|
|
361
|
+
|
|
362
|
+
editor.setData({
|
|
363
|
+
foo: '<p>Modified foo content</p>',
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
expect($wire.set).toHaveBeenCalledWith('content', '<p>Modified foo content</p>');
|
|
367
|
+
});
|
|
265
368
|
});
|
|
266
369
|
|
|
267
370
|
describe('sync editable content after commit', () => {
|
package/src/hooks/editable.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { MultiRootEditor } from 'ckeditor5';
|
|
2
2
|
|
|
3
|
+
import type { RootAttributesUpdater } from './utils';
|
|
4
|
+
|
|
3
5
|
import { debounce } from '../shared';
|
|
4
6
|
import { EditorsRegistry } from './editor/editors-registry';
|
|
5
|
-
import { isWireModelConnected } from './editor/utils';
|
|
6
7
|
import { ClassHook } from './hook';
|
|
8
|
+
import { createRootAttributesUpdater, isWireModelConnected } from './utils';
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
11
|
* Editable hook for Livewire. It allows you to create editables for multi-root editors.
|
|
@@ -14,6 +16,11 @@ export class EditableComponentHook extends ClassHook<Snapshot> {
|
|
|
14
16
|
*/
|
|
15
17
|
private editorPromise: Promise<MultiRootEditor | null> | null = null;
|
|
16
18
|
|
|
19
|
+
/**
|
|
20
|
+
* The root attributes updater for the editable's root.
|
|
21
|
+
*/
|
|
22
|
+
private rootAttributesUpdater: RootAttributesUpdater | null = null;
|
|
23
|
+
|
|
17
24
|
/**
|
|
18
25
|
* Pending content to apply when the editor loses focus.
|
|
19
26
|
*/
|
|
@@ -65,19 +72,64 @@ export class EditableComponentHook extends ClassHook<Snapshot> {
|
|
|
65
72
|
// Add livewire sync.
|
|
66
73
|
this.syncTypingContentPush(editor);
|
|
67
74
|
this.setupPendingReceivedContentHandlers(editor);
|
|
75
|
+
this.applyRootAttributes(editor);
|
|
68
76
|
|
|
69
77
|
return editor;
|
|
70
78
|
});
|
|
71
79
|
}
|
|
72
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Called when the component is updated by Livewire.
|
|
83
|
+
*/
|
|
84
|
+
override async afterCommitSynced(): Promise<void> {
|
|
85
|
+
const editor = (await this.editorPromise)!;
|
|
86
|
+
|
|
87
|
+
this.applyCanonicalContentToEditor(editor);
|
|
88
|
+
this.applyRootAttributes(editor);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Destroys the editable component. Unmounts root from the editor.
|
|
93
|
+
*/
|
|
94
|
+
override async destroyed() {
|
|
95
|
+
const { rootName } = this.canonical;
|
|
96
|
+
|
|
97
|
+
// Let's hide the element during destruction to prevent flickering.
|
|
98
|
+
this.element.style.display = 'none';
|
|
99
|
+
|
|
100
|
+
// Let's wait for the mounted promise to resolve before proceeding with destruction.
|
|
101
|
+
const editor = await this.editorPromise;
|
|
102
|
+
this.editorPromise = null;
|
|
103
|
+
|
|
104
|
+
// Remove root attributes we may have set on this root.
|
|
105
|
+
this.rootAttributesUpdater?.(null);
|
|
106
|
+
|
|
107
|
+
// Unmount root from the editor if editor is still registered.
|
|
108
|
+
if (editor && editor.state !== 'destroyed') {
|
|
109
|
+
const root = editor.model.document.getRoot(rootName);
|
|
110
|
+
|
|
111
|
+
/* v8 ignore next if -- @preserve */
|
|
112
|
+
if (root && 'detachEditable' in editor) {
|
|
113
|
+
editor.detachEditable(root);
|
|
114
|
+
editor.detachRoot(rootName, false);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
73
119
|
/**
|
|
74
120
|
* Setups the content sync from the editor to Livewire on user input with debounce.
|
|
75
121
|
*/
|
|
76
122
|
private syncTypingContentPush(editor: MultiRootEditor) {
|
|
77
123
|
const { rootName, saveDebounceMs } = this.canonical;
|
|
124
|
+
|
|
78
125
|
const input = this.element.querySelector<HTMLInputElement>('input');
|
|
126
|
+
let isDestroyed = false;
|
|
79
127
|
|
|
80
128
|
const sync = () => {
|
|
129
|
+
if (isDestroyed) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
81
133
|
const html = editor.getData({ rootName });
|
|
82
134
|
|
|
83
135
|
if (input) {
|
|
@@ -88,12 +140,21 @@ export class EditableComponentHook extends ClassHook<Snapshot> {
|
|
|
88
140
|
};
|
|
89
141
|
|
|
90
142
|
const debouncedSync = debounce(saveDebounceMs, sync);
|
|
143
|
+
const onChangeData = () => {
|
|
144
|
+
if (editor.ui.focusTracker.isFocused) {
|
|
145
|
+
debouncedSync();
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
sync();
|
|
149
|
+
}
|
|
150
|
+
};
|
|
91
151
|
|
|
92
|
-
editor.model.document.on('change:data',
|
|
152
|
+
editor.model.document.on('change:data', onChangeData);
|
|
93
153
|
sync();
|
|
94
154
|
|
|
95
155
|
this.onBeforeDestroy(() => {
|
|
96
|
-
|
|
156
|
+
isDestroyed = true;
|
|
157
|
+
editor.model.document.off('change:data', onChangeData);
|
|
97
158
|
});
|
|
98
159
|
}
|
|
99
160
|
|
|
@@ -155,37 +216,13 @@ export class EditableComponentHook extends ClassHook<Snapshot> {
|
|
|
155
216
|
}
|
|
156
217
|
|
|
157
218
|
/**
|
|
158
|
-
*
|
|
219
|
+
* Applies root attributes from the Livewire snapshot to the editor root.
|
|
159
220
|
*/
|
|
160
|
-
|
|
161
|
-
const
|
|
221
|
+
private applyRootAttributes(editor: MultiRootEditor): void {
|
|
222
|
+
const { rootName, rootAttributes } = this.canonical;
|
|
162
223
|
|
|
163
|
-
this.
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Destroys the editable component. Unmounts root from the editor.
|
|
168
|
-
*/
|
|
169
|
-
override async destroyed() {
|
|
170
|
-
const { rootName } = this.canonical;
|
|
171
|
-
|
|
172
|
-
// Let's hide the element during destruction to prevent flickering.
|
|
173
|
-
this.element.style.display = 'none';
|
|
174
|
-
|
|
175
|
-
// Let's wait for the mounted promise to resolve before proceeding with destruction.
|
|
176
|
-
const editor = await this.editorPromise;
|
|
177
|
-
this.editorPromise = null;
|
|
178
|
-
|
|
179
|
-
// Unmount root from the editor if editor is still registered.
|
|
180
|
-
if (editor && editor.state !== 'destroyed') {
|
|
181
|
-
const root = editor.model.document.getRoot(rootName);
|
|
182
|
-
|
|
183
|
-
/* v8 ignore next if -- @preserve */
|
|
184
|
-
if (root && 'detachEditable' in editor) {
|
|
185
|
-
editor.detachEditable(root);
|
|
186
|
-
editor.detachRoot(rootName, false);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
224
|
+
this.rootAttributesUpdater ??= createRootAttributesUpdater(editor, rootName);
|
|
225
|
+
this.rootAttributesUpdater(rootAttributes);
|
|
189
226
|
}
|
|
190
227
|
}
|
|
191
228
|
|
|
@@ -212,4 +249,9 @@ export type Snapshot = {
|
|
|
212
249
|
* The debounce time in milliseconds for saving changes.
|
|
213
250
|
*/
|
|
214
251
|
saveDebounceMs: number;
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Root attributes to apply to the editable root.
|
|
255
|
+
*/
|
|
256
|
+
rootAttributes?: Record<string, unknown>;
|
|
215
257
|
};
|
|
@@ -375,6 +375,53 @@ describe('editor component', () => {
|
|
|
375
375
|
}).not.toThrow();
|
|
376
376
|
});
|
|
377
377
|
});
|
|
378
|
+
|
|
379
|
+
describe('root attributes', () => {
|
|
380
|
+
it('should apply root attributes from snapshot', async () => {
|
|
381
|
+
const { id } = livewireStub.$internal.appendComponentToDOM<EditorSnapshot>({
|
|
382
|
+
name: 'ckeditor5',
|
|
383
|
+
el: createEditorHtmlElement(),
|
|
384
|
+
canonical: createEditorSnapshot({
|
|
385
|
+
rootAttributes: {
|
|
386
|
+
'data-test': 'initial',
|
|
387
|
+
'foo': 'bar',
|
|
388
|
+
},
|
|
389
|
+
}),
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
const editor = await waitForTestEditor();
|
|
393
|
+
const root = editor.model.document.getRoot('main')!;
|
|
394
|
+
|
|
395
|
+
expect(root.getAttribute('data-test')).toBe('initial');
|
|
396
|
+
expect(root.getAttribute('foo')).toBe('bar');
|
|
397
|
+
|
|
398
|
+
// Ensure attributes managed by other consumers are not removed.
|
|
399
|
+
editor.model.change((writer) => {
|
|
400
|
+
writer.setAttribute('external', 'value', root);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
await livewireStub.$internal.dispatchComponentCommit<EditorSnapshot>(id, {
|
|
404
|
+
rootAttributes: {
|
|
405
|
+
'data-test': 'updated',
|
|
406
|
+
},
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
await vi.waitFor(() => {
|
|
410
|
+
expect(root.getAttribute('data-test')).toBe('updated');
|
|
411
|
+
expect(root.getAttribute('foo')).toBeUndefined();
|
|
412
|
+
expect(root.getAttribute('external')).toBe('value');
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
await livewireStub.$internal.dispatchComponentCommit<EditorSnapshot>(id, {
|
|
416
|
+
rootAttributes: null as any,
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
await vi.waitFor(() => {
|
|
420
|
+
expect(root.getAttribute('data-test')).toBeUndefined();
|
|
421
|
+
expect(root.getAttribute('external')).toBe('value');
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
});
|
|
378
425
|
});
|
|
379
426
|
|
|
380
427
|
describe('`editableHeight` snapshot parameter`', () => {
|
|
@@ -432,6 +479,9 @@ describe('editor component', () => {
|
|
|
432
479
|
|
|
433
480
|
const editor = await waitForTestEditor();
|
|
434
481
|
|
|
482
|
+
// Debounce should only be applied while the editor is focused.
|
|
483
|
+
editor.ui.focusTracker.isFocused = true;
|
|
484
|
+
|
|
435
485
|
vi.mocked($wire.set).mockClear();
|
|
436
486
|
editor.setData('<p>Debounce test</p>');
|
|
437
487
|
|
|
@@ -442,6 +492,24 @@ describe('editor component', () => {
|
|
|
442
492
|
expect($wire.set).toHaveBeenCalledExactlyOnceWith('content', { main: '<p>Debounce test</p>' });
|
|
443
493
|
});
|
|
444
494
|
|
|
495
|
+
it('should immediately send `$wire.set` when editor is not focused', async () => {
|
|
496
|
+
const { $wire } = livewireStub.$internal.appendComponentToDOM<EditorSnapshot>({
|
|
497
|
+
name: 'ckeditor5',
|
|
498
|
+
el: createEditorHtmlElement(),
|
|
499
|
+
canonical: {
|
|
500
|
+
...createEditorSnapshot(),
|
|
501
|
+
saveDebounceMs: 400,
|
|
502
|
+
},
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
const editor = await waitForTestEditor();
|
|
506
|
+
|
|
507
|
+
vi.mocked($wire.set).mockClear();
|
|
508
|
+
editor.setData('<p>Debounce test</p>');
|
|
509
|
+
|
|
510
|
+
expect($wire.set).toHaveBeenCalledExactlyOnceWith('content', { main: '<p>Debounce test</p>' });
|
|
511
|
+
});
|
|
512
|
+
|
|
445
513
|
it('should use parameter to debounce input sync', async () => {
|
|
446
514
|
livewireStub.$internal.appendComponentToDOM<EditorSnapshot>({
|
|
447
515
|
name: 'ckeditor5',
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type { Editor } from 'ckeditor5';
|
|
2
2
|
|
|
3
|
+
import type { RootAttributesUpdater } from '../utils';
|
|
3
4
|
import type { EditorId, EditorLanguage, EditorPreset } from './typings';
|
|
4
5
|
import type { EditorCreator } from './utils';
|
|
5
6
|
|
|
6
7
|
import { ContextsRegistry } from '../../hooks/context';
|
|
7
8
|
import { isEmptyObject, waitFor } from '../../shared';
|
|
8
9
|
import { ClassHook } from '../hook';
|
|
10
|
+
import { createRootAttributesUpdater } from '../utils';
|
|
9
11
|
import { EditorsRegistry } from './editors-registry';
|
|
10
12
|
import {
|
|
11
13
|
createLivewireSyncPlugin,
|
|
@@ -37,6 +39,11 @@ export class EditorComponentHook extends ClassHook<Snapshot> {
|
|
|
37
39
|
*/
|
|
38
40
|
private editorPromise: Promise<Editor> | null = null;
|
|
39
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Root attributes updater for the main editor root.
|
|
44
|
+
*/
|
|
45
|
+
private rootAttributesUpdater: RootAttributesUpdater | null = null;
|
|
46
|
+
|
|
40
47
|
/**
|
|
41
48
|
* @inheritdoc
|
|
42
49
|
*/
|
|
@@ -115,7 +122,23 @@ export class EditorComponentHook extends ClassHook<Snapshot> {
|
|
|
115
122
|
override async afterCommitSynced(): Promise<void> {
|
|
116
123
|
const editor = await this.editorPromise;
|
|
117
124
|
|
|
118
|
-
|
|
125
|
+
/* v8 ignore if -- @preserve */
|
|
126
|
+
if (editor) {
|
|
127
|
+
editor.fire('afterCommitSynced');
|
|
128
|
+
this.applyRootAttributes(editor);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Applies root attributes from the Livewire snapshot to the editor.
|
|
134
|
+
*
|
|
135
|
+
* Note: we only apply attributes to the `main` root in the editor.
|
|
136
|
+
*/
|
|
137
|
+
private applyRootAttributes(editor: Editor): void {
|
|
138
|
+
const { rootAttributes } = this.canonical;
|
|
139
|
+
|
|
140
|
+
this.rootAttributesUpdater ??= createRootAttributesUpdater(editor, 'main');
|
|
141
|
+
this.rootAttributesUpdater(rootAttributes);
|
|
119
142
|
}
|
|
120
143
|
|
|
121
144
|
/**
|
|
@@ -261,6 +284,8 @@ export class EditorComponentHook extends ClassHook<Snapshot> {
|
|
|
261
284
|
setEditorEditableHeight(editor, editableHeight);
|
|
262
285
|
}
|
|
263
286
|
|
|
287
|
+
this.applyRootAttributes(editor);
|
|
288
|
+
|
|
264
289
|
return editor;
|
|
265
290
|
};
|
|
266
291
|
}
|
|
@@ -350,4 +375,9 @@ export type Snapshot = {
|
|
|
350
375
|
* The language of the editor UI and content.
|
|
351
376
|
*/
|
|
352
377
|
language: EditorLanguage;
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Root attributes to apply to the main editor root.
|
|
381
|
+
*/
|
|
382
|
+
rootAttributes?: Record<string, unknown>;
|
|
353
383
|
};
|
|
@@ -3,7 +3,8 @@ import type { PluginConstructor } from 'ckeditor5';
|
|
|
3
3
|
import type { EditorComponentHook } from '../editor';
|
|
4
4
|
|
|
5
5
|
import { debounce, shallowEqual } from '../../../shared';
|
|
6
|
-
import {
|
|
6
|
+
import { isWireModelConnected } from '../../utils';
|
|
7
|
+
import { getEditorRootsValues } from '../utils';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Creates a LivewireSync plugin class.
|
|
@@ -124,10 +125,18 @@ export async function createLivewireSyncPlugin(
|
|
|
124
125
|
* Setups the content push event for the editor.
|
|
125
126
|
*/
|
|
126
127
|
private setupTypingContentPush() {
|
|
127
|
-
const {
|
|
128
|
+
const { editor } = this;
|
|
129
|
+
const { model, ui } = editor;
|
|
128
130
|
const { $wire } = component;
|
|
129
131
|
|
|
132
|
+
let isDestroyed = false;
|
|
133
|
+
|
|
130
134
|
const syncContentChange = () => {
|
|
135
|
+
/* v8 ignore next if -- @preserve */
|
|
136
|
+
if (isDestroyed) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
131
140
|
const values = this.getEditorRootsValues();
|
|
132
141
|
|
|
133
142
|
// Prevent looping when editor changed content from Livewire.
|
|
@@ -140,8 +149,23 @@ export async function createLivewireSyncPlugin(
|
|
|
140
149
|
}
|
|
141
150
|
};
|
|
142
151
|
|
|
143
|
-
|
|
144
|
-
|
|
152
|
+
const debouncedSync = debounce(saveDebounceMs, syncContentChange);
|
|
153
|
+
const onChangeData = () => {
|
|
154
|
+
if (ui.focusTracker.isFocused) {
|
|
155
|
+
debouncedSync();
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
syncContentChange();
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
model.document.on('change:data', onChangeData);
|
|
163
|
+
|
|
164
|
+
editor.once('ready', syncContentChange);
|
|
165
|
+
editor.once('destroy', () => {
|
|
166
|
+
isDestroyed = true;
|
|
167
|
+
model.document.off('change:data', onChangeData);
|
|
168
|
+
});
|
|
145
169
|
}
|
|
146
170
|
|
|
147
171
|
/**
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
export * from './create-editor-in-context';
|
|
2
2
|
export * from './get-editor-roots-values';
|
|
3
3
|
export * from './is-single-root-editor';
|
|
4
|
-
export * from './is-wire-model-connected';
|
|
5
4
|
export * from './load-editor-constructor';
|
|
6
5
|
export * from './load-editor-plugins';
|
|
7
6
|
export * from './load-editor-translations';
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks if the given element is connected to a wire:model directive in its ancestry.
|
|
3
|
+
*
|
|
4
|
+
* @param element - The HTML element to check for wire:model connection.
|
|
5
|
+
* @returns True if the element is connected to a wire:model directive, false otherwise.
|
|
6
|
+
*/
|
|
1
7
|
export function isWireModelConnected(element: HTMLElement): boolean {
|
|
2
8
|
let parent: HTMLElement | null = element;
|
|
3
9
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Editor } from 'ckeditor5';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates a function that synchronizes root attributes on the given editor root.
|
|
5
|
+
*
|
|
6
|
+
* The returned function tracks which attributes were set by itself and will only
|
|
7
|
+
* remove attributes it previously managed. This avoids interfering with other
|
|
8
|
+
* consumers that may also change attributes on the same root.
|
|
9
|
+
*
|
|
10
|
+
* @param editor The editor instance containing the root to manage.
|
|
11
|
+
* @param rootName The name of the root to manage attributes on.
|
|
12
|
+
* @returns A function that can be called with the desired set of attributes to apply them to the root.
|
|
13
|
+
* Calling the function with `null` or an empty object will clear all attributes previously set by it.
|
|
14
|
+
*/
|
|
15
|
+
export function createRootAttributesUpdater(editor: Editor, rootName: string): RootAttributesUpdater {
|
|
16
|
+
const managedAttrs = new Set<string>();
|
|
17
|
+
|
|
18
|
+
return (rootAttributes?: Record<string, unknown> | null) => {
|
|
19
|
+
editor.model.enqueueChange({ isUndoable: false }, (writer) => {
|
|
20
|
+
const root = editor.model.document.getRoot(rootName);
|
|
21
|
+
|
|
22
|
+
/* v8 ignore next if -- @preserve */
|
|
23
|
+
if (!root) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Remove previously managed attributes that are no longer requested.
|
|
28
|
+
for (const key of managedAttrs) {
|
|
29
|
+
if (rootAttributes && key in rootAttributes) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
writer.removeAttribute(key, root);
|
|
34
|
+
managedAttrs.delete(key);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Apply or overwrite requested attributes.
|
|
38
|
+
for (const [key, value] of Object.entries(rootAttributes ?? {})) {
|
|
39
|
+
writer.setAttribute(key, value, root);
|
|
40
|
+
managedAttrs.add(key);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type RootAttributesUpdater = (rootAttributes?: Record<string, unknown> | null) => void;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"is-wire-model-connected.d.ts","sourceRoot":"","sources":["../../../../../src/hooks/editor/utils/is-wire-model-connected.ts"],"names":[],"mappings":"AAAA,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAalE"}
|