import { isFunction } from '../utils'; import type { SanitizerConfig, API as ApiMethods, ToolConfig } from '@/types'; import type { Tool, ToolConstructable, ToolSettings } from '@/types/tools'; import type { BaseToolAdapter as BaseToolAdapterInterface } from '@/types/tools/adapters/base-tool-adapter'; import type { BlockToolAdapter as BlockToolAdapterInterface } from '@/types/tools/adapters/block-tool-adapter'; import type { BlockTuneAdapter as BlockTuneAdapterInterface } from '@/types/tools/adapters/block-tune-adapter'; import type { InlineToolAdapter as InlineToolAdapterInterface } from '@/types/tools/adapters/inline-tool-adapter'; import { ToolType } from '@/types/tools/adapters/tool-type'; /** * Keys that are Blok-level settings (not passed to tool constructor) */ const BLOK_SETTINGS_KEYS = new Set([ 'class', 'inlineToolbar', 'tunes', 'shortcut', 'toolbox', 'config', 'isInternal', ]); /** * Enum of Tool options provided by user */ export enum UserSettings { /** * Shortcut for Tool */ Shortcut = 'shortcut', /** * Toolbox config for Tool */ Toolbox = 'toolbox', /** * Enabled Inline Tools for Block Tool */ EnabledInlineTools = 'inlineToolbar', /** * Enabled Block Tunes for Block Tool */ EnabledBlockTunes = 'tunes', /** * Tool configuration */ Config = 'config', } /** * Enum of Tool options provided by Tool */ export enum CommonInternalSettings { /** * Shortcut for Tool */ Shortcut = 'shortcut', /** * Sanitize configuration for Tool */ SanitizeConfig = 'sanitize', } /** * Enum of Tool options provided by Block Tool */ export enum InternalBlockToolSettings { /** * Is line breaks enabled for Tool */ IsEnabledLineBreaks = 'enableLineBreaks', /** * Tool Toolbox config */ Toolbox = 'toolbox', /** * Tool conversion config */ ConversionConfig = 'conversionConfig', /** * Is readonly mode supported for Tool */ IsReadOnlySupported = 'isReadOnlySupported', /** * Tool paste config */ PasteConfig = 'pasteConfig', /** * Tool exclusively manages its own child blocks (table cells, column_list * columns) — no user gesture may nest an arbitrary block into it */ OwnsChildren = 'ownsChildren', /** * Which block tools may be DIRECT children of this Tool's block * (`{ allow?, deny? }`). The generic form of the Table tool's cell * restrictions: any container can declare it, and core enforces it on insert, * move and in the toolbox. */ ChildTools = 'childTools', /** * Enter on this Tool's empty LAST child stays INSIDE the container instead of * escaping it (a column, a card) — per-tool policy the DOM cannot express, * since a callout renders the same nested-blocks slot yet wants the escape */ KeepsChildrenOnEnter = 'keepsChildrenOnEnter', /** * Tool stores a host-uploaded asset URL at `data.url` (image, video, audio, * file). Lets consumers discover the media-bearing tool set for orphaned-CDN * cleanup without hardcoding each tool's data shape. */ AssetKind = 'assetKind', /** * Per-tool data-migration hook: upgrades a stored block's `data` from a * legacy shape the tool once wrote into the shape it reads today. Runs at load * (block composition), before the tool is constructed. */ UpgradeData = 'upgradeData' } /** * Enum of Tool options provided by Inline Tool */ export enum InternalInlineToolSettings { /** * Flag specifies Tool is inline */ IsInline = 'isInline', } /** * Enum of Tool options provided by Block Tune */ export enum InternalTuneSettings { /** * Flag specifies Tool is Block Tune */ IsTune = 'isTune', } export type ToolOptions = Omit; type ToolPreparePayload = { toolName: string; config: ToolConfig; }; interface ConstructorOptions { name: string; constructable: ToolConstructable; config: ToolOptions; api: ApiMethods; isDefault: boolean; isInternal: boolean; defaultPlaceholder?: string | false; } /** * Base abstract class for Tools */ export abstract class BaseToolAdapter implements BaseToolAdapterInterface { /** * Tool type: Block, Inline or Tune */ public abstract type: Type; /** * Tool name specified in Blok config */ public name: string; /** * Flag show is current Tool internal (bundled with Blok core) or not */ public readonly isInternal: boolean; /** * Flag show is current Tool default or not */ public readonly isDefault: boolean; /** * Blok API for current Tool */ protected api: ApiMethods; /** * Current tool user configuration */ protected config: ToolOptions; /** * Tool's constructable blueprint */ protected constructable: ToolConstructable; /** * Default placeholder specified in Blok user configuration */ protected defaultPlaceholder?: string | false; /** * @class * @param {ConstructorOptions} options - Constructor options */ constructor({ name, constructable, config, api, isDefault, isInternal = false, defaultPlaceholder, }: ConstructorOptions) { this.api = api; this.name = name; this.constructable = constructable; this.config = config; this.isDefault = isDefault; this.isInternal = isInternal; this.defaultPlaceholder = defaultPlaceholder; } /** * Update the editor-level default placeholder. Used by the reactive * `editor.placeholder` API so blocks created AFTER the change use the new value. * @param value - new default placeholder, or false to disable it */ public setDefaultPlaceholder(value: string | false): void { this.defaultPlaceholder = value; } /** * Returns Tool user configuration. * Extracts tool-specific options from flat config and merges with nested config. */ public get settings(): ToolConfig { // eslint-disable-next-line @typescript-eslint/no-deprecated -- Internal: reading legacy config for backwards compatibility const nestedConfig = (this.config[UserSettings.Config] ?? {}); // Extract non-Blok keys as tool-specific config const flatConfig: Record = {}; for (const key of Object.keys(this.config)) { if (!BLOK_SETTINGS_KEYS.has(key)) { flatConfig[key] = this.config[key as keyof typeof this.config]; } } // Merge: nested config first, flat config overrides const config = { ...nestedConfig, ...flatConfig }; if (this.isDefault && !('placeholder' in config) && this.defaultPlaceholder) { config.placeholder = this.defaultPlaceholder; } return config; } /** * Calls Tool's reset method */ public reset(): void | Promise { if (isFunction(this.constructable.reset)) { return this.constructable.reset(); } } /** * Calls Tool's prepare method */ public prepare(): void | Promise { const prepare = this.constructable.prepare; if (!isFunction(prepare)) { return; } const payload: ToolPreparePayload = { toolName: this.name, config: this.settings, }; return (prepare as (data: ToolPreparePayload) => void | Promise).call(this.constructable, payload); } /** * Returns shortcut for Tool (internal or specified by user) */ public get shortcut(): string | undefined { const toolShortcut = this.constructable[CommonInternalSettings.Shortcut]; const userShortcut = this.config[UserSettings.Shortcut]; return userShortcut || toolShortcut; } /** * Returns Tool's sanitizer configuration */ public get sanitizeConfig(): SanitizerConfig { return this.constructable[CommonInternalSettings.SanitizeConfig] || {}; } /** * Returns true if Tools is inline */ public isInline(): this is InlineToolAdapterInterface { return this.type === ToolType.Inline; } /** * Returns true if Tools is block */ public isBlock(): this is BlockToolAdapterInterface { return this.type === ToolType.Block; } /** * Returns true if Tools is tune */ public isTune(): this is BlockTuneAdapterInterface { return this.type === ToolType.Tune; } /** * Constructs new Tool instance from constructable blueprint * @param args */ public abstract create(...args: unknown[]): ToolClass; }