ckeditor5-blazor 1.1.0 → 1.2.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/index.cjs +2 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +89 -41
- package/dist/index.mjs.map +1 -1
- package/dist/interop/create-editor-blazor-interop.d.ts +6 -0
- package/dist/interop/create-editor-blazor-interop.d.ts.map +1 -1
- package/dist/types/dot-net-interop.type.d.ts +1 -1
- package/dist/types/dot-net-interop.type.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/interop/create-editor-blazor-interop.test.ts +108 -0
- package/src/interop/create-editor-blazor-interop.ts +85 -0
- package/src/types/dot-net-interop.type.ts +1 -1
|
@@ -15,5 +15,11 @@ export declare function createEditorBlazorInterop(element: HTMLElement, interop:
|
|
|
15
15
|
* Cleans up all event listeners when the Blazor component is disposed.
|
|
16
16
|
*/
|
|
17
17
|
unmount(): void;
|
|
18
|
+
/**
|
|
19
|
+
* Installs the custom image upload adapter that delegates uploads to Blazor.
|
|
20
|
+
* This is called lazily from Blazor when the consumer sets the `OnImageUpload` callback
|
|
21
|
+
* to avoid unnecessary overhead for consumers that don't use this feature.
|
|
22
|
+
*/
|
|
23
|
+
attachImageUploadAdapter: () => Promise<void>;
|
|
18
24
|
};
|
|
19
25
|
//# sourceMappingURL=create-editor-blazor-interop.d.ts.map
|
|
@@ -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;AAU9C;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,aAAa;IA+DlF;;OAEG;sBACqB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAS9C;;OAEG;;IAkBH;;;;OAIG;;EAWN"}
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
* Represents the .NET interop helper for communication with Blazor.
|
|
3
3
|
*/
|
|
4
4
|
export type DotNetInterop = {
|
|
5
|
-
invokeMethodAsync: (methodName: string, ...args: any[]) => Promise<
|
|
5
|
+
invokeMethodAsync: <T = void>(methodName: string, ...args: any[]) => Promise<T>;
|
|
6
6
|
};
|
|
7
7
|
//# sourceMappingURL=dot-net-interop.type.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dot-net-interop.type.d.ts","sourceRoot":"","sources":["../../../src/types/dot-net-interop.type.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,iBAAiB,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,
|
|
1
|
+
{"version":3,"file":"dot-net-interop.type.d.ts","sourceRoot":"","sources":["../../../src/types/dot-net-interop.type.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,iBAAiB,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;CACjF,CAAC"}
|
package/package.json
CHANGED
|
@@ -180,4 +180,112 @@ describe('createEditorBlazorInterop', () => {
|
|
|
180
180
|
}).not.toThrow();
|
|
181
181
|
});
|
|
182
182
|
});
|
|
183
|
+
|
|
184
|
+
describe('attachImageUploadAdapter', () => {
|
|
185
|
+
it('should do nothing when already unmounted', async () => {
|
|
186
|
+
const interop = createEditorBlazorInterop(element, dotnetInterop);
|
|
187
|
+
|
|
188
|
+
await waitForTestEditor();
|
|
189
|
+
interop.unmount();
|
|
190
|
+
|
|
191
|
+
await expect(interop.attachImageUploadAdapter()).resolves.toBeUndefined();
|
|
192
|
+
expect(dotnetInterop.invokeMethodAsync).not.toHaveBeenCalledWith(
|
|
193
|
+
'OnEditorImageUpload',
|
|
194
|
+
expect.anything(),
|
|
195
|
+
);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should do nothing when FileRepository plugin is not available', async () => {
|
|
199
|
+
const { attachImageUploadAdapter } = createEditorBlazorInterop(element, dotnetInterop);
|
|
200
|
+
const editor = await waitForTestEditor();
|
|
201
|
+
|
|
202
|
+
expect(editor.plugins.has('FileRepository')).toBe(false);
|
|
203
|
+
|
|
204
|
+
await expect(attachImageUploadAdapter()).resolves.toBeUndefined();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('should set createUploadAdapter on FileRepository when plugin is available', async () => {
|
|
208
|
+
const { attachImageUploadAdapter } = createEditorBlazorInterop(element, dotnetInterop);
|
|
209
|
+
const editor = await waitForTestEditor();
|
|
210
|
+
|
|
211
|
+
const mockFileRepository: { createUploadAdapter: any; } = { createUploadAdapter: null };
|
|
212
|
+
|
|
213
|
+
vi.spyOn(editor.plugins, 'has').mockReturnValue(true);
|
|
214
|
+
vi.spyOn(editor.plugins, 'get').mockReturnValue(mockFileRepository as any);
|
|
215
|
+
|
|
216
|
+
await attachImageUploadAdapter();
|
|
217
|
+
|
|
218
|
+
expect(mockFileRepository.createUploadAdapter).toBeTypeOf('function');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('upload adapter should call OnEditorImageUpload with file details and return the url', async () => {
|
|
222
|
+
const { attachImageUploadAdapter } = createEditorBlazorInterop(element, dotnetInterop);
|
|
223
|
+
const editor = await waitForTestEditor();
|
|
224
|
+
|
|
225
|
+
const mockFileRepository: { createUploadAdapter: any; } = { createUploadAdapter: null };
|
|
226
|
+
|
|
227
|
+
vi.spyOn(editor.plugins, 'has').mockReturnValue(true);
|
|
228
|
+
vi.spyOn(editor.plugins, 'get').mockReturnValue(mockFileRepository as any);
|
|
229
|
+
|
|
230
|
+
await attachImageUploadAdapter();
|
|
231
|
+
|
|
232
|
+
const expectedUrl = 'https://example.com/uploaded.jpg';
|
|
233
|
+
|
|
234
|
+
(dotnetInterop.invokeMethodAsync as Mock).mockResolvedValueOnce(expectedUrl);
|
|
235
|
+
|
|
236
|
+
const mockFile = new File(['fake image data'], 'photo.jpg', { type: 'image/jpeg' });
|
|
237
|
+
const adapter = mockFileRepository.createUploadAdapter({ file: Promise.resolve(mockFile) });
|
|
238
|
+
|
|
239
|
+
const result = await adapter.upload();
|
|
240
|
+
|
|
241
|
+
expect(dotnetInterop.invokeMethodAsync).toHaveBeenCalledWith(
|
|
242
|
+
'OnEditorImageUpload',
|
|
243
|
+
expect.objectContaining({
|
|
244
|
+
fileName: 'photo.jpg',
|
|
245
|
+
mimeType: 'image/jpeg',
|
|
246
|
+
payload: expect.any(String),
|
|
247
|
+
}),
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
expect(result).toEqual({ default: expectedUrl });
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('upload adapter should throw when OnEditorImageUpload returns null', async () => {
|
|
254
|
+
const { attachImageUploadAdapter } = createEditorBlazorInterop(element, dotnetInterop);
|
|
255
|
+
const editor = await waitForTestEditor();
|
|
256
|
+
|
|
257
|
+
const mockFileRepository: { createUploadAdapter: any; } = { createUploadAdapter: null };
|
|
258
|
+
|
|
259
|
+
vi.spyOn(editor.plugins, 'has').mockReturnValue(true);
|
|
260
|
+
vi.spyOn(editor.plugins, 'get').mockReturnValue(mockFileRepository as any);
|
|
261
|
+
|
|
262
|
+
await attachImageUploadAdapter();
|
|
263
|
+
|
|
264
|
+
(dotnetInterop.invokeMethodAsync as Mock).mockResolvedValueOnce(null);
|
|
265
|
+
|
|
266
|
+
const mockFile = new File(['fake image data'], 'photo.jpg', { type: 'image/jpeg' });
|
|
267
|
+
const adapter = mockFileRepository.createUploadAdapter({ file: Promise.resolve(mockFile) });
|
|
268
|
+
|
|
269
|
+
await expect(adapter.upload()).rejects.toThrow('OnImageUpload handler returned null');
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('upload adapter should throw when aborted before file read completes', async () => {
|
|
273
|
+
const { attachImageUploadAdapter } = createEditorBlazorInterop(element, dotnetInterop);
|
|
274
|
+
const editor = await waitForTestEditor();
|
|
275
|
+
|
|
276
|
+
const mockFileRepository: { createUploadAdapter: any; } = { createUploadAdapter: null };
|
|
277
|
+
|
|
278
|
+
vi.spyOn(editor.plugins, 'has').mockReturnValue(true);
|
|
279
|
+
vi.spyOn(editor.plugins, 'get').mockReturnValue(mockFileRepository as any);
|
|
280
|
+
|
|
281
|
+
await attachImageUploadAdapter();
|
|
282
|
+
|
|
283
|
+
const mockFile = new File(['fake image data'], 'photo.jpg', { type: 'image/jpeg' });
|
|
284
|
+
const adapter = mockFileRepository.createUploadAdapter({ file: Promise.resolve(mockFile) });
|
|
285
|
+
|
|
286
|
+
adapter.abort();
|
|
287
|
+
|
|
288
|
+
await expect(adapter.upload()).rejects.toThrow('Upload aborted.');
|
|
289
|
+
});
|
|
290
|
+
});
|
|
183
291
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { DotNetInterop } from '../types';
|
|
2
|
+
import type { Editor, FileRepository } from 'ckeditor5';
|
|
2
3
|
|
|
3
4
|
import { ensureEditorElementsRegistered } from '../elements';
|
|
4
5
|
import { EditorsRegistry } from '../elements/editor/editors-registry';
|
|
@@ -108,5 +109,89 @@ export function createEditorBlazorInterop(element: HTMLElement, interop: DotNetI
|
|
|
108
109
|
|
|
109
110
|
unmounted = true;
|
|
110
111
|
},
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Installs the custom image upload adapter that delegates uploads to Blazor.
|
|
115
|
+
* This is called lazily from Blazor when the consumer sets the `OnImageUpload` callback
|
|
116
|
+
* to avoid unnecessary overhead for consumers that don't use this feature.
|
|
117
|
+
*/
|
|
118
|
+
attachImageUploadAdapter: async () => {
|
|
119
|
+
if (unmounted) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const editor = await EditorsRegistry.the.waitFor(editorId);
|
|
124
|
+
|
|
125
|
+
installImageUploadAdapter(editor, interop);
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Installs a custom CKEditor 5 upload adapter that delegates image uploads to Blazor.
|
|
132
|
+
* When the user inserts an image the adapter encodes the file as Base64 and calls
|
|
133
|
+
* `OnEditorImageUpload` on the .NET interop object, which returns the public URL to embed.
|
|
134
|
+
*/
|
|
135
|
+
function installImageUploadAdapter(editor: Editor, interop: DotNetInterop) {
|
|
136
|
+
if (!editor.plugins.has('FileRepository')) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const fileRepository = editor.plugins.get('FileRepository') as FileRepository;
|
|
141
|
+
|
|
142
|
+
fileRepository.createUploadAdapter = (loader: any) => {
|
|
143
|
+
let aborted = false;
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
async upload() {
|
|
147
|
+
const file: File = await loader.file;
|
|
148
|
+
|
|
149
|
+
if (aborted) {
|
|
150
|
+
throw new Error('Upload aborted.');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const payload = await fileToBase64(file);
|
|
154
|
+
const url = await interop.invokeMethodAsync<string | null>('OnEditorImageUpload', {
|
|
155
|
+
fileName: file.name,
|
|
156
|
+
mimeType: file.type,
|
|
157
|
+
payload,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
if (!url) {
|
|
161
|
+
throw new Error(
|
|
162
|
+
'OnImageUpload handler returned null. '
|
|
163
|
+
+ 'Make sure the OnImageUpload parameter is set on the <CKE5Editor> component.',
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return { default: url };
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
abort() {
|
|
171
|
+
aborted = true;
|
|
172
|
+
},
|
|
173
|
+
};
|
|
111
174
|
};
|
|
112
175
|
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Converts a File object to a Base64-encoded string (data-URL prefix stripped).
|
|
179
|
+
*/
|
|
180
|
+
function fileToBase64(file: File): Promise<string> {
|
|
181
|
+
return new Promise<string>((resolve, reject) => {
|
|
182
|
+
const reader = new FileReader();
|
|
183
|
+
|
|
184
|
+
reader.onload = () => {
|
|
185
|
+
const result = reader.result as string;
|
|
186
|
+
|
|
187
|
+
/* v8 ignore next -- @preserve */
|
|
188
|
+
const base64 = result.split(',')[1] ?? result;
|
|
189
|
+
|
|
190
|
+
resolve(base64);
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
/* v8 ignore next -- @preserve */
|
|
194
|
+
reader.onerror = () => reject(reader.error);
|
|
195
|
+
reader.readAsDataURL(file);
|
|
196
|
+
});
|
|
197
|
+
}
|