ckeditor5-livewire 1.2.10 → 1.3.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 +17 -0
- package/dist/hooks/editable.d.ts.map +1 -1
- package/dist/hooks/editor/plugins/livewire-sync.d.ts.map +1 -1
- package/dist/index.cjs +2 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +182 -137
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/hooks/editable.test.ts +131 -6
- package/src/hooks/editable.ts +89 -32
- package/src/hooks/editor/plugins/livewire-sync.ts +26 -18
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
|
|
16
16
|
import type { Snapshot as EditorSnapshot } from './editor';
|
|
17
17
|
|
|
18
|
+
import { timeout } from '../shared';
|
|
18
19
|
import { EditableComponentHook } from './editable';
|
|
19
20
|
import { EditorComponentHook } from './editor';
|
|
20
21
|
import { registerLivewireComponentHook } from './hook';
|
|
@@ -267,7 +268,9 @@ describe('editable component', () => {
|
|
|
267
268
|
it('should update editable content after commit if content changed in Livewire', async () => {
|
|
268
269
|
const { id } = livewireStub.$internal.appendComponentToDOM({
|
|
269
270
|
name: 'ckeditor5-editable',
|
|
270
|
-
el: createEditableHtmlElement(
|
|
271
|
+
el: createEditableHtmlElement({
|
|
272
|
+
wireModel: 'content',
|
|
273
|
+
}),
|
|
271
274
|
canonical: createEditableSnapshot('foo', '<p>Initial foo content</p>'),
|
|
272
275
|
});
|
|
273
276
|
|
|
@@ -286,7 +289,7 @@ describe('editable component', () => {
|
|
|
286
289
|
});
|
|
287
290
|
});
|
|
288
291
|
|
|
289
|
-
it('should not update editable content after commit if
|
|
292
|
+
it('should not update editable content after commit if `wire.model` is not set', async () => {
|
|
290
293
|
const { id } = livewireStub.$internal.appendComponentToDOM({
|
|
291
294
|
name: 'ckeditor5-editable',
|
|
292
295
|
el: createEditableHtmlElement(),
|
|
@@ -299,6 +302,29 @@ describe('editable component', () => {
|
|
|
299
302
|
|
|
300
303
|
const editor = await waitForTestEditor();
|
|
301
304
|
|
|
305
|
+
await livewireStub.$internal.dispatchComponentCommit(id, {
|
|
306
|
+
content: '<p>Updated foo content from Livewire</p>',
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
await timeout(50);
|
|
310
|
+
expect(editor.getData({ rootName: 'foo' })).toBe('<p>Initial foo content</p>');
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('should not update editable content after commit if content has not changed', async () => {
|
|
314
|
+
const { id } = livewireStub.$internal.appendComponentToDOM({
|
|
315
|
+
name: 'ckeditor5-editable',
|
|
316
|
+
el: createEditableHtmlElement({
|
|
317
|
+
wireModel: 'content',
|
|
318
|
+
}),
|
|
319
|
+
canonical: createEditableSnapshot('foo', '<p>Initial foo content</p>'),
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
appendMultirootEditor({
|
|
323
|
+
foo: '<p>Initial foo content</p>',
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
const editor = await waitForTestEditor();
|
|
327
|
+
|
|
302
328
|
await livewireStub.$internal.dispatchComponentCommit(id, {
|
|
303
329
|
content: '<p>Initial foo content</p>',
|
|
304
330
|
});
|
|
@@ -309,7 +335,9 @@ describe('editable component', () => {
|
|
|
309
335
|
it('should handle null content in commit', async () => {
|
|
310
336
|
const { id } = livewireStub.$internal.appendComponentToDOM({
|
|
311
337
|
name: 'ckeditor5-editable',
|
|
312
|
-
el: createEditableHtmlElement(
|
|
338
|
+
el: createEditableHtmlElement({
|
|
339
|
+
wireModel: 'content',
|
|
340
|
+
}),
|
|
313
341
|
canonical: createEditableSnapshot('foo', '<p>Initial foo content</p>'),
|
|
314
342
|
});
|
|
315
343
|
|
|
@@ -328,13 +356,106 @@ describe('editable component', () => {
|
|
|
328
356
|
});
|
|
329
357
|
});
|
|
330
358
|
|
|
359
|
+
it('should defer editable content update if editable is focused during commit and apply it on blur', async () => {
|
|
360
|
+
const { id } = livewireStub.$internal.appendComponentToDOM({
|
|
361
|
+
name: 'ckeditor5-editable',
|
|
362
|
+
el: createEditableHtmlElement({
|
|
363
|
+
wireModel: 'content',
|
|
364
|
+
}),
|
|
365
|
+
canonical: createEditableSnapshot('foo', '<p>Initial foo content</p>'),
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
appendMultirootEditor({
|
|
369
|
+
foo: '<p>Initial foo content</p>',
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
const editor = await waitForTestEditor();
|
|
373
|
+
const { ui: { focusTracker } } = editor;
|
|
374
|
+
|
|
375
|
+
focusTracker.isFocused = true;
|
|
376
|
+
|
|
377
|
+
await livewireStub.$internal.dispatchComponentCommit(id, {
|
|
378
|
+
content: '<p>Updated foo content from Livewire</p>',
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
expect(editor.getData({ rootName: 'foo' })).toBe('<p>Initial foo content</p>');
|
|
382
|
+
|
|
383
|
+
focusTracker.isFocused = false;
|
|
384
|
+
|
|
385
|
+
expect(editor.getData({ rootName: 'foo' })).toBe('<p>Updated foo content from Livewire</p>');
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it('should abort editable content update if editable is focused during commit and content changes before blur', async () => {
|
|
389
|
+
const { id } = livewireStub.$internal.appendComponentToDOM({
|
|
390
|
+
name: 'ckeditor5-editable',
|
|
391
|
+
el: createEditableHtmlElement({
|
|
392
|
+
wireModel: 'content',
|
|
393
|
+
}),
|
|
394
|
+
canonical: createEditableSnapshot('foo', '<p>Initial foo content</p>'),
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
appendMultirootEditor({
|
|
398
|
+
foo: '<p>Initial foo content</p>',
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
const editor = await waitForTestEditor();
|
|
402
|
+
const { ui: { focusTracker } } = editor;
|
|
403
|
+
|
|
404
|
+
focusTracker.isFocused = true;
|
|
405
|
+
|
|
406
|
+
await livewireStub.$internal.dispatchComponentCommit(id, {
|
|
407
|
+
content: '<p>Updated foo content from Livewire</p>',
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
expect(editor.getData({ rootName: 'foo' })).toBe('<p>Initial foo content</p>');
|
|
411
|
+
|
|
412
|
+
editor.setData({
|
|
413
|
+
foo: '<p>Content changed by user</p>',
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
focusTracker.isFocused = false;
|
|
417
|
+
|
|
418
|
+
expect(editor.getData({ rootName: 'foo' })).toBe('<p>Content changed by user</p>');
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it('should defer editable null content update if editable is focused during commit and apply it on blur', async () => {
|
|
422
|
+
const { id } = livewireStub.$internal.appendComponentToDOM({
|
|
423
|
+
name: 'ckeditor5-editable',
|
|
424
|
+
el: createEditableHtmlElement({
|
|
425
|
+
wireModel: 'content',
|
|
426
|
+
}),
|
|
427
|
+
canonical: createEditableSnapshot('foo', '<p>Initial foo content</p>'),
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
appendMultirootEditor({
|
|
431
|
+
foo: '<p>Initial foo content</p>',
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
const editor = await waitForTestEditor();
|
|
435
|
+
const { ui: { focusTracker } } = editor;
|
|
436
|
+
|
|
437
|
+
focusTracker.isFocused = true;
|
|
438
|
+
|
|
439
|
+
await livewireStub.$internal.dispatchComponentCommit(id, {
|
|
440
|
+
content: null,
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
expect(editor.getData({ rootName: 'foo' })).toBe('<p>Initial foo content</p>');
|
|
444
|
+
|
|
445
|
+
focusTracker.isFocused = false;
|
|
446
|
+
|
|
447
|
+
expect(editor.getData({ rootName: 'foo' })).toBe('');
|
|
448
|
+
});
|
|
449
|
+
|
|
331
450
|
it('should not crash if editor is not found during commit', async () => {
|
|
332
451
|
appendMultirootEditor();
|
|
333
452
|
await waitForTestEditor();
|
|
334
453
|
|
|
335
454
|
const { id } = livewireStub.$internal.appendComponentToDOM({
|
|
336
455
|
name: 'ckeditor5-editable',
|
|
337
|
-
el: createEditableHtmlElement(
|
|
456
|
+
el: createEditableHtmlElement({
|
|
457
|
+
wireModel: 'content',
|
|
458
|
+
}),
|
|
338
459
|
canonical: createEditableSnapshot('foo', '<p>Initial foo content</p>'),
|
|
339
460
|
});
|
|
340
461
|
|
|
@@ -352,13 +473,17 @@ describe('editable component', () => {
|
|
|
352
473
|
it('should sync multiple editables independently after commit', async () => {
|
|
353
474
|
const { id: fooId } = livewireStub.$internal.appendComponentToDOM({
|
|
354
475
|
name: 'ckeditor5-editable',
|
|
355
|
-
el: createEditableHtmlElement(
|
|
476
|
+
el: createEditableHtmlElement({
|
|
477
|
+
wireModel: 'content',
|
|
478
|
+
}),
|
|
356
479
|
canonical: createEditableSnapshot('foo', '<p>Initial foo content</p>'),
|
|
357
480
|
});
|
|
358
481
|
|
|
359
482
|
const { id: barId } = livewireStub.$internal.appendComponentToDOM({
|
|
360
483
|
name: 'ckeditor5-editable',
|
|
361
|
-
el: createEditableHtmlElement(
|
|
484
|
+
el: createEditableHtmlElement({
|
|
485
|
+
wireModel: 'content',
|
|
486
|
+
}),
|
|
362
487
|
canonical: createEditableSnapshot('bar', '<p>Initial bar content</p>'),
|
|
363
488
|
});
|
|
364
489
|
|
package/src/hooks/editable.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { MultiRootEditor } from 'ckeditor5';
|
|
|
2
2
|
|
|
3
3
|
import { debounce } from '../shared';
|
|
4
4
|
import { EditorsRegistry } from './editor/editors-registry';
|
|
5
|
+
import { isWireModelConnected } from './editor/utils';
|
|
5
6
|
import { ClassHook } from './hook';
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -13,12 +14,16 @@ export class EditableComponentHook extends ClassHook<Snapshot> {
|
|
|
13
14
|
*/
|
|
14
15
|
private editorPromise: Promise<MultiRootEditor | null> | null = null;
|
|
15
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Pending content to apply when the editor loses focus.
|
|
19
|
+
*/
|
|
20
|
+
private pendingContent: string | null = null;
|
|
21
|
+
|
|
16
22
|
/**
|
|
17
23
|
* Mounts the editable component.
|
|
18
24
|
*/
|
|
19
25
|
override mounted() {
|
|
20
|
-
const { editorId, rootName, content
|
|
21
|
-
const input = this.element.querySelector<HTMLInputElement>('input');
|
|
26
|
+
const { editorId, rootName, content } = this.canonical;
|
|
22
27
|
|
|
23
28
|
// If the editor is not registered yet, we will wait for it to be registered.
|
|
24
29
|
this.editorPromise = EditorsRegistry.the.execute(editorId, (editor: MultiRootEditor) => {
|
|
@@ -41,54 +46,106 @@ export class EditableComponentHook extends ClassHook<Snapshot> {
|
|
|
41
46
|
});
|
|
42
47
|
}
|
|
43
48
|
}
|
|
44
|
-
|
|
45
|
-
return editor;
|
|
46
49
|
}
|
|
50
|
+
else {
|
|
51
|
+
editor.addRoot(rootName, {
|
|
52
|
+
isUndoable: false,
|
|
53
|
+
...content !== null && {
|
|
54
|
+
data: content,
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const contentElement = this.element.querySelector('[data-cke-editable-content]') as HTMLElement | null;
|
|
59
|
+
const editable = ui.view.createEditable(rootName, contentElement!);
|
|
60
|
+
|
|
61
|
+
ui.addEditable(editable);
|
|
62
|
+
editing.view.forceRender();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Add livewire sync.
|
|
66
|
+
this.syncTypingContentPush(editor);
|
|
67
|
+
this.setupPendingReceivedContentHandlers(editor);
|
|
47
68
|
|
|
48
|
-
editor
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
data: content,
|
|
52
|
-
},
|
|
53
|
-
});
|
|
69
|
+
return editor;
|
|
70
|
+
});
|
|
71
|
+
}
|
|
54
72
|
|
|
55
|
-
|
|
56
|
-
|
|
73
|
+
/**
|
|
74
|
+
* Setups the content sync from the editor to Livewire on user input with debounce.
|
|
75
|
+
*/
|
|
76
|
+
private syncTypingContentPush(editor: MultiRootEditor) {
|
|
77
|
+
const { rootName, saveDebounceMs } = this.canonical;
|
|
78
|
+
const input = this.element.querySelector<HTMLInputElement>('input');
|
|
57
79
|
|
|
58
|
-
|
|
59
|
-
|
|
80
|
+
const sync = () => {
|
|
81
|
+
const html = editor.getData({ rootName });
|
|
60
82
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
83
|
+
if (input) {
|
|
84
|
+
input.value = html;
|
|
85
|
+
}
|
|
64
86
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
87
|
+
this.$wire.set('content', html);
|
|
88
|
+
};
|
|
68
89
|
|
|
69
|
-
|
|
70
|
-
|
|
90
|
+
editor.model.document.on('change:data', debounce(saveDebounceMs, sync));
|
|
91
|
+
sync();
|
|
92
|
+
}
|
|
71
93
|
|
|
72
|
-
|
|
73
|
-
|
|
94
|
+
/**
|
|
95
|
+
* Sets up handlers that manage pending incoming content (clears pending
|
|
96
|
+
* content on user edits and applies pending content on blur).
|
|
97
|
+
*/
|
|
98
|
+
private setupPendingReceivedContentHandlers(editor: MultiRootEditor): void {
|
|
99
|
+
const { ui, model } = editor;
|
|
100
|
+
const { rootName } = this.canonical;
|
|
74
101
|
|
|
75
|
-
|
|
102
|
+
model.document.on('change:data', () => {
|
|
103
|
+
this.pendingContent = null;
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
ui.focusTracker.on('change:isFocused', () => {
|
|
107
|
+
if (!ui.focusTracker.isFocused && this.pendingContent !== null) {
|
|
108
|
+
editor.setData({
|
|
109
|
+
[rootName]: this.pendingContent,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
this.pendingContent = null;
|
|
113
|
+
}
|
|
76
114
|
});
|
|
77
115
|
}
|
|
78
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Applies canonical content to the editor while respecting focus/pending state.
|
|
119
|
+
*/
|
|
120
|
+
private applyCanonicalContentToEditor(editor: MultiRootEditor): void {
|
|
121
|
+
if (!isWireModelConnected(this.element)) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const { content, rootName } = this.canonical;
|
|
126
|
+
const { ui } = editor;
|
|
127
|
+
|
|
128
|
+
const currentValue = editor.getData({ rootName });
|
|
129
|
+
|
|
130
|
+
if (currentValue === (content ?? '')) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (ui.focusTracker.isFocused) {
|
|
135
|
+
this.pendingContent = content ?? '';
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
editor.setData({ [rootName]: content ?? '' });
|
|
140
|
+
}
|
|
141
|
+
|
|
79
142
|
/**
|
|
80
143
|
* Called when the component is updated by Livewire.
|
|
81
144
|
*/
|
|
82
145
|
override async afterCommitSynced(): Promise<void> {
|
|
83
146
|
const editor = (await this.editorPromise)!;
|
|
84
|
-
const { content, rootName } = this.canonical;
|
|
85
|
-
const value = editor.getData({ rootName });
|
|
86
147
|
|
|
87
|
-
|
|
88
|
-
editor.setData({
|
|
89
|
-
[rootName]: content ?? '',
|
|
90
|
-
});
|
|
91
|
-
}
|
|
148
|
+
this.applyCanonicalContentToEditor(editor);
|
|
92
149
|
}
|
|
93
150
|
|
|
94
151
|
/**
|
|
@@ -30,26 +30,29 @@ export async function createLivewireSyncPlugin(
|
|
|
30
30
|
public init(): void {
|
|
31
31
|
this.setupTypingContentPush();
|
|
32
32
|
this.setupFocusableEventPush();
|
|
33
|
-
this.
|
|
33
|
+
this.setupAfterCommitHandler();
|
|
34
|
+
this.setupSetEditorContentHandler();
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
/**
|
|
37
|
-
* Setups the content
|
|
38
|
+
* Setups handler that updates the editor content after Livewire changes attributes
|
|
39
|
+
* on the component and commits the changes, but only if the editor is not focused to prevent
|
|
40
|
+
* disrupting the user while editing.
|
|
38
41
|
*/
|
|
39
|
-
private
|
|
42
|
+
private setupAfterCommitHandler() {
|
|
40
43
|
const { editor } = this;
|
|
41
44
|
const { model, ui: { focusTracker } } = editor;
|
|
42
45
|
|
|
43
46
|
let pendingContent: Record<string, string> | null = null;
|
|
44
47
|
|
|
45
48
|
editor.on('afterCommitSynced', () => {
|
|
46
|
-
const { content } = component.canonical;
|
|
47
|
-
const values = this.getEditorRootsValues();
|
|
48
|
-
|
|
49
49
|
if (!isWireModelConnected(component.element)) {
|
|
50
50
|
return;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
const { content } = component.canonical;
|
|
54
|
+
const values = this.getEditorRootsValues();
|
|
55
|
+
|
|
53
56
|
// If editor is focused, save the content to apply later when it blurs.
|
|
54
57
|
if (focusTracker.isFocused) {
|
|
55
58
|
if (!shallowEqual(content, values)) {
|
|
@@ -64,18 +67,6 @@ export async function createLivewireSyncPlugin(
|
|
|
64
67
|
}
|
|
65
68
|
});
|
|
66
69
|
|
|
67
|
-
Livewire.on('set-editor-content', ({ editorId, content }: SetContentPayload) => {
|
|
68
|
-
if (editorId !== component.canonical.editorId) {
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const currentValues = this.getEditorRootsValues();
|
|
73
|
-
|
|
74
|
-
if (!shallowEqual(currentValues, content)) {
|
|
75
|
-
editor.setData(content);
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
|
|
79
70
|
// Track user changes while focused.
|
|
80
71
|
model.document.on('change:data', () => {
|
|
81
72
|
pendingContent = null;
|
|
@@ -90,6 +81,23 @@ export async function createLivewireSyncPlugin(
|
|
|
90
81
|
});
|
|
91
82
|
}
|
|
92
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Setups the content sync from Livewire to the editor when Livewire emits an event.
|
|
86
|
+
*/
|
|
87
|
+
private setupSetEditorContentHandler() {
|
|
88
|
+
Livewire.on('set-editor-content', ({ editorId, content }: SetContentPayload) => {
|
|
89
|
+
if (editorId !== component.canonical.editorId) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const currentValues = this.getEditorRootsValues();
|
|
94
|
+
|
|
95
|
+
if (!shallowEqual(currentValues, content)) {
|
|
96
|
+
this.editor.setData(content);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
93
101
|
/**
|
|
94
102
|
* Setups the content push event for the editor.
|
|
95
103
|
*/
|