ckeditor5-blazor 1.8.0 → 1.9.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/elements/editable.d.ts.map +1 -1
- package/dist/elements/editor/editor.d.ts.map +1 -1
- package/dist/index.cjs +2 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +285 -253
- package/dist/index.mjs.map +1 -1
- package/dist/interop/create-editable-blazor-interop.d.ts +5 -0
- package/dist/interop/create-editable-blazor-interop.d.ts.map +1 -1
- package/dist/interop/create-editor-blazor-interop.d.ts +4 -0
- package/dist/interop/create-editor-blazor-interop.d.ts.map +1 -1
- package/dist/interop/utils/index.d.ts +1 -0
- package/dist/interop/utils/index.d.ts.map +1 -1
- package/dist/interop/utils/sync-root-attributes.d.ts +16 -0
- package/dist/interop/utils/sync-root-attributes.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/elements/editable.test.ts +57 -0
- package/src/elements/editable.ts +11 -1
- package/src/elements/editor/editor.test.ts +15 -0
- package/src/elements/editor/editor.ts +8 -0
- package/src/interop/create-editable-blazor-interop.test.ts +55 -0
- package/src/interop/create-editable-blazor-interop.ts +22 -2
- package/src/interop/create-editor-blazor-interop.test.ts +48 -0
- package/src/interop/create-editor-blazor-interop.ts +20 -1
- package/src/interop/utils/index.ts +1 -0
- package/src/interop/utils/sync-root-attributes.ts +46 -0
|
@@ -17,5 +17,10 @@ export declare function createEditableBlazorInterop(element: HTMLElement, intero
|
|
|
17
17
|
* If the editor is focused, the update is deferred until blur to avoid interrupting the user.
|
|
18
18
|
*/
|
|
19
19
|
setValue: (value: string) => Promise<void>;
|
|
20
|
+
/**
|
|
21
|
+
* Updates the root attributes on the editor. This is useful when the Blazor component
|
|
22
|
+
* re-renders with new root attributes.
|
|
23
|
+
*/
|
|
24
|
+
setRootAttributes: (rootAttributes?: Record<string, unknown> | null) => Promise<void>;
|
|
20
25
|
};
|
|
21
26
|
//# sourceMappingURL=create-editable-blazor-interop.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-editable-blazor-interop.d.ts","sourceRoot":"","sources":["../../../src/interop/create-editable-blazor-interop.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"create-editable-blazor-interop.d.ts","sourceRoot":"","sources":["../../../src/interop/create-editable-blazor-interop.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAS9C;;;;;;;GAOG;AACH,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,aAAa;IAoDpF;;OAEG;;IAkBH;;;OAGG;sBACqB,MAAM;IAW9B;;;OAGG;yCACwC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;EAU5E"}
|
|
@@ -11,6 +11,10 @@ export declare function createEditorBlazorInterop(element: HTMLElement, interop:
|
|
|
11
11
|
* Updates the editor data from Blazor. If the editor is focused, the update is deferred until blur to avoid interrupting the user.
|
|
12
12
|
*/
|
|
13
13
|
setValue: (value: Record<string, string>) => Promise<void>;
|
|
14
|
+
/**
|
|
15
|
+
* Updates the root attributes on the editor instance.
|
|
16
|
+
*/
|
|
17
|
+
setRootAttributes: (rootAttributes?: Record<string, unknown> | null) => Promise<void>;
|
|
14
18
|
/**
|
|
15
19
|
* Cleans up all event listeners when the Blazor component is disposed.
|
|
16
20
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-editor-blazor-interop.d.ts","sourceRoot":"","sources":["../../../src/interop/create-editor-blazor-interop.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"create-editor-blazor-interop.d.ts","sourceRoot":"","sources":["../../../src/interop/create-editor-blazor-interop.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAW9C;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,aAAa;IAmElF;;OAEG;sBACqB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAS9C;;OAEG;yCACwC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAUzE;;OAEG;;IAmBH;;;;OAIG;;EAWN"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/interop/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,4BAA4B,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/interop/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,4BAA4B,CAAC;AAC3C,cAAc,wBAAwB,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Editor } from 'ckeditor5';
|
|
2
|
+
/**
|
|
3
|
+
* Creates a function that synchronizes root attributes on the given editor root.
|
|
4
|
+
*
|
|
5
|
+
* The returned function tracks which attributes were set by itself and will only
|
|
6
|
+
* remove attributes it previously managed. This avoids interfering with other
|
|
7
|
+
* consumers that may also change attributes on the same root.
|
|
8
|
+
*
|
|
9
|
+
* @param editor The editor instance containing the root to manage.
|
|
10
|
+
* @param rootName The name of the root to manage attributes on.
|
|
11
|
+
* @returns A function that can be called with the desired set of attributes to apply them to the root.
|
|
12
|
+
* Calling the function with `null` or an empty object will clear all attributes previously set by it.
|
|
13
|
+
*/
|
|
14
|
+
export declare function createRootAttributesUpdater(editor: Editor, rootName: string): RootAttributesUpdater;
|
|
15
|
+
export type RootAttributesUpdater = (rootAttributes?: Record<string, unknown> | null) => void;
|
|
16
|
+
//# sourceMappingURL=sync-root-attributes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-root-attributes.d.ts","sourceRoot":"","sources":["../../../../src/interop/utils/sync-root-attributes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAExC;;;;;;;;;;;GAWG;AACH,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,qBAAqB,CA6BnG;AAED,MAAM,MAAM,qBAAqB,GAAG,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,KAAK,IAAI,CAAC"}
|
package/package.json
CHANGED
|
@@ -139,6 +139,63 @@ describe('editable component', () => {
|
|
|
139
139
|
});
|
|
140
140
|
});
|
|
141
141
|
|
|
142
|
+
it('should apply provided root attributes to the editable root', async () => {
|
|
143
|
+
renderTestEditor({
|
|
144
|
+
preset: createEditorPreset('multiroot'),
|
|
145
|
+
content: {},
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const editor = await waitForTestEditor<MultiRootEditor>();
|
|
149
|
+
|
|
150
|
+
renderTestEditable({
|
|
151
|
+
rootName: 'foo',
|
|
152
|
+
content: '<p>Initial foo component</p>',
|
|
153
|
+
rootAttributes: {
|
|
154
|
+
'data-test-attr': 'foo-root',
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
await vi.waitFor(() => {
|
|
159
|
+
expect(editor.model.document.getRoot('foo')!.getAttribute('data-test-attr')).toBe('foo-root');
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should apply provided root attributes when editable is mounted after root already exists', async () => {
|
|
164
|
+
renderTestEditor({
|
|
165
|
+
preset: createEditorPreset('multiroot'),
|
|
166
|
+
content: {},
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const editor = await waitForTestEditor<MultiRootEditor>();
|
|
170
|
+
|
|
171
|
+
renderTestEditable({
|
|
172
|
+
rootName: 'foo',
|
|
173
|
+
content: '<p>Initial foo component</p>',
|
|
174
|
+
rootAttributes: {
|
|
175
|
+
'data-test-attr': 'foo-root',
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
editor.model.change((writer) => {
|
|
180
|
+
writer.addRoot('foo-2');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
renderTestEditable({
|
|
184
|
+
rootName: 'foo-2',
|
|
185
|
+
content: '<p>Initial foo-2 component</p>',
|
|
186
|
+
rootAttributes: {
|
|
187
|
+
'data-test-attr': 'foo-2-root',
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
await vi.waitFor(() => {
|
|
192
|
+
const { document } = editor.model;
|
|
193
|
+
|
|
194
|
+
expect(document.getRoot('foo')!.getAttribute('data-test-attr')).toBe('foo-root');
|
|
195
|
+
expect(document.getRoot('foo-2')!.getAttribute('data-test-attr')).toBe('foo-2-root');
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
142
199
|
it('should auto-assign editor ID if not provided', async () => {
|
|
143
200
|
renderTestEditor({
|
|
144
201
|
preset: createEditorPreset('multiroot'),
|
package/src/elements/editable.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { WaitForInteractiveResult } from '../shared';
|
|
|
2
2
|
import type { MultiRootEditor } from 'ckeditor5';
|
|
3
3
|
|
|
4
4
|
import { CKEditor5BlazorError } from '../ckeditor5-blazor-error';
|
|
5
|
-
import { debounce, waitForDOMReady, waitForInteractiveAttribute } from '../shared';
|
|
5
|
+
import { debounce, isEmptyObject, waitForDOMReady, waitForInteractiveAttribute } from '../shared';
|
|
6
6
|
import { EditorsRegistry } from './editor/editors-registry';
|
|
7
7
|
import { queryAllEditorIds } from './editor/utils';
|
|
8
8
|
|
|
@@ -53,6 +53,7 @@ export class EditableComponentElement extends HTMLElement {
|
|
|
53
53
|
|
|
54
54
|
const editorId = this.getAttribute('data-cke-editor-id');
|
|
55
55
|
const rootName = this.getAttribute('data-cke-root-name');
|
|
56
|
+
const rootAttributes = JSON.parse(this.getAttribute('data-cke-root-attributes') || '{}');
|
|
56
57
|
const content = this.getAttribute('data-cke-content');
|
|
57
58
|
const saveDebounceMs = Number.parseInt(this.getAttribute('data-cke-save-debounce-ms')!, 10);
|
|
58
59
|
|
|
@@ -86,11 +87,20 @@ export class EditableComponentElement extends HTMLElement {
|
|
|
86
87
|
}
|
|
87
88
|
}
|
|
88
89
|
|
|
90
|
+
// Assign attributes to the root if they are not empty.
|
|
91
|
+
// This allows users to add custom attributes to the root element of the editable.
|
|
92
|
+
if (!isEmptyObject(rootAttributes)) {
|
|
93
|
+
editor.model.change((writer) => {
|
|
94
|
+
writer.setAttributes(rootAttributes, root);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
89
98
|
return editor;
|
|
90
99
|
}
|
|
91
100
|
|
|
92
101
|
editor.addRoot(rootName, {
|
|
93
102
|
isUndoable: false,
|
|
103
|
+
attributes: { ...rootAttributes },
|
|
94
104
|
...content !== null && {
|
|
95
105
|
data: content,
|
|
96
106
|
},
|
|
@@ -103,6 +103,21 @@ describe('editor component', () => {
|
|
|
103
103
|
|
|
104
104
|
expect(editor.getData()).toBe('');
|
|
105
105
|
});
|
|
106
|
+
|
|
107
|
+
it('should apply provided root attributes to the editor root', async () => {
|
|
108
|
+
renderTestEditor({
|
|
109
|
+
rootAttributes: {
|
|
110
|
+
'data-test-attr': '123',
|
|
111
|
+
'data-another-attr': 'abc',
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const editor = await waitForTestEditor();
|
|
116
|
+
const root = editor.model.document.getRoot()!;
|
|
117
|
+
|
|
118
|
+
expect(root.getAttribute('data-test-attr')).toBe('123');
|
|
119
|
+
expect(root.getAttribute('data-another-attr')).toBe('abc');
|
|
120
|
+
});
|
|
106
121
|
});
|
|
107
122
|
|
|
108
123
|
describe('inline', () => {
|
|
@@ -151,6 +151,7 @@ export class EditorComponentElement extends HTMLElement {
|
|
|
151
151
|
const editorId = this.getAttribute('data-cke-editor-id')!;
|
|
152
152
|
const preset = JSON.parse(this.getAttribute('data-cke-preset')!) as EditorPreset;
|
|
153
153
|
const contextId = this.getAttribute('data-cke-context-id');
|
|
154
|
+
const rootAttributes = JSON.parse(this.getAttribute('data-cke-root-attributes') || '{}');
|
|
154
155
|
const editableHeight = this.getAttribute('data-cke-editable-height') ? Number.parseInt(this.getAttribute('data-cke-editable-height')!, 10) : null;
|
|
155
156
|
const saveDebounceMs = Number.parseInt(this.getAttribute('data-cke-save-debounce-ms')!, 10);
|
|
156
157
|
const language = JSON.parse(this.getAttribute('data-cke-language')!) as EditorLanguage;
|
|
@@ -279,6 +280,13 @@ export class EditorComponentElement extends HTMLElement {
|
|
|
279
280
|
return result.editor;
|
|
280
281
|
})();
|
|
281
282
|
|
|
283
|
+
// Assign root attributes if they are not empty. This is needed to support custom attributes on the root element of the editor.
|
|
284
|
+
if (!isEmptyObject(rootAttributes)) {
|
|
285
|
+
editor.model.change((writer) => {
|
|
286
|
+
writer.setAttributes(rootAttributes, editor.model.document.getRoot()!);
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
282
290
|
if (isSingleRootEditor(editorType) && editableHeight) {
|
|
283
291
|
setEditorEditableHeight(editor, editableHeight);
|
|
284
292
|
}
|
|
@@ -149,6 +149,48 @@ describe('createEditableBlazorInterop', () => {
|
|
|
149
149
|
expect(editor.getData()).toBe('<p>test</p>');
|
|
150
150
|
});
|
|
151
151
|
|
|
152
|
+
it('should set root attributes on the editor when requested', async () => {
|
|
153
|
+
const { setRootAttributes } = createEditableBlazorInterop(element, dotnetInterop);
|
|
154
|
+
|
|
155
|
+
const editor = await waitForTestEditor();
|
|
156
|
+
const root = editor.model.document.getRoot()!;
|
|
157
|
+
|
|
158
|
+
expect(root.getAttribute('data-test')).toBeUndefined();
|
|
159
|
+
|
|
160
|
+
await setRootAttributes({ 'data-test': 'value' });
|
|
161
|
+
|
|
162
|
+
expect(root.getAttribute('data-test')).toBe('value');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should only remove attributes that it previously set', async () => {
|
|
166
|
+
const { setRootAttributes } = createEditableBlazorInterop(element, dotnetInterop);
|
|
167
|
+
const editor = await waitForTestEditor();
|
|
168
|
+
const root = editor.model.document.getRoot()!;
|
|
169
|
+
|
|
170
|
+
// Simulate another consumer setting an attribute.
|
|
171
|
+
editor.model.change(writer => writer.setAttribute('data-keep', 'true', root));
|
|
172
|
+
expect(root.getAttribute('data-keep')).toBe('true');
|
|
173
|
+
|
|
174
|
+
await setRootAttributes({ 'data-test': 'value' });
|
|
175
|
+
expect(root.getAttribute('data-test')).toBe('value');
|
|
176
|
+
expect(root.getAttribute('data-keep')).toBe('true');
|
|
177
|
+
|
|
178
|
+
// Updating with the same key should not remove it.
|
|
179
|
+
await setRootAttributes({ 'data-test': 'updated' });
|
|
180
|
+
expect(root.getAttribute('data-test')).toBe('updated');
|
|
181
|
+
|
|
182
|
+
// Clearing with an empty object should remove only the attribute managed by us.
|
|
183
|
+
await setRootAttributes({});
|
|
184
|
+
|
|
185
|
+
expect(root.getAttribute('data-test')).toBeUndefined();
|
|
186
|
+
expect(root.getAttribute('data-keep')).toBe('true');
|
|
187
|
+
|
|
188
|
+
// Clearing with null should still not remove attributes managed by others.
|
|
189
|
+
await setRootAttributes(null);
|
|
190
|
+
|
|
191
|
+
expect(root.getAttribute('data-keep')).toBe('true');
|
|
192
|
+
});
|
|
193
|
+
|
|
152
194
|
it('should not set data if the interop is unmounted before the editor is ready', async () => {
|
|
153
195
|
const { setValue, unmount } = createEditableBlazorInterop(element, dotnetInterop);
|
|
154
196
|
|
|
@@ -161,6 +203,19 @@ describe('createEditableBlazorInterop', () => {
|
|
|
161
203
|
expect(editor.getData()).toBe('<p>Initial content</p>');
|
|
162
204
|
});
|
|
163
205
|
|
|
206
|
+
it('should not set root attributes if the interop is unmounted before the editor is ready', async () => {
|
|
207
|
+
const { setRootAttributes, unmount } = createEditableBlazorInterop(element, dotnetInterop);
|
|
208
|
+
|
|
209
|
+
unmount();
|
|
210
|
+
|
|
211
|
+
const editor = await waitForTestEditor();
|
|
212
|
+
const root = editor.model.document.getRoot()!;
|
|
213
|
+
|
|
214
|
+
await setRootAttributes({ 'data-test': 'value' });
|
|
215
|
+
|
|
216
|
+
expect(root.getAttribute('data-test')).toBeUndefined();
|
|
217
|
+
});
|
|
218
|
+
|
|
164
219
|
it('should delay setting data if the editor is focused', async () => {
|
|
165
220
|
const { setValue } = createEditableBlazorInterop(element, dotnetInterop);
|
|
166
221
|
const editor = await waitForTestEditor();
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { DotNetInterop } from '../types';
|
|
2
|
+
import type { RootAttributesUpdater } from './utils';
|
|
2
3
|
|
|
3
4
|
import { EditorsRegistry } from '../elements/editor/editors-registry';
|
|
4
5
|
import { CKEditor5ChangeDataEvent } from '../elements/editor/plugins/dispatch-editor-roots-change-event';
|
|
5
6
|
import { queryAllEditorIds } from '../elements/editor/utils';
|
|
6
7
|
import { markElementAsInteractive } from '../shared';
|
|
7
|
-
import { createEditorValueSync, createNoopSync } from './utils
|
|
8
|
+
import { createEditorValueSync, createNoopSync, createRootAttributesUpdater } from './utils';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Creates an interop layer to synchronize a single CKEditor 5 editable root with a Blazor component.
|
|
@@ -19,9 +20,11 @@ export function createEditableBlazorInterop(element: HTMLElement, interop: DotNe
|
|
|
19
20
|
const rootName = element.getAttribute('data-cke-root-name') ?? 'main';
|
|
20
21
|
|
|
21
22
|
let unmounted = false;
|
|
22
|
-
let sync = createNoopSync<string>();
|
|
23
23
|
let editorRef: unknown | null = null;
|
|
24
24
|
|
|
25
|
+
let sync = createNoopSync<string>();
|
|
26
|
+
let syncRootAttributes: RootAttributesUpdater | null = null;
|
|
27
|
+
|
|
25
28
|
/**
|
|
26
29
|
* Handles data change events dispatched by the CKEditor plugin.
|
|
27
30
|
* Filters by both editorId and rootName, then notifies Blazor if the root value changed.
|
|
@@ -55,6 +58,8 @@ export function createEditableBlazorInterop(element: HTMLElement, interop: DotNe
|
|
|
55
58
|
applyValue: value => editor.setData({ [rootName]: value }),
|
|
56
59
|
isEqual: (a, b) => a === b,
|
|
57
60
|
});
|
|
61
|
+
|
|
62
|
+
syncRootAttributes = createRootAttributesUpdater(editor, rootName);
|
|
58
63
|
};
|
|
59
64
|
|
|
60
65
|
void initializeSynchronization();
|
|
@@ -78,6 +83,7 @@ export function createEditableBlazorInterop(element: HTMLElement, interop: DotNe
|
|
|
78
83
|
editorRef = null;
|
|
79
84
|
}
|
|
80
85
|
|
|
86
|
+
syncRootAttributes = null;
|
|
81
87
|
unmounted = true;
|
|
82
88
|
},
|
|
83
89
|
|
|
@@ -95,5 +101,19 @@ export function createEditableBlazorInterop(element: HTMLElement, interop: DotNe
|
|
|
95
101
|
// Ensure sync is initialized before forwarding (waitFor guarantees the editor exists)
|
|
96
102
|
sync.setValue(value);
|
|
97
103
|
},
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Updates the root attributes on the editor. This is useful when the Blazor component
|
|
107
|
+
* re-renders with new root attributes.
|
|
108
|
+
*/
|
|
109
|
+
setRootAttributes: async (rootAttributes?: Record<string, unknown> | null) => {
|
|
110
|
+
if (unmounted) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
await EditorsRegistry.the.waitFor(editorId);
|
|
115
|
+
|
|
116
|
+
syncRootAttributes?.(rootAttributes);
|
|
117
|
+
},
|
|
98
118
|
};
|
|
99
119
|
}
|
|
@@ -113,6 +113,54 @@ describe('createEditorBlazorInterop', () => {
|
|
|
113
113
|
});
|
|
114
114
|
});
|
|
115
115
|
|
|
116
|
+
describe('setRootAttributes', () => {
|
|
117
|
+
it('should update root attributes on the editor', async () => {
|
|
118
|
+
const { setRootAttributes } = createEditorBlazorInterop(element, dotnetInterop);
|
|
119
|
+
|
|
120
|
+
const editor = await waitForTestEditor();
|
|
121
|
+
const root = editor.model.document.getRoot()!;
|
|
122
|
+
|
|
123
|
+
expect(root.getAttribute('data-test')).toBeUndefined();
|
|
124
|
+
|
|
125
|
+
await setRootAttributes({ 'data-test': 'value' });
|
|
126
|
+
expect(root.getAttribute('data-test')).toBe('value');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should only remove attributes that it previously set', async () => {
|
|
130
|
+
const { setRootAttributes } = createEditorBlazorInterop(element, dotnetInterop);
|
|
131
|
+
|
|
132
|
+
const editor = await waitForTestEditor();
|
|
133
|
+
const root = editor.model.document.getRoot()!;
|
|
134
|
+
|
|
135
|
+
// Simulate another consumer setting an attribute.
|
|
136
|
+
editor.model.change(writer => writer.setAttribute('data-keep', 'true', root));
|
|
137
|
+
expect(root.getAttribute('data-keep')).toBe('true');
|
|
138
|
+
|
|
139
|
+
await setRootAttributes({ 'data-test': 'value' });
|
|
140
|
+
expect(root.getAttribute('data-test')).toBe('value');
|
|
141
|
+
expect(root.getAttribute('data-keep')).toBe('true');
|
|
142
|
+
|
|
143
|
+
// Clearing should remove only the attribute managed by us.
|
|
144
|
+
await setRootAttributes(null);
|
|
145
|
+
|
|
146
|
+
expect(root.getAttribute('data-test')).toBeUndefined();
|
|
147
|
+
expect(root.getAttribute('data-keep')).toBe('true');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should not set root attributes if the interop is unmounted', async () => {
|
|
151
|
+
const { setRootAttributes, unmount } = createEditorBlazorInterop(element, dotnetInterop);
|
|
152
|
+
|
|
153
|
+
unmount();
|
|
154
|
+
|
|
155
|
+
const editor = await waitForTestEditor();
|
|
156
|
+
const root = editor.model.document.getRoot()!;
|
|
157
|
+
|
|
158
|
+
await setRootAttributes({ 'data-test': 'value' });
|
|
159
|
+
|
|
160
|
+
expect(root.getAttribute('data-test')).toBeUndefined();
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
116
164
|
describe('focus tracking', () => {
|
|
117
165
|
it('should call OnEditorFocus if editor gets focused', async () => {
|
|
118
166
|
createEditorBlazorInterop(element, dotnetInterop);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { DotNetInterop } from '../types';
|
|
2
|
+
import type { RootAttributesUpdater } from './utils';
|
|
2
3
|
import type { Editor, FileRepository } from 'ckeditor5';
|
|
3
4
|
|
|
4
5
|
import { ensureEditorElementsRegistered } from '../elements';
|
|
@@ -6,7 +7,7 @@ import { EditorsRegistry } from '../elements/editor/editors-registry';
|
|
|
6
7
|
import { CKEditor5ChangeDataEvent } from '../elements/editor/plugins/dispatch-editor-roots-change-event';
|
|
7
8
|
import { getEditorRootsValues } from '../elements/editor/utils';
|
|
8
9
|
import { markElementAsInteractive, shallowEqual } from '../shared';
|
|
9
|
-
import { createEditorValueSync, createNoopSync } from './utils
|
|
10
|
+
import { createEditorValueSync, createNoopSync, createRootAttributesUpdater } from './utils';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Creates an interop layer to synchronize a CKEditor 5 instance with a Blazor component.
|
|
@@ -22,6 +23,8 @@ export function createEditorBlazorInterop(element: HTMLElement, interop: DotNetI
|
|
|
22
23
|
let unmountCKEditorListeners: VoidFunction | null = null;
|
|
23
24
|
|
|
24
25
|
let sync = createNoopSync<Record<string, string>>();
|
|
26
|
+
let syncRootAttributes: RootAttributesUpdater | null = null;
|
|
27
|
+
|
|
25
28
|
let editorRef: unknown | null = null;
|
|
26
29
|
|
|
27
30
|
// Handles data change events dispatched by the CKEditor plugin.
|
|
@@ -49,6 +52,8 @@ export function createEditorBlazorInterop(element: HTMLElement, interop: DotNetI
|
|
|
49
52
|
isEqual: shallowEqual,
|
|
50
53
|
});
|
|
51
54
|
|
|
55
|
+
syncRootAttributes = createRootAttributesUpdater(editor, 'main');
|
|
56
|
+
|
|
52
57
|
// Notify Blazor of focus changes so it can trigger the appropriate callbacks.
|
|
53
58
|
const onFocusChange = (_evt: unknown, _name: unknown, isFocused: boolean) => {
|
|
54
59
|
const method = isFocused ? 'OnEditorFocus' : 'OnEditorBlur';
|
|
@@ -90,6 +95,19 @@ export function createEditorBlazorInterop(element: HTMLElement, interop: DotNetI
|
|
|
90
95
|
sync.setValue(value);
|
|
91
96
|
},
|
|
92
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Updates the root attributes on the editor instance.
|
|
100
|
+
*/
|
|
101
|
+
setRootAttributes: async (rootAttributes?: Record<string, unknown> | null) => {
|
|
102
|
+
if (unmounted) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
await EditorsRegistry.the.waitFor(editorId);
|
|
107
|
+
|
|
108
|
+
syncRootAttributes?.(rootAttributes);
|
|
109
|
+
},
|
|
110
|
+
|
|
93
111
|
/**
|
|
94
112
|
* Cleans up all event listeners when the Blazor component is disposed.
|
|
95
113
|
*/
|
|
@@ -107,6 +125,7 @@ export function createEditorBlazorInterop(element: HTMLElement, interop: DotNetI
|
|
|
107
125
|
editorRef = null;
|
|
108
126
|
}
|
|
109
127
|
|
|
128
|
+
syncRootAttributes = null;
|
|
110
129
|
unmounted = true;
|
|
111
130
|
},
|
|
112
131
|
|
|
@@ -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;
|