Skip to content

[WIP] Implement Tools loading via .use() method #117

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 68 additions & 24 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import { CollaborationManager } from '@editorjs/collaboration-manager';
import type { ToolSettings } from '@editorjs/editorjs/types/tools/index';
import { type DocumentId, EditorJSModel, EventType } from '@editorjs/model';
import type { ContainerInstance } from 'typedi';
import { Container } from 'typedi';
import { CoreEventType, EventBus, UiComponentType } from '@editorjs/sdk';
import {
type BlockToolConstructor,
CoreEventType,
EventBus,
type InlineToolConstructor,
UiComponentType
} from '@editorjs/sdk';
import { Paragraph } from './tools/internal/block-tools/paragraph/index';
import type { ExtendedToolSettings } from './tools/ToolsFactory';
import { composeDataFromVersion2 } from './utils/composeDataFromVersion2.js';
import ToolsManager from './tools/ToolsManager.js';
import { CaretAdapter, FormattingAdapter } from '@editorjs/dom-adapters';
Expand Down Expand Up @@ -69,7 +78,7 @@
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
this.#iocContainer = Container.of(Math.floor(Math.random() * 1e10).toString());

this.validateConfig(config);
this.#validateConfig(config);

this.#config = config as CoreConfigValidated;

Expand Down Expand Up @@ -119,44 +128,79 @@
eventBus.addEventListener(`core:${CoreEventType.Redo}`, () => {
this.#collaborationManager.redo();
});

// @ts-expect-error - weird TS error, will resolve later
this.use(Paragraph);
}

/**
* Initialize and injects Plugin into the container
* Injects Tool constructor and it's config into the container
* @param tool
* @param config
*/
public use(tool: BlockToolConstructor | InlineToolConstructor, config?: Omit<ToolSettings, 'class'>): Core;

Check failure on line 141 in packages/core/src/index.ts

View workflow job for this annotation

GitHub Actions / lint

'InlineToolConstructor' is an 'error' type that acts as 'any' and overrides all other types in this union type

Check failure on line 141 in packages/core/src/index.ts

View workflow job for this annotation

GitHub Actions / lint

'BlockToolConstructor' is an 'error' type that acts as 'any' and overrides all other types in this union type
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tools can have "type" ("block", "inline" (ex isInline(), "tune"), so they can implement EditorjsPluginConstructor

/**
* Injects Plugin into the container to initialize on Editor's init
* @param plugin - allows to pass any implementation of editor plugins
*/
public use(plugin: EditorjsPluginConstructor): Core {
const pluginType = plugin.type;

this.#iocContainer.set(pluginType, plugin);
public use(plugin: EditorjsPluginConstructor): Core;
/**
* Overloaded method to register Editor.js Plugins/Tools/etc
* @param pluginOrTool - entity to register
* @param toolConfig - entity configuration
*/
public use(
pluginOrTool: BlockToolConstructor | InlineToolConstructor | EditorjsPluginConstructor,

Check failure on line 153 in packages/core/src/index.ts

View workflow job for this annotation

GitHub Actions / lint

'EditorjsPluginConstructor' is an 'error' type that acts as 'any' and overrides all other types in this union type

Check failure on line 153 in packages/core/src/index.ts

View workflow job for this annotation

GitHub Actions / lint

'InlineToolConstructor' is an 'error' type that acts as 'any' and overrides all other types in this union type

Check failure on line 153 in packages/core/src/index.ts

View workflow job for this annotation

GitHub Actions / lint

'BlockToolConstructor' is an 'error' type that acts as 'any' and overrides all other types in this union type
toolConfig?: Omit<ToolSettings, 'class'>
): Core {
const pluginType = pluginOrTool.type;

switch (pluginType) {
case 'tool':
this.#iocContainer.set({
id: pluginType,
multiple: true,
value: [pluginOrTool, toolConfig],
});
break;
default:
this.#iocContainer.set(pluginType, pluginOrTool);
}

return this;
}

/**
* Initializes the core
*/
public initialize(): void {
const { blocks } = composeDataFromVersion2(this.#config.data ?? { blocks: [] });
public async initialize(): Promise<void> {
try {
const { blocks } = composeDataFromVersion2(this.#config.data ?? { blocks: [] });

this.initializePlugins();
this.#initializePlugins();

this.#toolsManager.prepareTools()
.then(() => {
this.#model.initializeDocument({ blocks });
})
.then(() => {
this.#collaborationManager.connect();
})
.catch((error) => {
console.error('Editor.js initialization failed', error);
});
await this.#initializeTools();

this.#model.initializeDocument({ blocks });
this.#collaborationManager.connect();
} catch (error) {
console.error('Editor.js initialization failed', error);
}
}

/**
* Initalizes loaded tools
*/
async #initializeTools(): Promise<void> {
const tools = this.#iocContainer.getMany<[ BlockToolConstructor | InlineToolConstructor, ExtendedToolSettings]>('tool');

Check failure on line 195 in packages/core/src/index.ts

View workflow job for this annotation

GitHub Actions / lint

'InlineToolConstructor' is an 'error' type that acts as 'any' and overrides all other types in this union type

Check failure on line 195 in packages/core/src/index.ts

View workflow job for this annotation

GitHub Actions / lint

'BlockToolConstructor' is an 'error' type that acts as 'any' and overrides all other types in this union type

return this.#toolsManager.prepareTools(tools);
}

/**
* Initialize all registered UI plugins
*/
private initializePlugins(): void {
#initializePlugins(): void {
/**
* Get all registered plugin types from the container
*/
Expand All @@ -166,7 +210,7 @@
const plugin = this.#iocContainer.get<EditorjsPluginConstructor>(pluginType);

if (plugin !== undefined && typeof plugin === 'function') {
this.initializePlugin(plugin);
this.#initializePlugin(plugin);
}
}
}
Expand All @@ -175,7 +219,7 @@
* Create instance of plugin
* @param plugin - Plugin constructor to initialize
*/
private initializePlugin(plugin: EditorjsPluginConstructor): void {
#initializePlugin(plugin: EditorjsPluginConstructor): void {
const eventBus = this.#iocContainer.get(EventBus);
const api = this.#iocContainer.get(EditorAPI);

Expand All @@ -190,7 +234,7 @@
* Validate configuration
* @param config - Editor configuration
*/
private validateConfig(config: CoreConfig): void {
#validateConfig(config: CoreConfig): void {
if (config.holder === undefined) {
const holder = document.getElementById(DEFAULT_HOLDER_ID);

Expand Down
59 changes: 44 additions & 15 deletions packages/core/src/tools/ToolsFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,31 +11,43 @@
ToolConstructable,
EditorConfig,
InlineToolConstructable,
BlockTuneConstructable
BlockTuneConstructable, ToolSettings
} from '@editorjs/editorjs';

// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
type ToolConstructor = typeof InlineToolFacade | typeof BlockToolFacade | typeof BlockTuneFacade;

Check failure on line 17 in packages/core/src/tools/ToolsFactory.ts

View workflow job for this annotation

GitHub Actions / lint

'any' overrides all other types in this union type

Check failure on line 17 in packages/core/src/tools/ToolsFactory.ts

View workflow job for this annotation

GitHub Actions / lint

'any' overrides all other types in this union type

Check failure on line 17 in packages/core/src/tools/ToolsFactory.ts

View workflow job for this annotation

GitHub Actions / lint

'any' overrides all other types in this union type

export type ExtendedToolSettings = ToolSettings & {
/**
* Flag shows if a Tool is an internal tool
* @todo do we need this distinction any more?
*/
isInternal: boolean;
};

/**
* Factory to construct classes to work with tools
*/
export class ToolsFactory {
/**
* Tools configuration specified by user
*/
private config: UnifiedToolConfig;
#config: UnifiedToolConfig;

/**
* EditorJS API Module
*/

private api: EditorAPI;
#api: EditorAPI;

/**
* EditorJS configuration
*/
private editorConfig: EditorConfig;
#editorConfig: EditorConfig;

/**
* Map of tool settings
*/
#toolsSettings = new Map<string, ExtendedToolSettings>();

/**
* ToolsFactory
Expand All @@ -49,20 +61,38 @@
// eslint-disable-next-line @typescript-eslint/no-explicit-any
api: any
) {
this.api = api;
this.config = config;
this.editorConfig = editorConfig;
this.#api = api;
this.#config = config;
this.#editorConfig = editorConfig;
}

/**
* Register tools in the factory
* @param tools - tools to register in the factory
*/
public setTools(tools: [InlineToolConstructor | BlockToolConstructor, ExtendedToolSettings][]): void {
tools.forEach(([tool, settings]) => {
this.#toolsSettings.set(tool.name, {
...settings,
class: tool,
});
});
}

/**
* Returns Tool object based on it's type
* @param name - tool name
*/
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
public get(name: string): InlineToolFacade | BlockToolFacade | BlockTuneFacade {
const { class: constructable, isInternal = false, ...config } = this.config[name];
const toolSettings = this.#toolsSettings.get(name);

if (!toolSettings) {
throw new Error(`Tool ${name} is not registered`);
}

const { class: constructable, isInternal = false, ...config } = toolSettings;

const Constructor = this.getConstructor(constructable);
const Constructor = this.#getConstructor(constructable!);
// const isTune = constructable[InternalTuneSettings.IsTune];

return new Constructor({
Expand All @@ -71,8 +101,8 @@
config,
api: {},
// api: this.api.getMethodsForTool(name, isTune),
isDefault: name === this.editorConfig.defaultBlock,
defaultPlaceholder: this.editorConfig.placeholder,
isDefault: name === this.#editorConfig.defaultBlock,
defaultPlaceholder: this.#editorConfig.placeholder,
isInternal,
/**
* @todo implement api.getMethodsForTool
Expand All @@ -85,8 +115,7 @@
* Find appropriate Tool object constructor for Tool constructable
* @param constructable - Tools constructable
*/
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
private getConstructor(constructable: ToolConstructable | BlockToolConstructor | InlineToolConstructor): ToolConstructor {
#getConstructor(constructable: ToolConstructable | BlockToolConstructor | InlineToolConstructor): ToolConstructor {
switch (true) {
case (constructable as InlineToolConstructable)[InternalInlineToolSettings.IsInline]:
return InlineToolFacade;
Expand Down
21 changes: 12 additions & 9 deletions packages/core/src/tools/ToolsManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type {
import 'reflect-metadata';
import { deepMerge, isFunction, isObject, PromiseQueue } from '@editorjs/helpers';
import { Inject, Service } from 'typedi';
import { ToolsFactory } from './ToolsFactory.js';
import { type ExtendedToolSettings, ToolsFactory } from './ToolsFactory.js';
import { Paragraph } from './internal/block-tools/paragraph/index.js';
import type {
EditorConfig,
Expand Down Expand Up @@ -33,8 +33,6 @@ import LinkInlineTool from './internal/inline-tools/link/index.js';
*/
@Service()
export default class ToolsManager {
#tools: EditorConfig['tools'];

/**
* ToolsFactory instance
*/
Expand Down Expand Up @@ -122,9 +120,9 @@ export default class ToolsManager {

/**
* Calls tools prepare method if it exists and adds tools to relevant collection (available or unavailable tools)
* @returns Promise<void>
* @param tools - tools to prepare and their settings
*/
public async prepareTools(): Promise<void> {
public async prepareTools(tools: [InlineToolConstructor | BlockToolConstructor, ExtendedToolSettings][]): Promise<void> {
const promiseQueue = new PromiseQueue();

const setToAvailableToolsCollection = (toolName: string, tool: ToolFacadeClass): void => {
Expand All @@ -135,15 +133,20 @@ export default class ToolsManager {
}));
};

Object.entries(this.#config).forEach(([toolName, config]) => {
if (isFunction(config.class.prepare)) {
this.#factory.setTools(tools);

tools.forEach(([toolConstructor, config]) => {
const toolName = toolConstructor.name;

// eslint-disable-next-line @typescript-eslint/unbound-method
if (isFunction(toolConstructor.prepare)) {
void promiseQueue.add(async () => {
try {
/**
* TypeScript doesn't get type guard here, so non-null assertion is used
*/
await config.class.prepare!({
toolName: toolName,
await toolConstructor.prepare!({
toolName,
config: config,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ export type ParagraphConfig = ToolConfig<{
* Base text block tool
*/
export class Paragraph implements BlockTool<ParagraphData, ParagraphConfig> {
public static type = 'tool';

public static name = 'paragraph';

/**
* Adapter for linking block data with the DOM
*/
Expand Down
24 changes: 20 additions & 4 deletions packages/sdk/src/entities/BlockTool.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import type { BlockTool as BlockToolVersion2, BlockToolConstructable as BlockToolConstructableV2, ToolConfig } from '@editorjs/editorjs';
import type {
BlockTool as BlockToolVersion2,
BlockToolConstructable as BlockToolConstructableV2,
ToolConfig
} from '@editorjs/editorjs';
import type { BlockToolConstructorOptions as BlockToolConstructorOptionsVersion2 } from '@editorjs/editorjs';
import type { ValueSerialized } from '@editorjs/model';
import type { BlockToolAdapter } from './BlockToolAdapter.js';
Expand All @@ -11,7 +15,6 @@ export interface BlockToolConstructorOptions<
* Data structure describing the tool's input/output data
*/
Data extends BlockToolData = BlockToolData,

/**
* User-end configuration for the tool
*/
Expand Down Expand Up @@ -46,7 +49,6 @@ export type BlockTool<
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any
Data extends BlockToolData = any,

/**
* User-end configuration for the tool
*
Expand All @@ -59,7 +61,21 @@ export type BlockTool<
/**
* Block Tool constructor class
*/
export type BlockToolConstructor = BlockToolConstructableV2 & (new (options: BlockToolConstructorOptions) => BlockTool);
export type BlockToolConstructor<
/**
* Data structure describing the tool's input/output data
*/
Data extends BlockToolData = BlockToolData,
/**
* User-end configuration for the tool
*/
Config extends ToolConfig = ToolConfig
> = BlockToolConstructableV2 & (new (options: BlockToolConstructorOptions<Data, Config>) => BlockTool) & {
/**
* Property specifies that the class is a Tool
*/
type: 'tool';
};

/**
* Data structure describing the tool's input/output data
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@
/**
* Updated caret index
*/
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
readonly index: Index | null;

Check failure on line 14 in packages/sdk/src/entities/EventBus/events/core/SelectionChangedCoreEvent.ts

View workflow job for this annotation

GitHub Actions / lint

'Index' is an 'error' type that acts as 'any' and overrides all other types in this union type

/**
* Inline tools available for the current selection
Expand Down
Loading
Loading