Skip to content

Commit 46615ca

Browse files
gohaberegneSpecc
andauthored
Introduce DI & migrate tools builders (#80)
* Introduce DI & migrate tools builders * Fixes after review * 'Fix' lint * Add unsaved files * Fix comments * Remove log & add docs * Update packages/core/src/entities/UnifiedToolConfig.ts Co-authored-by: Peter <specc.dev@gmail.com> * Fixes after review --------- Co-authored-by: Peter <specc.dev@gmail.com>
1 parent 929d996 commit 46615ca

19 files changed

+1090
-49
lines changed

packages/core/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@
2121
"@editorjs/dom": "^1.0.0",
2222
"@editorjs/dom-adapters": "workspace:^",
2323
"@editorjs/editorjs": "^2.30.5",
24+
"@editorjs/helpers": "^1.0.0",
2425
"@editorjs/model": "workspace:^",
25-
"@editorjs/sdk": "workspace:^"
26+
"@editorjs/sdk": "workspace:^",
27+
"reflect-metadata": "^0.2.2",
28+
"typedi": "^0.10.0"
2629
}
2730
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type { ToolSettings, ToolConstructable } from '@editorjs/editorjs';
2+
import type { BlockToolConstructor, InlineToolConstructor } from '@editorjs/sdk';
3+
4+
/**
5+
* Users can pass tool's config in two ways:
6+
* toolName: ToolClass
7+
* or
8+
* toolName: {
9+
* class: ToolClass,
10+
* // .. other options
11+
* }
12+
*
13+
* This interface unifies these variants to a single format
14+
*/
15+
export type UnifiedToolConfig = Record<string, Omit<ToolSettings, 'class'> & {
16+
/**
17+
* Tool constructor
18+
*/
19+
class: ToolConstructable | BlockToolConstructor | InlineToolConstructor;
20+
21+
/**
22+
* Specifies if tool is internal
23+
*
24+
* Internal tools set it to true, external tools omit it
25+
*/
26+
isInternal?: boolean;
27+
}>;

packages/core/src/entities/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './Config.js';
2+
export * from './UnifiedToolConfig.js';

packages/core/src/index.ts

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import type { ModelEvents } from '@editorjs/model';
22
import { BlockAddedEvent, EditorJSModel, EventType } from '@editorjs/model';
3+
import type { ContainerInstance } from 'typedi';
4+
import { Container } from 'typedi';
35
import { composeDataFromVersion2 } from './utils/composeDataFromVersion2.js';
46
import ToolsManager from './tools/ToolsManager.js';
57
import { BlockToolAdapter, CaretAdapter, InlineToolsAdapter } from '@editorjs/dom-adapters';
6-
import type { BlockAPI, BlockToolData, API as EditorjsApi, ToolConfig } from '@editorjs/editorjs';
8+
import type { BlockAPI, BlockToolData } from '@editorjs/editorjs';
79
import { InlineToolbar } from './ui/InlineToolbar/index.js';
810
import type { CoreConfigValidated } from './entities/Config.js';
911
import type { BlockTool, CoreConfig } from '@editorjs/sdk';
@@ -42,6 +44,11 @@ export default class Core {
4244
*/
4345
#caretAdapter: CaretAdapter;
4446

47+
/**
48+
* Inversion of Control container for dependency injections
49+
*/
50+
#iocContainer: ContainerInstance;
51+
4552
/**
4653
* Inline tool adapter is responsible for handling model formatting updates
4754
* Applies format, got from inline toolbar to the model
@@ -61,19 +68,32 @@ export default class Core {
6168
* @param config - Editor configuration
6269
*/
6370
constructor(config: CoreConfig) {
71+
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
72+
this.#iocContainer = Container.of(Math.floor(Math.random() * 1e10).toString());
73+
6474
this.validateConfig(config);
6575
this.#config = config as CoreConfigValidated;
6676

77+
this.#iocContainer.set('EditorConfig', this.#config);
78+
6779
const { blocks } = composeDataFromVersion2(config.data ?? { blocks: [] });
6880

6981
this.#model = new EditorJSModel();
82+
83+
this.#iocContainer.set(EditorJSModel, this.#model);
84+
7085
this.#model.addEventListener(EventType.Changed, (event: ModelEvents) => this.handleModelUpdate(event));
7186

72-
this.#toolsManager = new ToolsManager(this.#config.tools);
87+
this.#toolsManager = this.#iocContainer.get(ToolsManager);
88+
7389
this.#caretAdapter = new CaretAdapter(this.#config.holder, this.#model);
90+
this.#iocContainer.set(CaretAdapter, this.#caretAdapter);
91+
7492
this.#inlineToolsAdapter = new InlineToolsAdapter(this.#model, this.#caretAdapter);
93+
this.#iocContainer.set(InlineToolsAdapter, this.#inlineToolsAdapter);
7594

76-
this.#inlineToolbar = new InlineToolbar(this.#model, this.#inlineToolsAdapter, this.#toolsManager.getInlineTools(), this.#config.holder);
95+
this.#inlineToolbar = new InlineToolbar(this.#model, this.#inlineToolsAdapter, this.#toolsManager.inlineTools, this.#config.holder);
96+
this.#iocContainer.set(InlineToolbar, this.#inlineToolbar);
7797

7898
this.#model.initializeDocument({ blocks });
7999
}
@@ -160,14 +180,15 @@ export default class Core {
160180
*/
161181
data: BlockToolData<Record<string, unknown>>;
162182
}, blockToolAdapter: BlockToolAdapter): BlockTool {
163-
const tool = this.#toolsManager.resolveBlockTool(name);
164-
const block = new tool({
183+
const tool = this.#toolsManager.blockTools.get(name);
184+
185+
if (!tool) {
186+
throw new Error(`Block Tool ${name} not found`);
187+
}
188+
189+
const block = tool.create({
165190
adapter: blockToolAdapter,
166191
data: data,
167-
168-
// @todo
169-
api: {} as EditorjsApi,
170-
config: {} as ToolConfig<Record<string, unknown>>,
171192
block: {} as BlockAPI,
172193
readOnly: false,
173194
});
Lines changed: 216 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,239 @@
1-
import type { BlockToolConstructor, InlineToolsConfig } from '@editorjs/sdk';
1+
import 'reflect-metadata';
2+
import { deepMerge, isFunction, isObject, PromiseQueue } from '@editorjs/helpers';
3+
import { Inject, Service } from 'typedi';
4+
import {
5+
BlockToolFacade, BlockTuneFacade,
6+
InlineToolFacade,
7+
ToolsCollection,
8+
ToolsFactory
9+
} from './facades/index.js';
210
import { Paragraph } from './internal/block-tools/paragraph/index.js';
3-
import type { EditorConfig } from '@editorjs/editorjs';
11+
import type {
12+
EditorConfig,
13+
ToolConstructable,
14+
ToolSettings
15+
} from '@editorjs/editorjs';
416
import BoldInlineTool from './internal/inline-tools/bold/index.js';
517
import ItalicInlineTool from './internal/inline-tools/italic/index.js';
18+
import { BlockToolConstructor, InlineTool, InlineToolConstructor } from '@editorjs/sdk';
19+
import { UnifiedToolConfig } from '../entities/index.js';
620

721
/**
822
* Works with tools
23+
* @todo - validate tools configurations
24+
* @todo - merge internal tools
925
*/
26+
@Service()
1027
export default class ToolsManager {
11-
#tools: EditorConfig['tools'];
28+
/**
29+
* ToolsFactory instance
30+
*/
31+
#factory: ToolsFactory;
32+
33+
/**
34+
* Unified config with internal and internal tools
35+
*/
36+
#config: UnifiedToolConfig;
37+
38+
/**
39+
* Tools available for use
40+
*/
41+
#availableTools = new ToolsCollection();
42+
43+
/**
44+
* Tools loaded but unavailable for use
45+
*/
46+
#unavailableTools = new ToolsCollection();
47+
48+
/**
49+
* Returns available Tools
50+
*/
51+
public get available(): ToolsCollection {
52+
return this.#availableTools;
53+
}
54+
55+
/**
56+
* Returns unavailable Tools
57+
*/
58+
public get unavailable(): ToolsCollection {
59+
return this.#unavailableTools;
60+
}
61+
62+
/**
63+
* Return Tools for the Inline Toolbar
64+
*/
65+
public get inlineTools(): ToolsCollection<InlineToolFacade> {
66+
return this.available.inlineTools;
67+
}
68+
69+
/**
70+
* Return editor block tools
71+
*/
72+
public get blockTools(): ToolsCollection<BlockToolFacade> {
73+
return this.available.blockTools;
74+
}
75+
76+
/**
77+
* Return available Block Tunes
78+
* @returns - object of Inline Tool's classes
79+
*/
80+
public get blockTunes(): ToolsCollection<BlockTuneFacade> {
81+
return this.available.blockTunes;
82+
}
1283

1384
/**
14-
* @param tools - Tools configuration passed by user
85+
* Returns internal tools
1586
*/
16-
constructor(tools: EditorConfig['tools']) {
17-
this.#tools = tools;
87+
public get internal(): ToolsCollection {
88+
return this.available.internalTools;
1889
}
1990

2091
/**
21-
* Returns a block tool by its name
22-
* @param toolName - name of a tool to resolve
92+
* @param editorConfig - EditorConfig object
93+
* @param editorConfig.tools - Tools configuration passed by user
2394
*/
24-
public resolveBlockTool(toolName: string): BlockToolConstructor {
25-
switch (toolName) {
26-
case 'paragraph':
27-
return Paragraph;
28-
default:
29-
throw new Error(`Unknown tool: ${toolName}`);
95+
constructor(@Inject('EditorConfig') editorConfig: EditorConfig) {
96+
this.#config = this.#prepareConfig(editorConfig.tools ?? {});
97+
98+
this.#validateTools();
99+
100+
this.#factory = new ToolsFactory(this.#config, editorConfig, {});
101+
102+
void this.prepareTools();
103+
}
104+
105+
/**
106+
* Calls tools prepare method if it exists and adds tools to relevant collection (available or unavailable tools)
107+
* @returns Promise<void>
108+
*/
109+
public async prepareTools(): Promise<void> {
110+
const promiseQueue = new PromiseQueue();
111+
112+
Object.entries(this.#config).forEach(([toolName, config]) => {
113+
if (isFunction(config.class.prepare)) {
114+
void promiseQueue.add(async () => {
115+
try {
116+
/**
117+
* TypeScript doesn't get type guard here, so non-null assertion is used
118+
*/
119+
await config.class.prepare!({
120+
toolName: toolName,
121+
config: config,
122+
});
123+
124+
const tool = this.#factory.get(toolName);
125+
126+
if (tool.isInline()) {
127+
/**
128+
* Some Tools validation
129+
*/
130+
const inlineToolRequiredMethods = ['render'];
131+
const notImplementedMethods = inlineToolRequiredMethods.filter(method => tool.create()[method as keyof InlineTool] !== undefined);
132+
133+
if (notImplementedMethods.length) {
134+
/**
135+
* @todo implement logger
136+
*/
137+
console.log(
138+
`Incorrect Inline Tool: ${tool.name}. Some of required methods is not implemented %o`,
139+
'warn',
140+
notImplementedMethods
141+
);
142+
143+
this.#unavailableTools.set(tool.name, tool);
144+
145+
return;
146+
}
147+
}
148+
149+
this.#availableTools.set(toolName, tool);
150+
} catch (e) {
151+
console.error(`Tool ${toolName} failed to prepare`, e);
152+
153+
this.#unavailableTools.set(toolName, this.#factory.get(toolName));
154+
}
155+
});
156+
} else {
157+
this.#availableTools.set(toolName, this.#factory.get(toolName));
158+
}
159+
});
160+
161+
await promiseQueue.completed;
162+
}
163+
164+
/**
165+
* Unify tools config
166+
* @param config - user's tools config
167+
*/
168+
#prepareConfig(config: EditorConfig['tools']): UnifiedToolConfig {
169+
const unifiedConfig: UnifiedToolConfig = {} as UnifiedToolConfig;
170+
171+
/**
172+
* Save Tools settings to a map
173+
*/
174+
for (const toolName in config) {
175+
/**
176+
* If Tool is an object not a Tool's class then
177+
* save class and settings separately
178+
*/
179+
if (isObject(config)) {
180+
unifiedConfig[toolName] = config[toolName] as UnifiedToolConfig[string];
181+
} else {
182+
unifiedConfig[toolName] = { class: config[toolName] as ToolConstructable };
183+
}
30184
}
185+
186+
deepMerge(unifiedConfig, this.#internalTools);
187+
188+
return unifiedConfig;
31189
}
32190

33191
/**
34-
* Returns inline tools got from the EditorConfig tools
192+
* Validate Tools configuration objects and throw Error for user if it is invalid
35193
*/
36-
public getInlineTools(): InlineToolsConfig {
194+
#validateTools(): void {
195+
/**
196+
* Check Tools for a class containing
197+
*/
198+
for (const toolName in this.#config) {
199+
if (Object.prototype.hasOwnProperty.call(this.#config, toolName)) {
200+
// if (toolName in this.internalTools) {
201+
// return;
202+
// }
203+
204+
const tool = this.#config[toolName];
205+
206+
if (!isFunction(tool) && !isFunction((tool as ToolSettings).class)) {
207+
throw Error(
208+
`Tool «${toolName}» must be a constructor function or an object with function in the «class» property`
209+
);
210+
}
211+
}
212+
}
213+
}
214+
215+
/**
216+
* Returns internal tools
217+
* Includes Bold, Italic, Link and Paragraph
218+
*/
219+
get #internalTools(): UnifiedToolConfig {
37220
return {
38-
bold: BoldInlineTool,
39-
italic: ItalicInlineTool,
221+
paragraph: {
222+
/**
223+
* @todo solve problems with types
224+
*/
225+
class: Paragraph as unknown as BlockToolConstructor,
226+
inlineToolbar: true,
227+
isInternal: true,
228+
},
229+
bold: {
230+
class: BoldInlineTool as unknown as InlineToolConstructor,
231+
isInternal: true,
232+
},
233+
italic: {
234+
class: ItalicInlineTool as unknown as InlineToolConstructor,
235+
isInternal: true,
236+
},
40237
};
41-
};
238+
}
42239
}

0 commit comments

Comments
 (0)