import * as i0 from '@angular/core'; import { AfterViewInit, DoCheck, EventEmitter, OnDestroy, InjectionToken, EnvironmentProviders, Type, Signal, ElementRef } from '@angular/core'; import { ControlValueAccessor } from '@angular/forms'; import { BlokConfig, EditorWidth, Blok, OutputData, API, BlockMutationEvent, ResolvedTheme, BlocksRenderedPayload, BlockRenderedPayload, OutputBlockData, ThemeMode, LooseOutputData, BlockToolData, BlockTuneData, MarkdownImportConfig, BlockOrigin, BlockToolConstructable, ToolboxConfig, BlockAPI, BlockToolConstructorOptions } from '@bloklabs/core'; /** * Configuration for the Angular adapter. Same as `BlokConfig` but without * `holder` (the adapter owns the host element), plus the reactive `width` prop. * Mirrors the React adapter's `UseBlokConfig`. */ type BlokAngularConfig = Omit & { /** Editor content width mode. Synced reactively after mount via `editor.width.set()`. */ width?: EditorWidth; }; /** * The blessed all-in-one Angular component for embedding Blok (mirrors React's * `BlokEditor`). Delegates instance lifecycle to an internal `BlokContentDirective` * and layers the typed reactive input/output API on top. * * Reactive inputs (`readOnly`, `hideToolbar`, `toolbarPosition`, * `inlineToolbar`, `theme`, `width`, `placeholder`, `autofocus`, `styleTokens`, * `i18n`, `data`) are synced in place after mount via effects — `hideToolbar` * through `editor.toolbar.setHidden()`, `toolbarPosition` through * `editor.toolbar.setPosition()`, `inlineToolbar` through * `editor.tools.setInlineToolbar()` (content-compared), * `styleTokens` through `editor.tokens.set()` (replace semantics), * `i18n` through `editor.i18n.update()` (deep-equal–deduped; seeded at * construction so the locale resolves during boot) and `data` * through `editor.render()` (content-deduped via `equalsOutputData`); everything * else seeds construction. * * Implementation note: classic `@Input()`/`@ViewChild()` decorators are used * (not signal `input()`/`viewChild()`) for JIT compatibility — see * `BlokContentDirective`. Each reactive input is backed by an internal `signal` * so effect-based syncing still works; the public template API is unchanged. */ declare class BlokEditorComponent implements AfterViewInit, DoCheck, ControlValueAccessor { private readonly ngZone; /** App-wide defaults from `provideBlok()`; merged UNDER per-instance inputs. */ private readonly defaults; private contentQuery?; /** Signal bridge for the @ViewChild directive so effects react to its arrival. */ private readonly content; /** * Output half of two-way `[(data)]`. Emits the editor's serialized content on * every change. Wiring the core `onSave` callback is gated on this being * observed, since its mere presence makes the core serialize on every batch. */ /** Emits the live Blok instance once ready, after `instance()` is populated. */ readonly ready: EventEmitter; readonly dataChange: EventEmitter; /** Fires with the full serialized content on every change (notification half). */ readonly save: EventEmitter; /** Raw block mutation channel (core `onChange`). */ readonly change: EventEmitter<{ api: API; event: BlockMutationEvent | BlockMutationEvent[]; }>; /** Fires after the editor finishes (re-)rendering (core `onAfterRender`). */ readonly afterRender: EventEmitter; /** Fires with the resolved theme whenever it changes (core `onThemeChange`). */ readonly themeChange: EventEmitter; /** Fires after a batch render completes (core `blocks:rendered` event). */ readonly blocksRendered: EventEmitter; /** Fires for each block rendered into the DOM (core `block:rendered` event). */ readonly blockRendered: EventEmitter; /** * Transform hook applied to blocks before render (core `onBeforeRender`). Must * return the (possibly modified) block list. An input, not an output, because * it returns a value. */ onBeforeRender?: (blocks: OutputBlockData[]) => OutputBlockData[]; /** * Transform hook applied to pasted HTML (core `onBeforePaste`). Returns the * (possibly modified) html, or null to drop the paste. */ onBeforePaste?: (html: string) => string | null; /** * Error channel (core `onError`). Fires with the raised error whenever an * editor operation — currently serialization — fails instead of only logging. */ onError?: BlokConfig['onError']; private readonly readOnly$; set readOnly(value: BlokConfig['readOnly'] | undefined); private readonly hideToolbar$; set hideToolbar(value: boolean | undefined); private readonly toolbarPosition$; set toolbarPosition(value: BlokConfig['toolbarPosition'] | undefined); private readonly inlineToolbar$; set inlineToolbar(value: boolean | string[] | undefined); private readonly theme$; set theme(value: ThemeMode | undefined); private readonly width$; set width(value: EditorWidth | undefined); private readonly placeholder$; set placeholder(value: string | false | undefined); /** * Theme tokens. Construction-only config forced hosts with a live light/dark * toggle to recreate the editor or hand-write the global stylesheet Blok * already injects; this drives the runtime `tokens` API instead. */ private readonly styleTokens$; set styleTokens(value: Record | undefined); /** * Locale, host message overrides and text direction. `config.i18n` was * consumed once at boot, so a host driving a language switcher had to * recreate the editor (losing caret, focus and undo stack) to relabel the * UI; this drives the runtime `i18n.update` API instead. `defaultLocale` is * not forwarded — it only affects the INITIAL locale resolution. */ private readonly i18n$; set i18n(value: BlokConfig['i18n'] | undefined); private readonly autofocus$; set autofocus(value: boolean | undefined); private readonly data$; set data(value: OutputData | LooseOutputData | null | undefined); tools?: BlokConfig['tools']; /** Host-supplied per-type block migrations applied at load (merged across layers). */ migrations?: BlokConfig['migrations']; /** * Escape hatch: a full config object for keys without a dedicated input * (sanitizer, minHeight, …). Layered between provideBlok * defaults and the discrete inputs. */ config?: Partial; /** Changing this input's identity destroys and recreates the editor (≙ React `deps`). */ recreateKey: unknown; /** * Content the editor currently reflects. Set when seeded, when the data effect * renders, when the editor emits its own output (`coreOnSave`) and when the * imperative `render()` facade lands. A controlled `data` echo that * content-equals this baseline is a no-op, so it won't clobber the caret. * Renders are serialized via `renderChain`. `undefined` means "nothing recorded * yet" — never an empty document. */ private lastRenderedData?; /** Last token set pushed through `tokens.set`, for deep-equal deduping. */ private appliedTokens?; /** Last i18n config pushed through `i18n.update`, for deep-equal deduping. */ private appliedI18n?; /** Editor instance the i18n baseline above belongs to. */ private i18nAppliedFor; /** Last value pushed through `tools.setInlineToolbar`, for content-compare deduping. */ private appliedInlineToolbar?; private seededEditor; private renderChain; /** Registered by `ControlValueAccessor.registerOnChange` (Angular forms). */ private cvaOnChange?; private cvaOnTouched?; /** * Core `onSave` wrapper. Records the editor's own serialized output as the * rendered baseline BEFORE notifying Angular, so a controlled consumer echoing * it straight back into `data` deep-equals the baseline and is deduped to a * no-op (no redundant render, no caret reset). Re-enters the Angular zone since * the editor runs outside it. * * Falls back to an escape-hatch `[config]` callback when nothing Angular-side * consumes the save — the baseline is recorded either way, so the controlled * `data` dedupe works on both paths. */ private readonly coreOnSave; /** * Stable wrappers for the remaining live callbacks. Each resolves its target * at call time (an observed output, else the `[config]` escape hatch), so a * pushed handler never goes stale and only PRESENCE has to be synced. */ private readonly coreOnChange; private readonly coreOnAfterRender; private readonly coreOnBeforeRender; private readonly coreOnEnter; private readonly coreOnSubmit; /** provideBlok defaults merged under the `[config]` escape hatch. */ private escapeHatchConfig; /** True when an output, a two-way binding or Angular forms consumes `onSave`. */ private hasSaveConsumer; /** * The live callbacks this component currently wires into core, keyed by * handler name — `undefined` for the ones nothing consumes. * * PRESENCE is the semantics in core (an `onSubmit` turns Enter into * serialize-and-submit, an `onSave` arms the change-observation pipeline), so * an unconsumed callback must stay absent. One source of truth for both the * construction config and the runtime sync in `ngDoCheck`, so the two cannot * drift. * @returns the wrappers to install, `undefined` where nothing is wired */ private liveHandlers; /** Handler presence installed on the live editor (the sync dedupe baseline). */ private appliedHandlers; /** Live Blok instance, or null until `isReady` resolves / after destroy. */ readonly instance: i0.Signal; /** Guards `ready` to one emission per editor instance. */ private lastReadyInstance; /** * Construction config handed to the directive. Seeds the editor's initial * values; the directive only reads it once (at construction), so post-mount * changes to construction-only inputs are no-ops by design. */ buildConfig(): Partial; ngAfterViewInit(): void; /** * Reactive callback presence. * * Callback wiring was decided once, at construction, and in core the presence * of a handler IS the semantics: an `onSubmit` makes Enter serialize-and-submit * instead of splitting the block, an `onSave` arms the whole change-observation * pipeline. A `[config]` swap, an `*ngIf`-gated `(save)` output or a * `registerOnChange` from Angular forms arriving after mount therefore needed a * `recreateKey` bump — destroying the editor and losing caret and undo history. * * Diff the wired handlers against what is installed and push genuine flips * through the runtime `handlers.set` API, writing `undefined` for a handler * that lost its consumer so the change stays reversible. `ngDoCheck` (not an * `effect`) because `EventEmitter.observed` and the plain `@Input` transforms * are not signals; the comparison is six identity checks against stable * wrappers, so an unchanged cycle pushes nothing. */ ngDoCheck(): void; /** Serialize the current content. Resolves undefined until the editor is ready. */ save$(): Promise | undefined; /** Move the caret into the editor. */ focus(atEnd?: boolean): void; /** * Replace the editor content. Resolves undefined until the editor is ready. * * Safe to mix with a controlled `[data]` input: the rendered document becomes * the content baseline once the call lands, so a later `[data]` change back to * the previous document still re-renders instead of being deduped against a * baseline the editor no longer reflects. Recorded only on the resolved path — * a failed render leaves the editor on its previous content. * @param data - the document to render */ render(data: OutputData | LooseOutputData): Promise | undefined; constructor(); /** Seeds/renders an externally-set form value through the same dedup machinery. */ writeValue(value: OutputData | LooseOutputData | null): void; registerOnChange(fn: (data: OutputData) => void): void; registerOnTouched(fn: () => void): void; /** Form disabled state maps to the editor's read-only mode (via the readOnly effect). */ setDisabledState(isDisabled: boolean): void; static ɵfac: i0.ɵɵFactoryDeclaration; static ɵcmp: i0.ɵɵComponentDeclaration; } /** * Escape-hatch directive and lifecycle engine for Blok (mirrors React's * `useBlok` + `BlokContent`). Constructs a Blok instance into its own host * element and tears it down on destroy. * * The editor renders directly into the directive's host element — so the host * IS the holder. This sidesteps the fact that Blok core exposes no public * `holder` accessor: the adapter never has to read it back, it owns it. * * `BlokEditorComponent` applies this directive to its internal `
` and reads * the instance back; consumers can also use it directly for full control: * `
`. * * Implementation note: classic `@Input()`/`@Output()` are used (not signal * `input()`/`output()`) because the repo's Vitest+Analog harness compiles the * adapter via JIT, which does not register signal-based members. `instance` * remains a plain `signal` (runtime API, JIT-safe) for reactive consumption. */ declare class BlokContentDirective implements OnDestroy { private readonly host; private readonly ngZone; private readonly isBrowser; /** * App-wide defaults from `provideBlok()`, merged UNDER the bound `[config]` so * the escape-hatch path honors them just like `` (which also * merges, idempotently). Mirrors React's `useBlok` merging context defaults. */ private readonly defaults; private readonly envInjector; private readonly appRef; private readonly errorHandler; /** Construction-time Blok config (everything except `holder`, which the directive owns). */ config: Partial; /** Changing this input's identity (after the first build) destroys + recreates the editor. */ set recreateKey(value: unknown); /** Emits the live Blok instance once it is ready, after `instance` is populated. */ readonly ready: EventEmitter; /** The live Blok instance, or null before `isReady` resolves / after destroy. */ readonly instance: i0.WritableSignal; /** The editor created by the most recent construction; used as the staleness key. */ private current; private destroyed; private built; private currentKey; /** The portal registry for the current editor (Angular-block mounting). */ private registry; constructor(); /** Construct a Blok into the host element and publish it once ready. */ private build; ngOnDestroy(): void; static ɵfac: i0.ɵɵFactoryDeclaration; static ɵdir: i0.ɵɵDirectiveDeclaration; } /** * DI token holding app-wide Blok defaults (shared tools registry, default theme, * i18n). Merged under per-instance inputs by `BlokEditorComponent`. */ declare const BLOK_DEFAULT_CONFIG: InjectionToken>; /** * Standalone provider registering app-wide Blok defaults. * * @example * ```ts * bootstrapApplication(AppComponent, { * providers: [provideBlok({ theme: 'dark', tools: sharedTools })], * }); * ``` */ declare function provideBlok(defaults: Partial): EnvironmentProviders; /** * Where to place the caret within a block. `position` selects the input * (`'start'`/`'end'`/`'default'`) and `offset` is the character offset within it * — the same shape core's `caret.setToBlock` accepts. */ interface CaretTarget { position?: 'start' | 'end' | 'default'; offset?: number; } /** * A plain, serializable view of one block in the tree. * * Snapshot-volatile: every read allocates a fresh `BlockNode`, and `contentIds` * is DERIVED per read from the children that currently name this block as parent * (it is not a stored field). Read a node in render and re-read after a change — * don't stash one in a `useMemo`/`useEffect` dependency array expecting stable * identity; depend on the `id` instead. */ interface BlockNode { id: string; type: string; parentId: string | null; contentIds: readonly string[]; } /** Where to place a block among its siblings. */ type InsertPosition = 'start' | 'end' | { before: string; } | { after: string; }; interface InsertSpec { type?: string; data?: BlockToolData; parentId?: string | null; position?: InsertPosition; /** * Move the caret into the new block. Defaults to `false`: a programmatic * insert from React must not steal focus from wherever the user is typing. * Set `true` for an explicit "add a block and start editing it" flow. */ focus?: boolean; /** * Replace the block at the resolved slot instead of inserting a new one — a * programmatic "turn into". Combine with a `position` that targets the block * to replace, e.g. `{ position: { before: id }, replace: true }`. */ replace?: boolean; /** * Explicit id for the new block (generated when omitted). Passing a stable id * makes the insert idempotent: if a block with this id already exists the * existing node is returned and nothing is inserted ("insert if absent"), * so an effect that re-runs won't create duplicates. */ id?: string; /** Block tune data to apply at creation, keyed by tune name. */ tunes?: { [name: string]: BlockTuneData; }; /** * Place the caret inside the newly-created block at a specific position/offset * (e.g. `{ offset: 3 }`). Implies focus. Applied ONLY when a block is actually * created — an insert-if-absent hit (existing id) does not move the caret. * Use this instead of the boolean `focus` when you need a specific offset. */ caret?: CaretTarget; } /** * One node of a pre-built nested subtree for {@link UseBlocksApi.insertTree}. * * Each node maps to one block; `children` are inserted nested under it (their * `parentId` set to this node's id) in array order, recursively. Placement * options (`parentId`/`position`) are ROOT-ONLY — they position the whole * subtree among existing blocks and are ignored on nested children, whose parent * is always their enclosing node. */ interface TreeInsertSpec { type?: string; data?: BlockToolData; tunes?: { [name: string]: BlockTuneData; }; /** * Explicit id for this node (generated when omitted). Unlike `insert`, this is * NOT insert-if-absent: a tree insert always creates fresh blocks. A colliding * id — one that already exists in the document, or is reused by another node * in the same spec — is REJECTED up front: nothing is inserted and `insertTree` * returns `null` (a duplicate id would corrupt every id-keyed lookup). */ id?: string; /** Direct children, inserted nested under this node, in array order. */ children?: TreeInsertSpec[]; /** Root-only: where to place the whole subtree. Ignored on nested children. */ parentId?: string | null; /** Root-only: slot among siblings of `parentId`. Ignored on nested children. */ position?: InsertPosition; } /** * Where to move an existing block. * * `before`/`after` are POSITION targets, not parent assignments: the block is * relocated to that flat slot and — because Blok keeps the flat array as the * canonical document order — ADOPTS the parent of wherever it lands. Moving a * nested block to `{ after: someRootBlock }` therefore unnests it to root, and * moving a root block in among a container's children nests it. Use * `nest`/`unnest` when you want to change the parent without choosing a sibling * slot. `toIndex` is an absolute flat index (clamped into range). */ type MoveTarget = { before: string; } | { after: string; } | { toIndex: number; }; interface UseBlocksApi { getById(id: string): BlockNode | null; getChildren(parentId: string | null): BlockNode[]; /** * Insert one block; returns the created node or null when rejected (unknown * tool type, dangling `parentId`, or a `replace` whose target is missing). An * explicit `id` that already exists is insert-if-absent (returns the existing * node, creates nothing). Atomic — one undo step. The returned node is * {@link BlockNode}-volatile; read it now, don't put it in a dep array. */ insert(spec?: InsertSpec): BlockNode | null; /** * Insert several blocks atomically, in array order, as ONE undo step. Each * spec is a full {@link InsertSpec} (own type/data/parentId/position), routed * through the same single-`insert` path, so per-spec parent assertion and * positioning still apply. Specs that fail to insert (e.g. a dangling * parentId, or a replace whose target is missing) are dropped; the returned * array holds only the successfully created nodes. An empty input is a no-op * (returns `[]`, opens no transaction). Like `insert`, the returned nodes are * fresh-snapshot volatile — read them now, don't stash them in dep arrays. */ insertMany(specs: InsertSpec[]): BlockNode[]; /** * Insert a pre-built NESTED subtree in ONE atomic operation (one undo step). * Each {@link TreeInsertSpec} node becomes a block; its `children` are inserted * nested under it (recursively, in array order) so the whole hierarchy lands in * a single call — no follow-up `nest` round-trips. Delegates to core's * tree-aware `blocks.insertMany`, which composes the flat DFS pre-order array * honoring each node's `parent`/`content` links. * * Placement is root-only: the root node's `parentId`/`position` position the * whole subtree among existing blocks (default: appended at the document end); * nested children ignore those fields (their parent is their enclosing node). A * dangling root `parentId` is rejected — nothing is inserted and `null` is * returned (mirrors {@link insert}). Returns the root {@link BlockNode}, which * is fresh-snapshot volatile — read it now, don't stash it in a dep array. */ insertTree(spec: TreeInsertSpec): BlockNode | null; /** * Convert a Markdown string to blocks and insert them ADDITIVELY at a * position, WITHOUT clearing the document (unlike core's `importMarkdown` / * `renderFromHTML`, which replace the whole document). This is the React * "paste markdown → blocks appear" path. * * Async: the markdown converter is lazy-loaded (kept out of the main bundle), * so this is the ONE async creator in the API — `await` the returned promise. * The whole batch is inserted as a single atomic undo step. * * `position` (default `'end'`) places the converted run among `parentId`'s * children (or root siblings when `parentId` is omitted/null), reusing the * same `start`/`end`/`before`/`after` semantics as {@link insert}. * * `parentId` (default `null` = root) nests the import: every TOP-LEVEL * converted block (one the converter left un-parented) is reparented under * `parentId`, while blocks the markdown nested internally (e.g. table-cell * children) keep their intra-import parent. A dangling `parentId` is a no-op * (returns `[]`, opens no transaction), matching {@link insert}. * * `config` (optional {@link MarkdownImportConfig}) is forwarded to the * converter so custom-tool consumers can map markdown nodes into their tools * (`toolMap`/`onUnknownNode`), toggle GFM, or add micromark/mdast extensions. * * Returns ALL created {@link BlockNode}s in document order — including any the * markdown nested internally (e.g. a table's cell children), not just the * top-level run (this differs from {@link insertTree}, which returns only the * root). Empty or whitespace-only markdown, a dangling `parentId` (checked * again after the async convert, so a parent removed mid-flight also no-ops), * and a converter failure (chunk-load or parse error, swallowed) all return * `[]` and open no transaction. The nodes are fresh-snapshot volatile — read * them now, don't stash them in dep arrays. */ insertMarkdown(markdown: string, options?: { parentId?: string | null; position?: InsertPosition; config?: MarkdownImportConfig; }): Promise; /** * Serialize the WHOLE document to a Markdown string — the outbound twin of * core's `blocks.exportMarkdown` (and the read-side counterpart of the * additive {@link insertMarkdown}). Async: the serializer is lazy-loaded, like * the importer. Markdown cannot express every block, so some structure is * dropped (table `colspan`/`rowspan`, heading columns). Returns `''` for an * empty document. Pre-ready: resolves to `''`. */ exportMarkdown(): Promise; move(id: string, target: MoveTarget): void; nest(id: string, parentId: string): void; unnest(id: string): void; remove(id: string): void; /** * Update a block's data and/or tunes by id. Delegates to core's async * `blocks.update`, which forms its OWN undo step — the call is NOT wrapped in * `transact` (that would close the group before the async write lands). An * unknown id is a silent no-op; a rejected update is swallowed so it can't * surface as an unhandled rejection. Reads refresh reactively once core emits * 'block changed'. Returns `void`. */ update(id: string, data?: BlockToolData, tunes?: { [name: string]: BlockTuneData; }): void; /** * Convert a block to another type ("turn into") by id. Delegates to core's * async `blocks.convert`; both tools must provide a `conversionConfig` or core * rejects — that rejection (and any other) is swallowed so a non-convertible * block is a graceful no-op rather than an unhandled rejection. An unknown id * is a silent no-op. Not wrapped in `transact` (core owns its history step). * Returns `void`. */ convert(id: string, newType: string, dataOverrides?: BlockToolData, options?: { caret?: CaretTarget; }): void; transact(fn: () => void): void; /** * Run `fn` as one atomic operation that is NOT captured in the undo history — * the React-surface counterpart of core's `transactWithoutCapture`. Use for * silent auto-repair/normalization that a user's CMD+Z should never step * through. Mutations inside still emit reactively. Pre-ready it just runs `fn`. */ transactWithoutCapture(fn: () => void): void; /** * The current block count. Reactive (re-reads on 'block changed'). Pre-ready: 0. */ getBlocksCount(): number; /** * The flat index of the block holding the caret, or -1 when none. Pre-ready: -1. */ getCurrentBlockIndex(): number; /** The block at a flat index as a snapshot {@link BlockNode}, or null. */ getBlockByIndex(index: number): BlockNode | null; /** * The absolute flat index of a block by id, or null when unknown. The * counterpart to {@link getBlockByIndex} — use it to target an off-caret * {@link splitBlock} (whose `insertIndex` is absolute) without the ref. Unknown * ids return null silently (no console warn). Pre-ready: null. */ getBlockIndex(id: string): number | null; /** * Read a block's current `data` and `tunes` by id WITHOUT mutating anything — * the synchronous last-extracted view (the same snapshot clipboard ops use). * Makes a client-side duplicate composable from the hook alone: read a node, * then `insert({ type, data, tunes, position })`, no ref escape hatch. Unknown * id returns null. Pre-ready: null. */ getBlockData(id: string): { data: BlockToolData; tunes: { [name: string]: BlockTuneData; }; } | null; /** * The block whose holder contains/equals `element`, as a snapshot * {@link BlockNode}, or null. Useful for mapping a DOM event target back to a * block. */ getBlockByElement(element: HTMLElement): BlockNode | null; /** * Read a tool's default empty data WITHOUT inserting anything — delegates to * core's `composeBlockData`. Async (a tool's data may be composed lazily). * Rejects (via core) for an unknown tool. Pre-ready: resolves to `{}`. */ composeBlockData(toolName: string): Promise; /** * Replace the WHOLE document with blocks parsed from an HTML string — * delegates to core's `renderFromHTML`. Unlike {@link insertMarkdown} (which is * additive), this CLEARS existing content first, so it's a document-load * primitive, not an insert. Async. Pre-ready: resolves immediately (no-op). */ renderFromHTML(html: string): Promise; /** * Insert a flat array of already-serialized {@link OutputBlockData} (the * `save()` shape) directly, honoring each block's `parent`/`content` links — * the raw counterpart of core's `blocks.insertMany`. Use to re-insert a saved * document fragment without reshaping it into {@link TreeInsertSpec}. One * atomic undo step. Returns the created nodes; pre-ready: `[]` (no insert). */ insertOutputData(blocks: OutputBlockData[], options?: { index?: number; }): BlockNode[]; /** * Atomically split a block: update `currentBlockId` with `currentBlockData` * and insert a new `newBlockType` block at `insertIndex`, as ONE undo step. * Delegates to core's `splitBlock`. Returns the new node, or null pre-ready / * on an unknown id. */ splitBlock(currentBlockId: string, currentBlockData: Partial, newBlockType: string, newBlockData: BlockToolData, insertIndex: number): BlockNode | null; /** * Insert a single child block under `parentId` at flat `insertIndex`, atomically * (block creation AND parent assignment in ONE undo step) — delegates to core's * `blocks.insertInsideParent`. This is the atomic nested-child creator: prefer it * over `insert()` + `nest()`, which is TWO undo steps. A dangling `parentId` is a * no-op returning `null` (mirrors {@link insert}); an unknown child tool returns * `null`. `childData` defaults to an empty paragraph. Returns the created * {@link BlockNode}, fresh-snapshot volatile — read it now, don't stash it in a * dep array. Pre-ready: `null`. */ insertInsideParent(parentId: string, insertIndex: number, childData?: BlockToolData): BlockNode | null; /** * Replace the WHOLE document with blocks parsed from saved {@link OutputData} * (the `save()` shape) — delegates to core's `blocks.render`. Unlike * {@link insertOutputData}/{@link insertMarkdown} (which are ADDITIVE), this * CLEARS existing content first: a document-LOAD primitive, not an insert. The * HTML counterpart is {@link renderFromHTML}. Async. Pre-ready: resolves * immediately (no-op). */ render(data: OutputData): Promise; /** * Remove EVERY block from the document — delegates to core's `blocks.clear`. A * document-reset primitive (pairs with {@link render}). Async. Pre-ready: * resolves immediately (no-op). */ clear(): Promise; /** * Whether a Yjs sync (undo/redo) is currently in progress — the React mirror of * core's `blocks.isSyncingFromYjs`. A METHOD (not a property) so it reads the * LIVE flag at call time even though the api handle is memoized. Use it to skip * cleanup that would fight Yjs state during an undo/redo. Pre-ready: `false`. */ isSyncingFromYjs(): boolean; } /** One field of a block's prop schema. */ interface PropSchemaEntry { /** Default value, used when the incoming data omits this key. */ default: unknown; /** Optional allowed values (advisory; not enforced at runtime in v1). */ values?: readonly unknown[]; } /** * Declarative data shape. The keys here are EXACTLY the keys `save()` returns to * Yjs — this closes the per-key-sync key-resurrection gap (a cleared field is * written as its explicit default, never dropped). */ type PropSchema = Record; /** * Every STATIC member of core's block-tool contract an Angular block may declare * for itself — `ownsChildren`, `keepsChildrenOnEnter`, `conversionConfig`, * `pasteConfig`, `sanitize`, `shortcut`, `upgradeData`, and whatever core adds * next. Derived from * `BlockToolConstructable` rather than enumerated, so a new core static needs no * adapter change to become reachable. * * `toolbox` and `isReadOnlySupported` are excluded because the factory owns * them: `toolbox` is authored as {@link CreateAngularBlockSpec.toolbox}, and * in-place read-only support is unconditional. */ type BlockToolStatics = Omit; /** * Second argument of {@link CreateAngularBlockSpec.onMounted} and * {@link CreateAngularBlockSpec.onCreated} — everything the block cannot read * off its own `BlockAPI`. */ interface AngularBlockMountedContext { /** * Why this block instance was constructed: a CREATION origin (`user`, `api`, * `convert`) means the author just made it, so seeding default children is * correct; a RESTORE origin (`load`, `replay`, `paste`) means the document * already says what the children are. `probe` is an off-tree instance built * only to read a tool's default data — it must not touch the block tree at * all. Defaults to `'api'` when the constructor was handed no origin. */ origin: BlockOrigin; /** The editor-level API (`api.blocks`, `api.caret`, `api.events`…). */ api: API; } /** Spec for {@link createAngularBlock}. Authored as a standalone component. */ interface CreateAngularBlockSpec { /** Tool type name (registered key). */ type: string; /** Optional toolbox entry. */ toolbox?: ToolboxConfig; /** Declarative defaults that also define the exact `save()` key set. */ propSchema: PropSchema; /** * The standalone Angular component to render for each block. It injects the * per-block context via `inject(BLOK_BLOCK_CONTEXT)`. */ component: Type; /** * Static members of core's tool contract, forwarded verbatim onto the * generated tool class — the single channel for everything core reads off the * CLASS rather than the instance (`ownsChildren`, `keepsChildrenOnEnter`, * `conversionConfig`, `pasteConfig`, `sanitize`, `shortcut`, `upgradeData`…). * Without it the only way to declare one was to subclass the generated class. * * `keepsChildrenOnEnter` is the per-tool Enter POLICY: declare it and Enter on * this container's empty LAST child creates the new line INSIDE the container * instead of escaping to the container's parent (Blok's default, which is * Notion's callout behaviour). Core cannot read that off the DOM — a callout * renders the same `data-blok-nested-blocks` slot as a column yet wants the * escape — so before it existed a layout container (a card, a `steps` block) * had to hijack the editor-global `config.onEnter` and re-derive containment. * * `toolbox` and `isReadOnlySupported` are owned by the factory and cannot be * overridden here (see {@link BlockToolStatics}). */ statics?: BlockToolStatics; /** * The element the +/drag toolbar should vertically center on — core's * `getToolbarAnchorElement` hook, resolved against this block's host element * on every call (never cached, so it tracks re-renders). * * A container block whose own chrome is not editable needs it: with no anchor, * core centers the toolbar on the first `[contenteditable]` under the host, * which for a container is its FIRST CHILD BLOCK. Return `null`/`undefined` * (or omit the field) to keep core's default. * @param host - this block's mutation-free host element * @param block - this block's per-block API */ getToolbarAnchorElement?: (host: HTMLElement, block: BlockAPI) => HTMLElement | null | undefined; /** Optional lifecycle callbacks mapped from Blok's block hooks. */ onRendered?: (block: BlockAPI) => void; /** * Fired ONCE per block instance, once the component's DOM (and, for a * container, the child holders its `ctx.mountChildren` adopted) exists. * Angular mounts the block synchronously while core is still inside * `render()`, so this lands with `rendered()` — the first hook at which the * host is also in the document. The React and Vue adapters spell the same * contract; there it is genuinely LATER than `onRendered`, because their * portals commit a frame after core returns. * * It is also the create-vs-restore signal: `context.origin` says whether the * author just made this block (`user`/`api`/`convert`) or the document is * being re-materialised (`load`/`replay`/`paste`) — so a container can seed * its default children here exactly once, without the "children are * transiently empty during a replay" trap. * @example * ```ts * onMounted: (block, { origin, api }) => { * if (origin === 'user' && block.getChildren().length === 0) { * api.blocks.insertInsideParent(block.id); * } * } * ``` */ onMounted?: (block: BlockAPI, context: AngularBlockMountedContext) => void; /** * The SEEDING hook: `onMounted`, narrowed to a genuine creation. Fired ONCE * per block instance, once the component's DOM exists, and only when this * instance is the author making a new block (`origin` of `user`, `api` or * `convert`) — never for a `load`/`replay`/`paste` restore, and never for the * off-tree `probe` instance core builds to read a tool's default data. * * That predicate is why the hook exists rather than leaving every block to * read `context.origin` in `onMounted`: the intuitive `origin === 'user'` test * is wrong. It drops `api.blocks.insert('steps')` and turn-into, so a * container seeded that way comes up empty for every path except a keystroke. * Core refused to ship that axis into its own `column`/`column_list`; this * encodes the correct one once, here. * * The `context` is the same object {@link CreateAngularBlockSpec.onMounted} * receives, so a block that only seeds can read `origin` for finer decisions. * @example * ```ts * onCreated: (block, { api }) => { * if (block.getChildren().length === 0) { * api.blocks.insertInsideParent(block.id); * } * } * ``` */ onCreated?: (block: BlockAPI, context: AngularBlockMountedContext) => void; onMoved?: (block: BlockAPI) => void; onRemoved?: (block: BlockAPI) => void; } /** * Author a first-party Angular block. Returns a `BlockToolConstructable` * registered exactly like a vanilla tool (`tools: { type: { class: * createAngularBlock(...) } }`). * * The factory owns the host element (`data-blok-mutation-free`), a frozen * defaults-filled data mirror, and signals the component reads. It mounts the * component into the host via the editor's shared portal registry (the analog of * Vue's Teleport registry), bridging Blok's block lifecycle to Angular: * - `render()` creates the host and registers the portal entry (mounted sync). * - `setData()` dedups, swaps the reactive snapshot, flushes CD, resolves true. * - `save()` returns the complete frozen mirror (never the DOM, never partial). * - `commit()` merges a patch and fires `dispatchChange` exactly once. * - `setReadOnly()` flips a reactive flag and flushes CD (in-place, no remount). * - `removed()`/`destroy()` unregister the portal (deterministic unmount). */ declare function createAngularBlock(spec: CreateAngularBlockSpec): (new (options: BlockToolConstructorOptions) => { render(): HTMLElement; save(): BlockToolData; setData(newData: BlockToolData): Promise; setReadOnly(state: boolean): void; getToolbarAnchorElement(): HTMLElement | undefined; rendered(): void; moved(): void; removed(): void; destroy(): void; }) & BlockToolStatics & { readonly __isBlokAngularBlock: true; readonly toolbox: ToolboxConfig | undefined; readonly isReadOnlySupported: boolean; }; /** Options for {@link injectBlocks}. */ interface InjectBlocksOptions { /** * Invalidate reads only for changes inside the subtree rooted at this block id * (the block itself or any descendant). Omit — or pass `null` — for the * document-wide default. * * This bounds REACTIVITY, not reads: the returned API still sees the whole * tree, so a scoped consumer can still `getById` anything. Reach for it in a * container block that renders only its own children — unscoped, such a block * invalidates on every keystroke anywhere in the document, and a page of N * containers turns one keystroke into N re-renders. * * Accepted as a plain value or a signal (like `editor`) and read at EMIT time, * so changing it takes effect immediately with no re-subscription. * * A change whose block cannot be placed in the tree (a removal that emits * after the block is gone) counts as in-scope: skipping it would leave a * container rendering a child that no longer exists. */ within?: Signal | string | null; } /** * Angular factory exposing an id/parentId-relative, reactive view of the block * tree. Reads refresh whenever the editor emits `block changed`; mutators route * through the editor-level `blocks` API (core's chokepoints), so undo/redo and * Yjs sync are inherited rather than re-implemented. * * The block-tree logic is framework-agnostic and lives in the shared * {@link createBlocksApiForEditor} core — the SAME implementation behind React's * and Vue's `useBlocks`, so all three adapters expose the identical 28-method * surface and cannot drift. This wrapper supplies only Angular's reactivity: * * - A private `version` signal is bumped on every `block changed`. The shared API * is built with an `onRead` seam (`() => { version() }`) that every read method * calls, so reading inside a `computed`/template tracks `version` and re-runs on * each structural mutation. * - The bound API is rebuilt (via `bindToEditor`, called eagerly on first call and * then tracked by an `effect` for subsequent identity changes) when the editor * IDENTITY changes; the returned facade is stable across that swap. * * Call it in an injection context (component constructor / field initializer), * passing the editor signal (e.g. `BlokEditorComponent.instance` / * `BlokContentDirective.instance`). Pre-ready (editor null) the bound API is the * shared {@link EMPTY_API}: every mutator a no-op, reads empty/null — except * `transact`/`transactWithoutCapture`, which still run their callback. * * Note: Angular 20's `effect()` is scheduled (not eager) — it does not fire * synchronously on first call. The initial binding therefore runs synchronously * in the `injectBlocks` body itself (mirroring Vue's `{ immediate: true }` watch), * and the effect re-binds only on subsequent editor identity changes. When the * effect fires for the first time with the same editor already bound, the guard * `ed === sub.editor` makes it a no-op, so no double-subscription occurs. * * @param editor - a signal of the Blok instance, or null pre-ready * @param options - reactivity options; see {@link InjectBlocksOptions.within} to * scope invalidation to one block's subtree */ declare function injectBlocks(editor: Signal, options?: InjectBlocksOptions): UseBlocksApi; /** How the caller names the DOM scope to observe. */ type BlokReadyScope = (() => Element | ElementRef | null | undefined) | ElementRef | Element | null; /** Options accepted by {@link injectBlokReady}. */ interface InjectBlokReadyOptions { /** * Restrict the wait to editors mounted inside this element. Accepts an * element, an `ElementRef`, or a getter/signal returning either — the getter * form lets you pass `() => this.scopeRef?.nativeElement` from a field * initializer, before the view exists. It is re-read on every readiness * change. * * Omit it to observe every editor on the page. Passing it while it still * resolves to null reports NOT ready — an unresolved scope must never fall * back to the page-global one. */ within?: BlokReadyScope; /** * `'ready'` (default) settles when each editor has finished booting. * `'rendered'` also waits for its content to be in the DOM and re-arms on * every post-boot re-render. */ settleOn?: 'ready' | 'rendered'; } /** * Angular factory exposing the live readiness of the Blok editors in a DOM * scope as a boolean signal. * * The readiness logic itself is framework-agnostic and lives in core * (`Blok.readyState` / `Blok.subscribeReady`) — the SAME implementation behind * React's and Vue's `useBlokReady`, so the three adapters cannot drift. This * wrapper supplies only Angular's reactivity: a signal re-read on every * registry change, unsubscribed through the injector's `DestroyRef`. * * Call it in an injection context (component constructor / field initializer). * The signal starts `false` and takes its first real reading in * `afterNextRender` — browser-only by contract, and late enough for a * `@ViewChild` scope to exist. Over-waiting is safe; under-waiting is a bug. * @param options - scope and readiness depth */ declare function injectBlokReady(options?: InjectBlokReadyOptions): Signal; /** * One child's decoration: attribute name → value. `null`/`undefined` removes the * attribute; a boolean or number is stringified (so `false` writes * `data-active="false"`, which CSS can select, rather than dropping the hook). */ type ChildAttributes = Record; /** The per-child decorator accepted by {@link AngularBlockRenderContext.mountChildren}. */ type ChildAttributesFn = (child: BlockAPI, index: number) => ChildAttributes; /** * Context handed to a `createAngularBlock` component. Delivered via DI (the * BLOK_BLOCK_CONTEXT token) rather than @Input, because core constructs the tool * outside Angular and the signal-input() form does not compile under the repo's * JIT test harness. The ONLY data write path is `commit`. */ interface AngularBlockRenderContext { /** Reactive, FROZEN snapshot of the block data. Read `data()`; never mutate. */ data: Signal>; /** The ONLY data write path: merge a partial patch and sync once. */ commit: (patch: Partial) => void; /** This block's per-block API (id, getChildren, dispatchChange…). */ block: BlockAPI; /** * The EDITOR-level API this block belongs to (`api.blocks`, `api.caret`, * `api.toolbar`…) — the same object a vanilla tool receives in its * constructor. Reach for it when a block has to drive the document around it * instead of routing everything through `block.call()` string dispatch. * * For the reactive, id/parentId-relative view of the tree (and to re-render * when your own children change), pair `injectBlocks` with * `injectBlokInstance()` instead — the api handle itself is not reactive. */ api: API; /** * Reactive read-only flag. Read `readOnly()` in the template to disable * editing (drop `contenteditable`, hide controls). Toggled IN PLACE by core's * read-only switch — the component reacts without a remount, so ephemeral * state survives. A block that ignores it stays interactive when the editor is * read-only (same contract as a vanilla tool's `setReadOnly`). */ readOnly: Signal; /** * Container blocks only: append this block's real child holders into `host` * (a `data-blok-nested` element the author owns). Call it once in * `ngAfterViewInit`; the factory re-runs the same mount on every data change * so late-added children appear. Angular must NOT manage these child holders. * * `childAttributes` decorates each child's HOLDER after the holders are * mounted, and is REMEMBERED — every later remount re-applies it. Named hooks * (`data-step-index`, `data-active`…) replace positional `:nth-child()` CSS * over Blok's holders, which breaks the moment a child is inserted, removed or * reordered. The holders stay DIRECT children of `host` — core requires that * (hierarchy reparenting and caret navigation compare `holder.parentElement` * by identity), so decoration is attributes, never wrapper elements. * Attributes the callback stops producing are removed on the next pass. * * `childContentAttributes` is the same decoration applied one level IN — on * each child's `[data-blok-element-content]` wrapper instead of its holder — * and is remembered the same way. Core's decoration law blesses both, and a * container needs both: the holder is the child's outer box (rails, indices, * hover states), the content wrapper is where the child's own text box begins, * which is what a numbered rail or a connector line has to align to. Reach for * it instead of walking core's wrapper chain from a holder hook (`[data-step] > * [data-blok-element-content] > …`) — those selectors encode engine DOM in host * CSS and break when that structure changes. A child whose DOM has not * committed yet has no wrapper to write to and is stamped on the next pass. * * Writing on a child's holder is inert by design: core's mutation filter drops * a holder-targeted attribute record for the child block and suppresses it for * the container. The guarantee stops at the holder and its * `[data-blok-element-content]` wrapper — writing AT or BELOW a child's tool * root DOES score as that child's edit. */ mountChildren: (host: HTMLElement, childAttributes?: ChildAttributesFn, childContentAttributes?: ChildAttributesFn) => void; /** * Name the element the +/drag toolbar should vertically center on, from inside * the component: `ctx.setToolbarAnchor(this.head().nativeElement)` in * `ngAfterViewInit`. The Angular counterpart of React's/Vue's * `toolbarAnchorRef` — core's `getToolbarAnchorElement` is otherwise a * `(host, block) => Element` hook resolved OUTSIDE the component, so pointing * at an element the template renders meant inventing a data attribute and * `querySelector`-ing for it from * `CreateAngularBlockSpec.getToolbarAnchorElement`. * * A container block whose own chrome is not editable needs an anchor: with * none, core centers the toolbar on the first `[contenteditable]` under the * host, which for a container is its FIRST CHILD BLOCK — parking the +/drag * handles halfway down, beside content that has a toolbar of its own. * * The element set here outranks the declared hook while it is MOUNTED; once it * detaches (or `null` is passed) the hook takes over again, so the toolbar is * never positioned against a stale node. Never calling it keeps core's default. */ setToolbarAnchor: (element: HTMLElement | null) => void; } /** DI token carrying the per-block render context into the authored component. */ declare const BLOK_BLOCK_CONTEXT: InjectionToken>; /** * DI token carrying the LIVE editor instance (as a signal) into every block * mounted by `createAngularBlock`. `BlokContentDirective` publishes its own * `instance` signal through it, so the value is null before the editor is ready * and after teardown — matching the React/Vue adapters' pre-ready contract. * * Provided on the block's ELEMENT injector by the portal registry, so it is * per-EDITOR: two editors on one page each publish their own instance. */ declare const BLOK_EDITOR_INSTANCE: InjectionToken>; /** * The live Blok instance the component is mounted inside, as a signal, or a * signal of null before it exists. Inside a `createAngularBlock` component this * is the block's OWN editor — so a block can drive the tree it lives in without * the host prop-drilling the instance into it: * * ```ts * private readonly editor = injectBlokInstance(); * private readonly blocks = injectBlocks(this.editor); * ``` * * That pairing is also what makes a container block REACTIVE to its own child * tree: `injectBlocks` refreshes on the editor's `block changed` event, which * core emits for every structural mutation — including children the adapter * itself never sees (a pasted paragraph, a Tab-indent from the keyboard). * * Call it in an injection context (a field initializer or the constructor). * Outside an editor it returns a signal of null. */ declare function injectBlokInstance(): Signal; export { BLOK_BLOCK_CONTEXT, BLOK_DEFAULT_CONFIG, BLOK_EDITOR_INSTANCE, BlokContentDirective, BlokEditorComponent, createAngularBlock, injectBlocks, injectBlokInstance, injectBlokReady, provideBlok }; export type { AngularBlockMountedContext, AngularBlockRenderContext, BlockNode, BlockToolStatics, BlokAngularConfig, BlokReadyScope, CaretTarget, ChildAttributes, ChildAttributesFn, CreateAngularBlockSpec, InjectBlocksOptions, InjectBlokReadyOptions, InsertPosition, InsertSpec, MoveTarget, PropSchema, PropSchemaEntry, TreeInsertSpec, UseBlocksApi };