ckeditor5-blazor 1.3.0 → 1.5.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.
@@ -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;AAO9C;;;;;;;GAOG;AACH,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,aAAa;IAgDpF;;OAEG;;IAiBH;;;OAGG;sBACqB,MAAM;EAWjC"}
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;AAQ9C;;;;;;;GAOG;AACH,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,aAAa;IAgDpF;;OAEG;;IAiBH;;;OAGG;sBACqB,MAAM;EAWjC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ckeditor5-blazor",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "CKEditor 5 integration for Blazor",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -10,9 +10,22 @@ export type EditorId = string;
10
10
  export type EditorType = 'inline' | 'classic' | 'balloon' | 'decoupled' | 'multiroot';
11
11
 
12
12
  /**
13
- * Represents a CKEditor5 plugin as a string identifier.
13
+ * Represents a custom plugin loaded from a JavaScript module path.
14
+ * Serialized from C# as <c>{ "$import": { "name": "...", "path": "..." } }</c>.
14
15
  */
15
- export type EditorPlugin = string;
16
+ export type EditorPluginImport = {
17
+ $import: {
18
+ name: string;
19
+ path: string;
20
+ };
21
+ };
22
+
23
+ /**
24
+ * Represents a CKEditor5 plugin — either a built-in string identifier or a
25
+ * custom {@link EditorPluginImport} descriptor that loads a named export from
26
+ * the given JavaScript module path.
27
+ */
28
+ export type EditorPlugin = string | EditorPluginImport;
16
29
 
17
30
  /**
18
31
  * Configuration object for CKEditor5 editor instance.
@@ -1,6 +1,6 @@
1
1
  import type { EditorPlugin } from '../typings';
2
2
 
3
- import { afterEach, describe, expect, it } from 'vitest';
3
+ import { afterEach, describe, expect, it, vi } from 'vitest';
4
4
 
5
5
  import { CustomEditorPluginsRegistry } from '../custom-editor-plugins';
6
6
  import { loadEditorPlugins } from './load-editor-plugins';
@@ -11,6 +11,22 @@ class CustomPlugin {
11
11
  }
12
12
  }
13
13
 
14
+ class ImportedPlugin {
15
+ static get pluginName() {
16
+ return 'ImportedPlugin';
17
+ }
18
+ }
19
+
20
+ vi.mock('custom-import-module', () => ({
21
+ ImportedPlugin,
22
+ }));
23
+
24
+ vi.mock('default-export-module', () => ({
25
+ default: ImportedPlugin,
26
+ }));
27
+
28
+ vi.mock('empty-module', () => ({}));
29
+
14
30
  describe('loadEditorPlugins', () => {
15
31
  it('should load plugins from base package', async () => {
16
32
  const plugins: EditorPlugin[] = ['Bold', 'Italic'];
@@ -97,4 +113,35 @@ describe('loadEditorPlugins', () => {
97
113
  expect(loadedPlugins[0]).toEqual(CustomPlugin);
98
114
  });
99
115
  });
116
+
117
+ describe('import descriptor plugins', () => {
118
+ it('should load plugin from named export via $import descriptor', async () => {
119
+ const plugins: EditorPlugin[] = [{ $import: { name: 'ImportedPlugin', path: 'custom-import-module' } }];
120
+ const { loadedPlugins } = await loadEditorPlugins(plugins);
121
+
122
+ expect(loadedPlugins).toHaveLength(1);
123
+ expect(loadedPlugins[0]).toEqual(ImportedPlugin);
124
+ });
125
+
126
+ it('should fall back to default export when named export is absent', async () => {
127
+ const plugins: EditorPlugin[] = [{ $import: { name: 'NonExistent', path: 'default-export-module' } }];
128
+ const { loadedPlugins } = await loadEditorPlugins(plugins);
129
+
130
+ expect(loadedPlugins).toHaveLength(1);
131
+ expect(loadedPlugins[0]).toEqual(ImportedPlugin);
132
+ });
133
+
134
+ it('should throw when neither named nor default export is found', async () => {
135
+ const plugins: EditorPlugin[] = [{ $import: { name: 'MissingExport', path: 'empty-module' } }];
136
+ await expect(loadEditorPlugins(plugins)).rejects.toThrowError(/not found in module/);
137
+ });
138
+
139
+ it('should mix string plugins and import descriptors', async () => {
140
+ const plugins: EditorPlugin[] = ['Bold', { $import: { name: 'ImportedPlugin', path: 'custom-import-module' } }];
141
+ const { loadedPlugins } = await loadEditorPlugins(plugins);
142
+
143
+ expect(loadedPlugins).toHaveLength(2);
144
+ expect(loadedPlugins[1]).toEqual(ImportedPlugin);
145
+ });
146
+ });
100
147
  });
@@ -1,4 +1,4 @@
1
- import type { EditorPlugin } from '../typings';
1
+ import type { EditorPlugin, EditorPluginImport } from '../typings';
2
2
  import type { PluginConstructor } from 'ckeditor5';
3
3
 
4
4
  import { CKEditor5BlazorError } from '../../../ckeditor5-blazor-error';
@@ -7,8 +7,9 @@ import { CustomEditorPluginsRegistry } from '../custom-editor-plugins';
7
7
  /**
8
8
  * Loads CKEditor plugins from base and premium packages.
9
9
  * First tries to load from the base 'ckeditor5' package, then falls back to 'ckeditor5-premium-features'.
10
+ * Supports custom import descriptors ({ $import: { name, path } }) for plugins loaded from custom modules.
10
11
  *
11
- * @param plugins - Array of plugin names to load
12
+ * @param plugins - Array of plugin names or import descriptors to load
12
13
  * @returns Promise that resolves to an array of loaded Plugin instances
13
14
  * @throws Error if a plugin is not found in either package
14
15
  */
@@ -17,8 +18,21 @@ export async function loadEditorPlugins(plugins: EditorPlugin[]): Promise<Loaded
17
18
  let premiumPackage: Record<string, any> | null = null;
18
19
 
19
20
  const loaders = plugins.map(async (plugin) => {
20
- // Let's first try to load the plugin from the base package.
21
- // Coverage is disabled due to Vitest issues with mocking dynamic imports.
21
+ // Handle custom import descriptor: { $import: { name, path } }
22
+ if (isPluginImport(plugin)) {
23
+ const { name, path } = plugin.$import;
24
+
25
+ const mod = await import(/* @vite-ignore */ path);
26
+ const typedMod = mod as Record<string, unknown>;
27
+ const ctor = (Object.prototype.hasOwnProperty.call(typedMod, name) ? typedMod[name] : undefined)
28
+ ?? (Object.prototype.hasOwnProperty.call(typedMod, 'default') ? typedMod['default'] : undefined);
29
+
30
+ if (!ctor) {
31
+ throw new CKEditor5BlazorError(`Plugin "${name}" not found in module "${path}".`);
32
+ }
33
+
34
+ return ctor as PluginConstructor;
35
+ }
22
36
 
23
37
  // If the plugin is not found in the base package, try custom plugins.
24
38
  const customPlugin = await CustomEditorPluginsRegistry.the.get(plugin);
@@ -63,6 +77,13 @@ export async function loadEditorPlugins(plugins: EditorPlugin[]): Promise<Loaded
63
77
  };
64
78
  }
65
79
 
80
+ /**
81
+ * Returns `true` when the plugin entry is an import descriptor (`{ $import: ... }`).
82
+ */
83
+ function isPluginImport(plugin: EditorPlugin): plugin is EditorPluginImport {
84
+ return typeof plugin === 'object' && plugin !== null && '$import' in plugin;
85
+ }
86
+
66
87
  /**
67
88
  * Type representing the loaded plugins and whether premium features are available.
68
89
  */
@@ -78,6 +78,27 @@ describe('createEditableBlazorInterop', () => {
78
78
  );
79
79
  });
80
80
 
81
+ it('falls back to the first available editor id when data-cke-editor-id attribute is missing', async () => {
82
+ element.removeAttribute('data-cke-editor-id');
83
+
84
+ createEditableBlazorInterop(element, dotnetInterop);
85
+
86
+ const editor = await waitForTestEditor();
87
+ const changeEvent = new CKEditor5ChangeDataEvent({
88
+ editorId: DEFAULT_TEST_EDITOR_ID,
89
+ editor,
90
+ roots: { main: 'fallback changed' },
91
+ });
92
+
93
+ document.body.dispatchEvent(changeEvent);
94
+
95
+ expect(dotnetInterop.invokeMethodAsync).toHaveBeenCalledWith(
96
+ 'OnChangeEditableData',
97
+ expect.anything(),
98
+ 'fallback changed',
99
+ );
100
+ });
101
+
81
102
  it('should ignore ckeditor5 change events from other editors', async () => {
82
103
  createEditableBlazorInterop(element, dotnetInterop);
83
104
 
@@ -2,6 +2,7 @@ import type { DotNetInterop } from '../types';
2
2
 
3
3
  import { EditorsRegistry } from '../elements/editor/editors-registry';
4
4
  import { CKEditor5ChangeDataEvent } from '../elements/editor/plugins/dispatch-editor-roots-change-event';
5
+ import { queryAllEditorIds } from '../elements/editor/utils';
5
6
  import { markElementAsInteractive } from '../shared';
6
7
  import { createEditorValueSync, createNoopSync } from './utils/create-editor-value-sync';
7
8
 
@@ -14,7 +15,7 @@ import { createEditorValueSync, createNoopSync } from './utils/create-editor-val
14
15
  * @returns An object containing lifecycle and synchronization methods.
15
16
  */
16
17
  export function createEditableBlazorInterop(element: HTMLElement, interop: DotNetInterop) {
17
- const editorId = element.getAttribute('data-cke-editor-id');
18
+ const editorId = element.getAttribute('data-cke-editor-id') ?? queryAllEditorIds()[0]!;
18
19
  const rootName = element.getAttribute('data-cke-root-name') ?? 'main';
19
20
 
20
21
  let unmounted = false;