Skip to content

Commit 5757d78

Browse files
committed
feat(playground): register Header tool
Header wasn't wired into the playground, so there was no way to see its config-driven toolbox running in the actual editor rather than only in its own unit tests. Registered with all six levels enabled via config.levels. Getting it to actually work surfaced two bugs: BlockManager.insert flattened a block's data onto its top-level properties instead of nesting it under data, dropping every tool's data on insert; and ToolboxUI.addTool only added one toolbox entry for a tool with several, instead of iterating all of them.
1 parent 024f958 commit 5757d78

12 files changed

Lines changed: 125 additions & 38 deletions

File tree

packages/core/src/components/BlockManager.spec.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,22 @@ describe('BlocksManager (unit, mocked deps)', () => {
143143
);
144144
});
145145

146+
it('should nest data under a data property instead of flattening it onto the block', () => {
147+
blocksManager.insert({
148+
type: 'header',
149+
data: { level: 2 }
150+
});
151+
152+
expect(model.addBlock).toHaveBeenCalledWith(
153+
USER_ID,
154+
expect.objectContaining({
155+
name: 'header',
156+
data: { level: 2 }
157+
}),
158+
BLOCKS_COUNT
159+
);
160+
});
161+
146162
it('should use model.length as insertion/removal index when replace is true and index is omitted', () => {
147163
blocksManager.insert({
148164
replace: true

packages/core/src/components/BlockManager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ export class BlocksManager {
145145
}
146146

147147
this.#model.addBlock(userId, {
148-
...data,
148+
data,
149149
id,
150150
name: type,
151151
}, newIndex);

packages/editorjs/src/index.spec.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,23 @@ describe('EditorJS bundle', () => {
7979
expect(registered).toContain('ClipboardPlugin');
8080
expect(registered).toContain('ShortcutsPlugin');
8181
});
82+
83+
it('forwards a tuple-registered user tool\'s options to core.use', () => {
84+
initialize.mockResolvedValue(undefined);
85+
class Header { public static name = 'header'; }
86+
const options = { config: { levels: [1, 2, 3] } };
87+
88+
void new EditorJS({ tools: { header: [Header, options] } } as any);
89+
90+
expect(use).toHaveBeenCalledWith(Header, options);
91+
});
92+
93+
it('registers a bare user tool with no options', () => {
94+
initialize.mockResolvedValue(undefined);
95+
class Header { public static name = 'header'; }
96+
97+
void new EditorJS({ tools: { header: Header } } as any);
98+
99+
expect(use).toHaveBeenCalledWith(Header, undefined);
100+
});
82101
});

packages/editorjs/src/index.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { LinkInlineTool } from '@editorjs/inline-link';
99
import { ClipboardPlugin } from '@editorjs/clipboard-plugin';
1010
import { ShortcutsPlugin } from '@editorjs/shortcuts-plugin';
1111
import { EditorjsUI, BlocksUI, InlineToolbarUI, ToolbarUI, ToolboxUI } from '@editorjs/ui';
12-
import { mergeTools } from './mergeTools.js';
12+
import { mergeTools, type ToolEntry } from './mergeTools.js';
1313

1414
/**
1515
* Default tools registered by the bundle, keyed later by their static `name`.
@@ -32,8 +32,10 @@ export type EditorJSConfig = Omit<CoreConfig, 'tools'> & {
3232
/**
3333
* User tools to register on top of the defaults. A tool provided under a name
3434
* that matches a default tool replaces that default instead of duplicating it.
35+
* Pass a `[tool, options]` tuple instead of a bare constructor to forward
36+
* options (most commonly `config`) to `core.use`.
3537
*/
36-
tools?: Record<string, ToolConstructable>;
38+
tools?: Record<string, ToolEntry>;
3739
};
3840

3941
/**
@@ -77,8 +79,8 @@ export default class EditorJS {
7779
/**
7880
* Default tools merged with user-provided `config.tools` (user wins by name).
7981
*/
80-
for (const tool of mergeTools(DEFAULT_TOOLS, tools)) {
81-
this.#core.use(tool);
82+
for (const [tool, options] of mergeTools(DEFAULT_TOOLS, tools)) {
83+
this.#core.use(tool, options);
8284
}
8385

8486
/**

packages/editorjs/src/mergeTools.spec.ts

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, it, expect } from '@jest/globals';
2-
import type { ToolConstructable } from '@editorjs/sdk';
2+
import type { ToolConstructable, ToolStaticOptions } from '@editorjs/sdk';
33
import { mergeTools } from './mergeTools.js';
44

55
/**
@@ -15,16 +15,16 @@ describe('mergeTools', () => {
1515
const bold = toolStub('bold');
1616
const defaults = [paragraph, bold];
1717

18-
it('returns the defaults when no user tools are provided', () => {
19-
expect(mergeTools(defaults)).toEqual([paragraph, bold]);
18+
it('returns the defaults with no options when no user tools are provided', () => {
19+
expect(mergeTools(defaults)).toEqual([[paragraph, undefined], [bold, undefined]]);
2020
});
2121

2222
it('adds a user tool registered under a new name', () => {
2323
const header = toolStub('header');
2424

2525
const result = mergeTools(defaults, { header });
2626

27-
expect(result).toContain(header);
27+
expect(result).toContainEqual([header, undefined]);
2828
expect(result).toHaveLength(defaults.length + 1);
2929
});
3030

@@ -33,17 +33,33 @@ describe('mergeTools', () => {
3333

3434
const result = mergeTools(defaults, { paragraph: customParagraph });
3535

36-
expect(result).toContain(customParagraph);
37-
expect(result).not.toContain(paragraph);
38-
expect(result.filter(tool => tool.name === 'paragraph')).toHaveLength(1);
36+
expect(result).toContainEqual([customParagraph, undefined]);
37+
expect(result.filter(([tool]) => tool.name === 'paragraph')).toHaveLength(1);
3938
expect(result).toHaveLength(defaults.length);
4039
});
4140

41+
it('attaches options to a user tool registered as a [tool, options] tuple', () => {
42+
const header = toolStub('header');
43+
const options: ToolStaticOptions = { config: { levels: [1, 2, 3] } };
44+
45+
const result = mergeTools(defaults, { header: [header, options] });
46+
47+
expect(result).toContainEqual([header, options]);
48+
});
49+
4250
it('throws when a config.tools key does not match the tool\'s static name', () => {
4351
const mismatched = toolStub('customParagraph');
4452

4553
expect(() => mergeTools(defaults, { paragraph: mismatched })).toThrow(
4654
/customParagraph/
4755
);
4856
});
57+
58+
it('throws when a config.tools key does not match a tuple-registered tool\'s static name', () => {
59+
const mismatched = toolStub('customParagraph');
60+
61+
expect(() => mergeTools(defaults, { paragraph: [mismatched, {}] })).toThrow(
62+
/customParagraph/
63+
);
64+
});
4965
});

packages/editorjs/src/mergeTools.ts

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,17 @@
1-
import type { ToolConstructable } from '@editorjs/sdk';
1+
import type { ToolConstructable, ToolStaticOptions } from '@editorjs/sdk';
2+
3+
/**
4+
* A user tool entry in `config.tools`: a bare constructor, or a
5+
* `[constructor, options]` tuple when `core.use` needs a second argument
6+
* (most commonly `{ config }`).
7+
*/
8+
export type ToolEntry = ToolConstructable | [ToolConstructable, ToolStaticOptions];
9+
10+
/**
11+
* A merged tool paired with the options it should be passed to `core.use` with,
12+
* if any.
13+
*/
14+
export type MergedTool = [ToolConstructable, ToolStaticOptions | undefined];
215

316
/**
417
* Merges user-provided tools over the default tools, keyed by name.
@@ -13,23 +26,25 @@ import type { ToolConstructable } from '@editorjs/sdk';
1326
*/
1427
export function mergeTools(
1528
defaults: ToolConstructable[],
16-
userTools?: Record<string, ToolConstructable>
17-
): ToolConstructable[] {
18-
const merged = new Map<string, ToolConstructable>();
29+
userTools?: Record<string, ToolEntry>
30+
): MergedTool[] {
31+
const merged = new Map<string, MergedTool>();
1932

2033
for (const tool of defaults) {
21-
merged.set(tool.name, tool);
34+
merged.set(tool.name, [tool, undefined]);
2235
}
2336

2437
if (userTools !== undefined) {
25-
for (const [name, tool] of Object.entries(userTools)) {
38+
for (const [name, entry] of Object.entries(userTools)) {
39+
const [tool, options] = Array.isArray(entry) ? entry : [entry, undefined];
40+
2641
if (name !== tool.name) {
2742
throw new Error(
2843
`Tool registered under key "${name}" in config.tools has a static name of "${tool.name}". The key must match the tool's name.`
2944
);
3045
}
3146

32-
merged.set(name, tool);
47+
merged.set(name, [tool, options]);
3348
}
3449
}
3550

packages/playground/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
},
1818
"dependencies": {
1919
"@editorjs/editorjs": "workspace:^",
20+
"@editorjs/header": "workspace:^",
2021
"@editorjs/model": "workspace:^",
2122
"@editorjs/sdk": "workspace:^",
2223
"vue": "^3.3.4"

packages/playground/src/App.vue

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { EditorDocument, EditorJSModel } from '@editorjs/model';
44
import EditorJS from '@editorjs/editorjs';
55
import { ref, onMounted } from 'vue';
66
import { Node } from './components';
7+
import { Header } from '@editorjs/header';
78
/**
89
* Editor document for visualizing
910
*/
@@ -58,6 +59,9 @@ onMounted(() => {
5859
editorDocument.value = (m as EditorJSModel).devModeGetDocument();
5960
},
6061
62+
tools: {
63+
header: [Header, { config: { levels: [1, 2, 3, 4, 5, 6] } }],
64+
},
6165
});
6266
6367
editor.isReady.catch((error: unknown) => console.error('Editor.js failed to initialize', error));

packages/playground/tsconfig.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@
4444
},
4545
{
4646
"path": "../ui/tsconfig.json"
47+
},
48+
{
49+
"path": "../tools/header/tsconfig.json"
4750
}
4851
]
4952
}

packages/playground/vite.config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ export default defineConfig({
99
'@': path.resolve(__dirname, './src'),
1010
},
1111
},
12+
server: {
13+
fs: {
14+
allow: [path.resolve(__dirname, '../..')],
15+
},
16+
},
1217
optimizeDeps: {
1318
exclude: [
1419
'@editorjs/ui',

0 commit comments

Comments
 (0)