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
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';
@@ -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;

@@ -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

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

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

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

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

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

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

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
*/
@@ -166,7 +210,7 @@
const plugin = this.#iocContainer.get<EditorjsPluginConstructor>(pluginType);

if (plugin !== undefined && typeof plugin === 'function') {
this.initializePlugin(plugin);
this.#initializePlugin(plugin);
}
}
}
@@ -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);

@@ -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);

59 changes: 44 additions & 15 deletions packages/core/src/tools/ToolsFactory.ts
Original file line number Diff line number Diff line change
@@ -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

GitHub Actions / lint

'any' overrides all other types in this union type

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

GitHub Actions / lint

'any' overrides all other types in this union type

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

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
@@ -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({
@@ -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
@@ -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;
21 changes: 12 additions & 9 deletions packages/core/src/tools/ToolsManager.ts
Original file line number Diff line number Diff line change
@@ -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,
@@ -33,8 +33,6 @@ import LinkInlineTool from './internal/inline-tools/link/index.js';
*/
@Service()
export default class ToolsManager {
#tools: EditorConfig['tools'];

/**
* ToolsFactory instance
*/
@@ -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 => {
@@ -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,
});

Original file line number Diff line number Diff line change
@@ -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
*/
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';
@@ -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
*/
@@ -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
*
@@ -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
Original file line number Diff line number Diff line change
@@ -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

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
13 changes: 9 additions & 4 deletions packages/sdk/src/entities/InlineTool.ts
Original file line number Diff line number Diff line change
@@ -47,7 +47,7 @@ export interface ActionsElementWithOptions {
element: HTMLElement;

/**
* Oprions of custom toolbar behaviour
* Options of custom toolbar behaviour
*/
toolbarOptions?: ToolbarOptions;
}
@@ -68,14 +68,14 @@ export interface InlineTool extends Omit<InlineToolVersion2, 'save' | 'checkStat
/**
* Function that returns the state of the tool for the current selection
* @param index - index of current text selection
* @param fragments - all fragments of the inline tool inside of the current input
* @param fragments - all fragments of the inline tool inside the current input
*/
isActive(index: TextRange, fragments: InlineFragment[]): boolean;

/**
* Returns formatting action and range for it to be applied
* @param range - current selection range
* @param fragments - all fragments of the inline tool inside of the current input
* @param fragments - all fragments of the inline tool inside the current input
*/
getFormattingOptions(range: TextRange, fragments: InlineFragment[]): ToolFormattingOptions;

@@ -101,4 +101,9 @@ export interface InlineToolsConfig extends Record<string, InlineToolConstructor>
* @todo support options: InlineToolConstructableOptions
* Inline Tool constructor class
*/
export type InlineToolConstructor = InlineToolConstructableV2 & (new () => InlineTool);
export type InlineToolConstructor = InlineToolConstructableV2 & (new () => InlineTool) & {
/**
* Property specifies the class is a Tool
*/
type: 'tool';
};