import type { API as ApiMethods, I18n } from '../../../types'; import type { BlokConfig } from '../../../types/configs'; import type { ToolConfig, ToolConstructable, ToolSettings } from '../../../types/tools'; import type { API as ApiModule } from '../modules/api'; import { InternalInlineToolSettings, InternalTuneSettings } from './base'; import { BlockToolAdapter } from './block'; import { InlineToolAdapter } from './inline'; import { BlockTuneAdapter } from './tune'; type ToolConstructor = typeof InlineToolAdapter | typeof BlockToolAdapter | typeof BlockTuneAdapter; /** * Factory to construct classes to work with tools */ export class ToolsFactory { /** * Tools configuration specified by user */ private config: {[name: string]: ToolSettings & { isInternal?: boolean }}; /** * Blok API Module */ private api: ApiModule; /** * Blok configuration */ private blokConfig: BlokConfig; /** * @class * @param config - tools config * @param blokConfig - Blok config * @param api - Blok API module */ constructor( config: {[name: string]: ToolSettings & { isInternal?: boolean }}, blokConfig: BlokConfig, api: ApiModule ) { this.api = api; this.config = config; this.blokConfig = blokConfig; } /** * Shallow-merges new user configuration into a registered tool's stored config. * * The merge targets the tool's nested `config` object (the part passed to the * tool constructor) and mutates it IN PLACE. Adapters read their config lazily * and share this exact object reference, so the live adapter — and any freshly * built adapter — both observe the new values immediately. * @param name - tool name * @param config - partial tool config to merge in */ public updateConfig(name: string, config: Partial): void { const settings = this.config[name]; if (settings === undefined) { throw new Error(`Tool "${name}" is not registered.`); } // `toolbox` is a tool-level SETTING (like `class`/`shortcut`), not nested // tool config: route it to the settings level so future adapters built from // this config observe the new toolbox visibility, and keep it out of the // config object handed to tool constructors. const { toolbox, ...nestedConfig } = config; if ('toolbox' in config) { settings.toolbox = toolbox as ToolSettings['toolbox']; } // eslint-disable-next-line @typescript-eslint/no-deprecated -- Internal: mutating legacy nested config in place to keep live + future adapters in sync if (settings.config === undefined) { // eslint-disable-next-line @typescript-eslint/no-deprecated -- Internal: initialize nested config object before merging settings.config = {}; } // eslint-disable-next-line @typescript-eslint/no-deprecated -- Internal: in-place merge so the shared nested config reference reflects the update Object.assign(settings.config, nestedConfig); } /** * Returns Tool object based on it's type * @param name - tool name */ public get(name: string): InlineToolAdapter | BlockToolAdapter | BlockTuneAdapter { const { class: constructableCandidate, isInternal = false, ...config } = this.config[name]; const constructable = constructableCandidate; if (constructable === undefined) { throw new Error(`Tool "${name}" does not provide a class.`); } const Constructor = this.getConstructor(constructable); const toolApi = this.createToolApi(name); return new Constructor({ name, constructable, config, api: toolApi, isDefault: name === this.blokConfig.defaultBlock, defaultPlaceholder: this.blokConfig.placeholder, isInternal, }); } /** * Creates a tool-specific API with namespaced i18n. * * EditorJS tools expect `api.i18n.t('key')` to automatically look up * `tools.{toolName}.key`. This wrapper provides that behavior while * falling back to direct key lookup for Blok internal tools that use * fully-qualified keys like `tools.stub.error`. * * @param toolName - Name of the tool * @returns API object with tool-namespaced i18n */ private createToolApi(toolName: string): ApiMethods { const baseApi = this.api.methods; const namespace = `tools.${toolName}`; const namespacedI18n: I18n = { t: ( dictKey: string, vars?: Record ): string => { /** * Try namespaced key first for EditorJS compatibility. * External tools call t('Add row') expecting lookup of 'tools.table.Add row'. */ const namespacedKey = `${namespace}.${dictKey}`; if (baseApi.i18n.has(namespacedKey)) { return vars === undefined ? baseApi.i18n.t(namespacedKey) : baseApi.i18n.t(namespacedKey, vars); } /** * Fall back to direct key lookup for Blok internal tools. * Internal tools use fully-qualified keys like 'tools.stub.error'. */ return vars === undefined ? baseApi.i18n.t(dictKey) : baseApi.i18n.t(dictKey, vars); }, has: (dictKey: string): boolean => { const namespacedKey = `${namespace}.${dictKey}`; return baseApi.i18n.has(namespacedKey) || baseApi.i18n.has(dictKey); }, getEnglishTranslation: (key: string): string => { return baseApi.i18n.getEnglishTranslation(key); }, getLocale: (): string => { return baseApi.i18n.getLocale(); }, }; return { ...baseApi, i18n: namespacedI18n, }; } /** * Find appropriate Tool object constructor for Tool constructable * @param constructable - Tools constructable */ private getConstructor(constructable: ToolConstructable): ToolConstructor { const isInline = Boolean(Reflect.get(constructable, InternalInlineToolSettings.IsInline)); const isTune = Boolean(Reflect.get(constructable, InternalTuneSettings.IsTune)); if (isInline) { return InlineToolAdapter; } if (isTune) { return BlockTuneAdapter; } return BlockToolAdapter; } }