@bloklabs/angular 1.2.6 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,752 @@
1
+ import * as i0 from '@angular/core';
2
+ import { AfterViewInit, EventEmitter, OnDestroy, InjectionToken, EnvironmentProviders, Type, Signal, ElementRef } from '@angular/core';
3
+ import { ControlValueAccessor } from '@angular/forms';
4
+ import { BlokConfig, EditorWidth, Blok, OutputData, API, BlockMutationEvent, ResolvedTheme, BlocksRenderedPayload, BlockRenderedPayload, OutputBlockData, ThemeMode, LooseOutputData, BlockToolData, BlockTuneData, MarkdownImportConfig, ToolboxConfig, BlockAPI, BlockToolConstructorOptions } from '@bloklabs/core';
5
+
6
+ /**
7
+ * Configuration for the Angular adapter. Same as `BlokConfig` but without
8
+ * `holder` (the adapter owns the host element), plus the reactive `width` prop.
9
+ * Mirrors the React adapter's `UseBlokConfig`.
10
+ */
11
+ type BlokAngularConfig = Omit<BlokConfig, 'holder'> & {
12
+ /** Editor content width mode. Synced reactively after mount via `editor.width.set()`. */
13
+ width?: EditorWidth;
14
+ };
15
+
16
+ /**
17
+ * The blessed all-in-one Angular component for embedding Blok (mirrors React's
18
+ * `BlokEditor`). Delegates instance lifecycle to an internal `BlokContentDirective`
19
+ * and layers the typed reactive input/output API on top.
20
+ *
21
+ * Reactive inputs (`readOnly`, `hideToolbar`, `inlineToolbar`, `theme`,
22
+ * `width`, `placeholder`, `autofocus`, `styleTokens`, `i18n`, `data`) are
23
+ * synced in place after mount via effects — `hideToolbar` through
24
+ * `editor.toolbar.setHidden()`, `inlineToolbar` through
25
+ * `editor.tools.setInlineToolbar()` (content-compared),
26
+ * `styleTokens` through `editor.tokens.set()` (replace semantics),
27
+ * `i18n` through `editor.i18n.update()` (deep-equal–deduped; seeded at
28
+ * construction so the locale resolves during boot) and `data`
29
+ * through `editor.render()` (deep-equal–deduped); everything else seeds
30
+ * construction.
31
+ *
32
+ * Implementation note: classic `@Input()`/`@ViewChild()` decorators are used
33
+ * (not signal `input()`/`viewChild()`) for JIT compatibility — see
34
+ * `BlokContentDirective`. Each reactive input is backed by an internal `signal`
35
+ * so effect-based syncing still works; the public template API is unchanged.
36
+ */
37
+ declare class BlokEditorComponent implements AfterViewInit, ControlValueAccessor {
38
+ private readonly ngZone;
39
+ /** App-wide defaults from `provideBlok()`; merged UNDER per-instance inputs. */
40
+ private readonly defaults;
41
+ private contentQuery?;
42
+ /** Signal bridge for the @ViewChild directive so effects react to its arrival. */
43
+ private readonly content;
44
+ /**
45
+ * Output half of two-way `[(data)]`. Emits the editor's serialized content on
46
+ * every change. Wiring the core `onSave` callback is gated on this being
47
+ * observed, since its mere presence makes the core serialize on every batch.
48
+ */
49
+ /** Emits the live Blok instance once ready, after `instance()` is populated. */
50
+ readonly ready: EventEmitter<Blok>;
51
+ readonly dataChange: EventEmitter<OutputData>;
52
+ /** Fires with the full serialized content on every change (notification half). */
53
+ readonly save: EventEmitter<OutputData>;
54
+ /** Raw block mutation channel (core `onChange`). */
55
+ readonly change: EventEmitter<{
56
+ api: API;
57
+ event: BlockMutationEvent | BlockMutationEvent[];
58
+ }>;
59
+ /** Fires after the editor finishes (re-)rendering (core `onAfterRender`). */
60
+ readonly afterRender: EventEmitter<API>;
61
+ /** Fires with the resolved theme whenever it changes (core `onThemeChange`). */
62
+ readonly themeChange: EventEmitter<ResolvedTheme>;
63
+ /** Fires after a batch render completes (core `blocks:rendered` event). */
64
+ readonly blocksRendered: EventEmitter<BlocksRenderedPayload>;
65
+ /** Fires for each block rendered into the DOM (core `block:rendered` event). */
66
+ readonly blockRendered: EventEmitter<BlockRenderedPayload>;
67
+ /**
68
+ * Transform hook applied to blocks before render (core `onBeforeRender`). Must
69
+ * return the (possibly modified) block list. An input, not an output, because
70
+ * it returns a value.
71
+ */
72
+ onBeforeRender?: (blocks: OutputBlockData[]) => OutputBlockData[];
73
+ /**
74
+ * Transform hook applied to pasted HTML (core `onBeforePaste`). Returns the
75
+ * (possibly modified) html, or null to drop the paste.
76
+ */
77
+ onBeforePaste?: (html: string) => string | null;
78
+ private readonly readOnly$;
79
+ set readOnly(value: BlokConfig['readOnly'] | undefined);
80
+ private readonly hideToolbar$;
81
+ set hideToolbar(value: boolean | undefined);
82
+ private readonly inlineToolbar$;
83
+ set inlineToolbar(value: boolean | string[] | undefined);
84
+ private readonly theme$;
85
+ set theme(value: ThemeMode | undefined);
86
+ private readonly width$;
87
+ set width(value: EditorWidth | undefined);
88
+ private readonly placeholder$;
89
+ set placeholder(value: string | false | undefined);
90
+ /**
91
+ * Theme tokens. Construction-only config forced hosts with a live light/dark
92
+ * toggle to recreate the editor or hand-write the global stylesheet Blok
93
+ * already injects; this drives the runtime `tokens` API instead.
94
+ */
95
+ private readonly styleTokens$;
96
+ set styleTokens(value: Record<string, string> | undefined);
97
+ /**
98
+ * Locale, host message overrides and text direction. `config.i18n` was
99
+ * consumed once at boot, so a host driving a language switcher had to
100
+ * recreate the editor (losing caret, focus and undo stack) to relabel the
101
+ * UI; this drives the runtime `i18n.update` API instead. `defaultLocale` is
102
+ * not forwarded — it only affects the INITIAL locale resolution.
103
+ */
104
+ private readonly i18n$;
105
+ set i18n(value: BlokConfig['i18n'] | undefined);
106
+ private readonly autofocus$;
107
+ set autofocus(value: boolean | undefined);
108
+ private readonly data$;
109
+ set data(value: OutputData | LooseOutputData | undefined);
110
+ tools?: BlokConfig['tools'];
111
+ /**
112
+ * Escape hatch: a full config object for keys without a dedicated input
113
+ * (sanitizer, minHeight, …). Layered between provideBlok
114
+ * defaults and the discrete inputs.
115
+ */
116
+ config?: Partial<BlokAngularConfig>;
117
+ /** Changing this input's identity destroys and recreates the editor (≙ React `deps`). */
118
+ recreateKey: unknown;
119
+ /**
120
+ * Content the editor currently reflects. Set when seeded or rendered (the data
121
+ * effect below). A controlled `data` echo that deep-equals this baseline is a
122
+ * no-op, so it won't clobber the caret. Renders are serialized via `renderChain`.
123
+ */
124
+ private lastRenderedData?;
125
+ /** Last token set pushed through `tokens.set`, for deep-equal deduping. */
126
+ private appliedTokens?;
127
+ /** Last i18n config pushed through `i18n.update`, for deep-equal deduping. */
128
+ private appliedI18n?;
129
+ /** Editor instance the i18n baseline above belongs to. */
130
+ private i18nAppliedFor;
131
+ /** Last value pushed through `tools.setInlineToolbar`, for content-compare deduping. */
132
+ private appliedInlineToolbar?;
133
+ private seededEditor;
134
+ private renderChain;
135
+ /** Registered by `ControlValueAccessor.registerOnChange` (Angular forms). */
136
+ private cvaOnChange?;
137
+ private cvaOnTouched?;
138
+ /**
139
+ * Core `onSave` wrapper. Records the editor's own serialized output as the
140
+ * rendered baseline BEFORE notifying Angular, so a controlled consumer echoing
141
+ * it straight back into `data` deep-equals the baseline and is deduped to a
142
+ * no-op (no redundant render, no caret reset). Re-enters the Angular zone since
143
+ * the editor runs outside it.
144
+ */
145
+ private readonly coreOnSave;
146
+ /** Live Blok instance, or null until `isReady` resolves / after destroy. */
147
+ readonly instance: i0.Signal<Blok | null>;
148
+ /** Guards `ready` to one emission per editor instance. */
149
+ private lastReadyInstance;
150
+ /**
151
+ * Construction config handed to the directive. Seeds the editor's initial
152
+ * values; the directive only reads it once (at construction), so post-mount
153
+ * changes to construction-only inputs are no-ops by design.
154
+ */
155
+ buildConfig(): Partial<BlokAngularConfig>;
156
+ ngAfterViewInit(): void;
157
+ /** Serialize the current content. Resolves undefined until the editor is ready. */
158
+ save$(): Promise<OutputData> | undefined;
159
+ /** Move the caret into the editor. */
160
+ focus(atEnd?: boolean): void;
161
+ /** Replace the editor content. Resolves undefined until the editor is ready. */
162
+ render(data: OutputData | LooseOutputData): Promise<void> | undefined;
163
+ constructor();
164
+ /** Seeds/renders an externally-set form value through the same dedup machinery. */
165
+ writeValue(value: OutputData | LooseOutputData | null): void;
166
+ registerOnChange(fn: (data: OutputData) => void): void;
167
+ registerOnTouched(fn: () => void): void;
168
+ /** Form disabled state maps to the editor's read-only mode (via the readOnly effect). */
169
+ setDisabledState(isDisabled: boolean): void;
170
+ static ɵfac: i0.ɵɵFactoryDeclaration<BlokEditorComponent, never>;
171
+ static ɵcmp: i0.ɵɵComponentDeclaration<BlokEditorComponent, "blok-editor", ["blok"], { "onBeforeRender": { "alias": "onBeforeRender"; "required": false; }; "onBeforePaste": { "alias": "onBeforePaste"; "required": false; }; "readOnly": { "alias": "readOnly"; "required": false; }; "hideToolbar": { "alias": "hideToolbar"; "required": false; }; "inlineToolbar": { "alias": "inlineToolbar"; "required": false; }; "theme": { "alias": "theme"; "required": false; }; "width": { "alias": "width"; "required": false; }; "placeholder": { "alias": "placeholder"; "required": false; }; "styleTokens": { "alias": "styleTokens"; "required": false; }; "i18n": { "alias": "i18n"; "required": false; }; "autofocus": { "alias": "autofocus"; "required": false; }; "data": { "alias": "data"; "required": false; }; "tools": { "alias": "tools"; "required": false; }; "config": { "alias": "config"; "required": false; }; "recreateKey": { "alias": "recreateKey"; "required": false; }; }, { "ready": "ready"; "dataChange": "dataChange"; "save": "save"; "change": "change"; "afterRender": "afterRender"; "themeChange": "themeChange"; "blocksRendered": "blocksRendered"; "blockRendered": "blockRendered"; }, never, never, true, never>;
172
+ }
173
+
174
+ /**
175
+ * Escape-hatch directive and lifecycle engine for Blok (mirrors React's
176
+ * `useBlok` + `BlokContent`). Constructs a Blok instance into its own host
177
+ * element and tears it down on destroy.
178
+ *
179
+ * The editor renders directly into the directive's host element — so the host
180
+ * IS the holder. This sidesteps the fact that Blok core exposes no public
181
+ * `holder` accessor: the adapter never has to read it back, it owns it.
182
+ *
183
+ * `BlokEditorComponent` applies this directive to its internal `<div>` and reads
184
+ * the instance back; consumers can also use it directly for full control:
185
+ * `<div blokContent [config]="cfg" (ready)="onReady($event)"></div>`.
186
+ *
187
+ * Implementation note: classic `@Input()`/`@Output()` are used (not signal
188
+ * `input()`/`output()`) because the repo's Vitest+Analog harness compiles the
189
+ * adapter via JIT, which does not register signal-based members. `instance`
190
+ * remains a plain `signal` (runtime API, JIT-safe) for reactive consumption.
191
+ */
192
+ declare class BlokContentDirective implements OnDestroy {
193
+ private readonly host;
194
+ private readonly ngZone;
195
+ private readonly isBrowser;
196
+ /**
197
+ * App-wide defaults from `provideBlok()`, merged UNDER the bound `[config]` so
198
+ * the escape-hatch path honors them just like `<blok-editor>` (which also
199
+ * merges, idempotently). Mirrors React's `useBlok` merging context defaults.
200
+ */
201
+ private readonly defaults;
202
+ private readonly envInjector;
203
+ private readonly appRef;
204
+ private readonly errorHandler;
205
+ /** Construction-time Blok config (everything except `holder`, which the directive owns). */
206
+ config: Partial<BlokAngularConfig>;
207
+ /** Changing this input's identity (after the first build) destroys + recreates the editor. */
208
+ set recreateKey(value: unknown);
209
+ /** Emits the live Blok instance once it is ready, after `instance` is populated. */
210
+ readonly ready: EventEmitter<Blok>;
211
+ /** The live Blok instance, or null before `isReady` resolves / after destroy. */
212
+ readonly instance: i0.WritableSignal<Blok | null>;
213
+ /** The editor created by the most recent construction; used as the staleness key. */
214
+ private current;
215
+ private destroyed;
216
+ private built;
217
+ private currentKey;
218
+ /** The portal registry for the current editor (Angular-block mounting). */
219
+ private registry;
220
+ constructor();
221
+ /** Construct a Blok into the host element and publish it once ready. */
222
+ private build;
223
+ ngOnDestroy(): void;
224
+ static ɵfac: i0.ɵɵFactoryDeclaration<BlokContentDirective, never>;
225
+ static ɵdir: i0.ɵɵDirectiveDeclaration<BlokContentDirective, "[blokContent]", never, { "config": { "alias": "config"; "required": false; }; "recreateKey": { "alias": "recreateKey"; "required": false; }; }, { "ready": "ready"; }, never, never, true, never>;
226
+ }
227
+
228
+ /**
229
+ * DI token holding app-wide Blok defaults (shared tools registry, default theme,
230
+ * i18n). Merged under per-instance inputs by `BlokEditorComponent`.
231
+ */
232
+ declare const BLOK_DEFAULT_CONFIG: InjectionToken<Partial<BlokAngularConfig>>;
233
+ /**
234
+ * Standalone provider registering app-wide Blok defaults.
235
+ *
236
+ * @example
237
+ * ```ts
238
+ * bootstrapApplication(AppComponent, {
239
+ * providers: [provideBlok({ theme: 'dark', tools: sharedTools })],
240
+ * });
241
+ * ```
242
+ */
243
+ declare function provideBlok(defaults: Partial<BlokAngularConfig>): EnvironmentProviders;
244
+
245
+ /**
246
+ * Where to place the caret within a block. `position` selects the input
247
+ * (`'start'`/`'end'`/`'default'`) and `offset` is the character offset within it
248
+ * — the same shape core's `caret.setToBlock` accepts.
249
+ */
250
+ interface CaretTarget {
251
+ position?: 'start' | 'end' | 'default';
252
+ offset?: number;
253
+ }
254
+ /**
255
+ * A plain, serializable view of one block in the tree.
256
+ *
257
+ * Snapshot-volatile: every read allocates a fresh `BlockNode`, and `contentIds`
258
+ * is DERIVED per read from the children that currently name this block as parent
259
+ * (it is not a stored field). Read a node in render and re-read after a change —
260
+ * don't stash one in a `useMemo`/`useEffect` dependency array expecting stable
261
+ * identity; depend on the `id` instead.
262
+ */
263
+ interface BlockNode {
264
+ id: string;
265
+ type: string;
266
+ parentId: string | null;
267
+ contentIds: readonly string[];
268
+ }
269
+ /** Where to place a block among its siblings. */
270
+ type InsertPosition = 'start' | 'end' | {
271
+ before: string;
272
+ } | {
273
+ after: string;
274
+ };
275
+ interface InsertSpec {
276
+ type?: string;
277
+ data?: BlockToolData;
278
+ parentId?: string | null;
279
+ position?: InsertPosition;
280
+ /**
281
+ * Move the caret into the new block. Defaults to `false`: a programmatic
282
+ * insert from React must not steal focus from wherever the user is typing.
283
+ * Set `true` for an explicit "add a block and start editing it" flow.
284
+ */
285
+ focus?: boolean;
286
+ /**
287
+ * Replace the block at the resolved slot instead of inserting a new one — a
288
+ * programmatic "turn into". Combine with a `position` that targets the block
289
+ * to replace, e.g. `{ position: { before: id }, replace: true }`.
290
+ */
291
+ replace?: boolean;
292
+ /**
293
+ * Explicit id for the new block (generated when omitted). Passing a stable id
294
+ * makes the insert idempotent: if a block with this id already exists the
295
+ * existing node is returned and nothing is inserted ("insert if absent"),
296
+ * so an effect that re-runs won't create duplicates.
297
+ */
298
+ id?: string;
299
+ /** Block tune data to apply at creation, keyed by tune name. */
300
+ tunes?: {
301
+ [name: string]: BlockTuneData;
302
+ };
303
+ /**
304
+ * Place the caret inside the newly-created block at a specific position/offset
305
+ * (e.g. `{ offset: 3 }`). Implies focus. Applied ONLY when a block is actually
306
+ * created — an insert-if-absent hit (existing id) does not move the caret.
307
+ * Use this instead of the boolean `focus` when you need a specific offset.
308
+ */
309
+ caret?: CaretTarget;
310
+ }
311
+ /**
312
+ * One node of a pre-built nested subtree for {@link UseBlocksApi.insertTree}.
313
+ *
314
+ * Each node maps to one block; `children` are inserted nested under it (their
315
+ * `parentId` set to this node's id) in array order, recursively. Placement
316
+ * options (`parentId`/`position`) are ROOT-ONLY — they position the whole
317
+ * subtree among existing blocks and are ignored on nested children, whose parent
318
+ * is always their enclosing node.
319
+ */
320
+ interface TreeInsertSpec {
321
+ type?: string;
322
+ data?: BlockToolData;
323
+ tunes?: {
324
+ [name: string]: BlockTuneData;
325
+ };
326
+ /**
327
+ * Explicit id for this node (generated when omitted). Unlike `insert`, this is
328
+ * NOT insert-if-absent: a tree insert always creates fresh blocks. A colliding
329
+ * id — one that already exists in the document, or is reused by another node
330
+ * in the same spec — is REJECTED up front: nothing is inserted and `insertTree`
331
+ * returns `null` (a duplicate id would corrupt every id-keyed lookup).
332
+ */
333
+ id?: string;
334
+ /** Direct children, inserted nested under this node, in array order. */
335
+ children?: TreeInsertSpec[];
336
+ /** Root-only: where to place the whole subtree. Ignored on nested children. */
337
+ parentId?: string | null;
338
+ /** Root-only: slot among siblings of `parentId`. Ignored on nested children. */
339
+ position?: InsertPosition;
340
+ }
341
+ /**
342
+ * Where to move an existing block.
343
+ *
344
+ * `before`/`after` are POSITION targets, not parent assignments: the block is
345
+ * relocated to that flat slot and — because Blok keeps the flat array as the
346
+ * canonical document order — ADOPTS the parent of wherever it lands. Moving a
347
+ * nested block to `{ after: someRootBlock }` therefore unnests it to root, and
348
+ * moving a root block in among a container's children nests it. Use
349
+ * `nest`/`unnest` when you want to change the parent without choosing a sibling
350
+ * slot. `toIndex` is an absolute flat index (clamped into range).
351
+ */
352
+ type MoveTarget = {
353
+ before: string;
354
+ } | {
355
+ after: string;
356
+ } | {
357
+ toIndex: number;
358
+ };
359
+ interface UseBlocksApi {
360
+ getById(id: string): BlockNode | null;
361
+ getChildren(parentId: string | null): BlockNode[];
362
+ /**
363
+ * Insert one block; returns the created node or null when rejected (unknown
364
+ * tool type, dangling `parentId`, or a `replace` whose target is missing). An
365
+ * explicit `id` that already exists is insert-if-absent (returns the existing
366
+ * node, creates nothing). Atomic — one undo step. The returned node is
367
+ * {@link BlockNode}-volatile; read it now, don't put it in a dep array.
368
+ */
369
+ insert(spec?: InsertSpec): BlockNode | null;
370
+ /**
371
+ * Insert several blocks atomically, in array order, as ONE undo step. Each
372
+ * spec is a full {@link InsertSpec} (own type/data/parentId/position), routed
373
+ * through the same single-`insert` path, so per-spec parent assertion and
374
+ * positioning still apply. Specs that fail to insert (e.g. a dangling
375
+ * parentId, or a replace whose target is missing) are dropped; the returned
376
+ * array holds only the successfully created nodes. An empty input is a no-op
377
+ * (returns `[]`, opens no transaction). Like `insert`, the returned nodes are
378
+ * fresh-snapshot volatile — read them now, don't stash them in dep arrays.
379
+ */
380
+ insertMany(specs: InsertSpec[]): BlockNode[];
381
+ /**
382
+ * Insert a pre-built NESTED subtree in ONE atomic operation (one undo step).
383
+ * Each {@link TreeInsertSpec} node becomes a block; its `children` are inserted
384
+ * nested under it (recursively, in array order) so the whole hierarchy lands in
385
+ * a single call — no follow-up `nest` round-trips. Delegates to core's
386
+ * tree-aware `blocks.insertMany`, which composes the flat DFS pre-order array
387
+ * honoring each node's `parent`/`content` links.
388
+ *
389
+ * Placement is root-only: the root node's `parentId`/`position` position the
390
+ * whole subtree among existing blocks (default: appended at the document end);
391
+ * nested children ignore those fields (their parent is their enclosing node). A
392
+ * dangling root `parentId` is rejected — nothing is inserted and `null` is
393
+ * returned (mirrors {@link insert}). Returns the root {@link BlockNode}, which
394
+ * is fresh-snapshot volatile — read it now, don't stash it in a dep array.
395
+ */
396
+ insertTree(spec: TreeInsertSpec): BlockNode | null;
397
+ /**
398
+ * Convert a Markdown string to blocks and insert them ADDITIVELY at a
399
+ * position, WITHOUT clearing the document (unlike core's `importMarkdown` /
400
+ * `renderFromHTML`, which replace the whole document). This is the React
401
+ * "paste markdown → blocks appear" path.
402
+ *
403
+ * Async: the markdown converter is lazy-loaded (kept out of the main bundle),
404
+ * so this is the ONE async creator in the API — `await` the returned promise.
405
+ * The whole batch is inserted as a single atomic undo step.
406
+ *
407
+ * `position` (default `'end'`) places the converted run among `parentId`'s
408
+ * children (or root siblings when `parentId` is omitted/null), reusing the
409
+ * same `start`/`end`/`before`/`after` semantics as {@link insert}.
410
+ *
411
+ * `parentId` (default `null` = root) nests the import: every TOP-LEVEL
412
+ * converted block (one the converter left un-parented) is reparented under
413
+ * `parentId`, while blocks the markdown nested internally (e.g. table-cell
414
+ * children) keep their intra-import parent. A dangling `parentId` is a no-op
415
+ * (returns `[]`, opens no transaction), matching {@link insert}.
416
+ *
417
+ * `config` (optional {@link MarkdownImportConfig}) is forwarded to the
418
+ * converter so custom-tool consumers can map markdown nodes into their tools
419
+ * (`toolMap`/`onUnknownNode`), toggle GFM, or add micromark/mdast extensions.
420
+ *
421
+ * Returns ALL created {@link BlockNode}s in document order — including any the
422
+ * markdown nested internally (e.g. a table's cell children), not just the
423
+ * top-level run (this differs from {@link insertTree}, which returns only the
424
+ * root). Empty or whitespace-only markdown, a dangling `parentId` (checked
425
+ * again after the async convert, so a parent removed mid-flight also no-ops),
426
+ * and a converter failure (chunk-load or parse error, swallowed) all return
427
+ * `[]` and open no transaction. The nodes are fresh-snapshot volatile — read
428
+ * them now, don't stash them in dep arrays.
429
+ */
430
+ insertMarkdown(markdown: string, options?: {
431
+ parentId?: string | null;
432
+ position?: InsertPosition;
433
+ config?: MarkdownImportConfig;
434
+ }): Promise<BlockNode[]>;
435
+ /**
436
+ * Serialize the WHOLE document to a Markdown string — the outbound twin of
437
+ * core's `blocks.exportMarkdown` (and the read-side counterpart of the
438
+ * additive {@link insertMarkdown}). Async: the serializer is lazy-loaded, like
439
+ * the importer. Markdown cannot express every block, so some structure is
440
+ * dropped (table `colspan`/`rowspan`, heading columns). Returns `''` for an
441
+ * empty document. Pre-ready: resolves to `''`.
442
+ */
443
+ exportMarkdown(): Promise<string>;
444
+ move(id: string, target: MoveTarget): void;
445
+ nest(id: string, parentId: string): void;
446
+ unnest(id: string): void;
447
+ remove(id: string): void;
448
+ /**
449
+ * Update a block's data and/or tunes by id. Delegates to core's async
450
+ * `blocks.update`, which forms its OWN undo step — the call is NOT wrapped in
451
+ * `transact` (that would close the group before the async write lands). An
452
+ * unknown id is a silent no-op; a rejected update is swallowed so it can't
453
+ * surface as an unhandled rejection. Reads refresh reactively once core emits
454
+ * 'block changed'. Returns `void`.
455
+ */
456
+ update(id: string, data?: BlockToolData, tunes?: {
457
+ [name: string]: BlockTuneData;
458
+ }): void;
459
+ /**
460
+ * Convert a block to another type ("turn into") by id. Delegates to core's
461
+ * async `blocks.convert`; both tools must provide a `conversionConfig` or core
462
+ * rejects — that rejection (and any other) is swallowed so a non-convertible
463
+ * block is a graceful no-op rather than an unhandled rejection. An unknown id
464
+ * is a silent no-op. Not wrapped in `transact` (core owns its history step).
465
+ * Returns `void`.
466
+ */
467
+ convert(id: string, newType: string, dataOverrides?: BlockToolData, options?: {
468
+ caret?: CaretTarget;
469
+ }): void;
470
+ transact(fn: () => void): void;
471
+ /**
472
+ * Run `fn` as one atomic operation that is NOT captured in the undo history —
473
+ * the React-surface counterpart of core's `transactWithoutCapture`. Use for
474
+ * silent auto-repair/normalization that a user's CMD+Z should never step
475
+ * through. Mutations inside still emit reactively. Pre-ready it just runs `fn`.
476
+ */
477
+ transactWithoutCapture(fn: () => void): void;
478
+ /**
479
+ * The current block count. Reactive (re-reads on 'block changed'). Pre-ready: 0.
480
+ */
481
+ getBlocksCount(): number;
482
+ /**
483
+ * The flat index of the block holding the caret, or -1 when none. Pre-ready: -1.
484
+ */
485
+ getCurrentBlockIndex(): number;
486
+ /** The block at a flat index as a snapshot {@link BlockNode}, or null. */
487
+ getBlockByIndex(index: number): BlockNode | null;
488
+ /**
489
+ * The absolute flat index of a block by id, or null when unknown. The
490
+ * counterpart to {@link getBlockByIndex} — use it to target an off-caret
491
+ * {@link splitBlock} (whose `insertIndex` is absolute) without the ref. Unknown
492
+ * ids return null silently (no console warn). Pre-ready: null.
493
+ */
494
+ getBlockIndex(id: string): number | null;
495
+ /**
496
+ * Read a block's current `data` and `tunes` by id WITHOUT mutating anything —
497
+ * the synchronous last-extracted view (the same snapshot clipboard ops use).
498
+ * Makes a client-side duplicate composable from the hook alone: read a node,
499
+ * then `insert({ type, data, tunes, position })`, no ref escape hatch. Unknown
500
+ * id returns null. Pre-ready: null.
501
+ */
502
+ getBlockData(id: string): {
503
+ data: BlockToolData;
504
+ tunes: {
505
+ [name: string]: BlockTuneData;
506
+ };
507
+ } | null;
508
+ /**
509
+ * The block whose holder contains/equals `element`, as a snapshot
510
+ * {@link BlockNode}, or null. Useful for mapping a DOM event target back to a
511
+ * block.
512
+ */
513
+ getBlockByElement(element: HTMLElement): BlockNode | null;
514
+ /**
515
+ * Read a tool's default empty data WITHOUT inserting anything — delegates to
516
+ * core's `composeBlockData`. Async (a tool's data may be composed lazily).
517
+ * Rejects (via core) for an unknown tool. Pre-ready: resolves to `{}`.
518
+ */
519
+ composeBlockData(toolName: string): Promise<BlockToolData>;
520
+ /**
521
+ * Replace the WHOLE document with blocks parsed from an HTML string —
522
+ * delegates to core's `renderFromHTML`. Unlike {@link insertMarkdown} (which is
523
+ * additive), this CLEARS existing content first, so it's a document-load
524
+ * primitive, not an insert. Async. Pre-ready: resolves immediately (no-op).
525
+ */
526
+ renderFromHTML(html: string): Promise<void>;
527
+ /**
528
+ * Insert a flat array of already-serialized {@link OutputBlockData} (the
529
+ * `save()` shape) directly, honoring each block's `parent`/`content` links —
530
+ * the raw counterpart of core's `blocks.insertMany`. Use to re-insert a saved
531
+ * document fragment without reshaping it into {@link TreeInsertSpec}. One
532
+ * atomic undo step. Returns the created nodes; pre-ready: `[]` (no insert).
533
+ */
534
+ insertOutputData(blocks: OutputBlockData[], options?: {
535
+ index?: number;
536
+ }): BlockNode[];
537
+ /**
538
+ * Atomically split a block: update `currentBlockId` with `currentBlockData`
539
+ * and insert a new `newBlockType` block at `insertIndex`, as ONE undo step.
540
+ * Delegates to core's `splitBlock`. Returns the new node, or null pre-ready /
541
+ * on an unknown id.
542
+ */
543
+ splitBlock(currentBlockId: string, currentBlockData: Partial<BlockToolData>, newBlockType: string, newBlockData: BlockToolData, insertIndex: number): BlockNode | null;
544
+ /**
545
+ * Insert a single child block under `parentId` at flat `insertIndex`, atomically
546
+ * (block creation AND parent assignment in ONE undo step) — delegates to core's
547
+ * `blocks.insertInsideParent`. This is the atomic nested-child creator: prefer it
548
+ * over `insert()` + `nest()`, which is TWO undo steps. A dangling `parentId` is a
549
+ * no-op returning `null` (mirrors {@link insert}); an unknown child tool returns
550
+ * `null`. `childData` defaults to an empty paragraph. Returns the created
551
+ * {@link BlockNode}, fresh-snapshot volatile — read it now, don't stash it in a
552
+ * dep array. Pre-ready: `null`.
553
+ */
554
+ insertInsideParent(parentId: string, insertIndex: number, childData?: BlockToolData): BlockNode | null;
555
+ /**
556
+ * Replace the WHOLE document with blocks parsed from saved {@link OutputData}
557
+ * (the `save()` shape) — delegates to core's `blocks.render`. Unlike
558
+ * {@link insertOutputData}/{@link insertMarkdown} (which are ADDITIVE), this
559
+ * CLEARS existing content first: a document-LOAD primitive, not an insert. The
560
+ * HTML counterpart is {@link renderFromHTML}. Async. Pre-ready: resolves
561
+ * immediately (no-op).
562
+ */
563
+ render(data: OutputData): Promise<void>;
564
+ /**
565
+ * Remove EVERY block from the document — delegates to core's `blocks.clear`. A
566
+ * document-reset primitive (pairs with {@link render}). Async. Pre-ready:
567
+ * resolves immediately (no-op).
568
+ */
569
+ clear(): Promise<void>;
570
+ /**
571
+ * Whether a Yjs sync (undo/redo) is currently in progress — the React mirror of
572
+ * core's `blocks.isSyncingFromYjs`. A METHOD (not a property) so it reads the
573
+ * LIVE flag at call time even though the api handle is memoized. Use it to skip
574
+ * cleanup that would fight Yjs state during an undo/redo. Pre-ready: `false`.
575
+ */
576
+ isSyncingFromYjs(): boolean;
577
+ }
578
+
579
+ /** One field of a block's prop schema. */
580
+ interface PropSchemaEntry {
581
+ /** Default value, used when the incoming data omits this key. */
582
+ default: unknown;
583
+ /** Optional allowed values (advisory; not enforced at runtime in v1). */
584
+ values?: readonly unknown[];
585
+ }
586
+ /**
587
+ * Declarative data shape. The keys here are EXACTLY the keys `save()` returns to
588
+ * Yjs — this closes the per-key-sync key-resurrection gap (a cleared field is
589
+ * written as its explicit default, never dropped).
590
+ */
591
+ type PropSchema = Record<string, PropSchemaEntry>;
592
+
593
+ /** Spec for {@link createAngularBlock}. Authored as a standalone component. */
594
+ interface CreateAngularBlockSpec<Data = BlockToolData> {
595
+ /** Tool type name (registered key). */
596
+ type: string;
597
+ /** Optional toolbox entry. */
598
+ toolbox?: ToolboxConfig;
599
+ /** Declarative defaults that also define the exact `save()` key set. */
600
+ propSchema: PropSchema;
601
+ /**
602
+ * The standalone Angular component to render for each block. It injects the
603
+ * per-block context via `inject(BLOK_BLOCK_CONTEXT)`.
604
+ */
605
+ component: Type<unknown>;
606
+ /** Optional lifecycle callbacks mapped from Blok's block hooks. */
607
+ onRendered?: (block: BlockAPI) => void;
608
+ onMoved?: (block: BlockAPI) => void;
609
+ onRemoved?: (block: BlockAPI) => void;
610
+ }
611
+ /**
612
+ * Author a first-party Angular block. Returns a `BlockToolConstructable`
613
+ * registered exactly like a vanilla tool (`tools: { type: { class:
614
+ * createAngularBlock(...) } }`).
615
+ *
616
+ * The factory owns the host element (`data-blok-mutation-free`), a frozen
617
+ * defaults-filled data mirror, and signals the component reads. It mounts the
618
+ * component into the host via the editor's shared portal registry (the analog of
619
+ * Vue's Teleport registry), bridging Blok's block lifecycle to Angular:
620
+ * - `render()` creates the host and registers the portal entry (mounted sync).
621
+ * - `setData()` dedups, swaps the reactive snapshot, flushes CD, resolves true.
622
+ * - `save()` returns the complete frozen mirror (never the DOM, never partial).
623
+ * - `commit()` merges a patch and fires `dispatchChange` exactly once.
624
+ * - `setReadOnly()` flips a reactive flag and flushes CD (in-place, no remount).
625
+ * - `removed()`/`destroy()` unregister the portal (deterministic unmount).
626
+ */
627
+ declare function createAngularBlock<Data = BlockToolData>(spec: CreateAngularBlockSpec<Data>): (new (options: BlockToolConstructorOptions) => {
628
+ render(): HTMLElement;
629
+ save(): BlockToolData;
630
+ setData(newData: BlockToolData): Promise<boolean>;
631
+ setReadOnly(state: boolean): void;
632
+ rendered(): void;
633
+ moved(): void;
634
+ removed(): void;
635
+ destroy(): void;
636
+ }) & {
637
+ readonly __isBlokAngularBlock: true;
638
+ readonly toolbox: ToolboxConfig | undefined;
639
+ readonly isReadOnlySupported: boolean;
640
+ };
641
+
642
+ /**
643
+ * Angular factory exposing an id/parentId-relative, reactive view of the block
644
+ * tree. Reads refresh whenever the editor emits `block changed`; mutators route
645
+ * through the editor-level `blocks` API (core's chokepoints), so undo/redo and
646
+ * Yjs sync are inherited rather than re-implemented.
647
+ *
648
+ * The block-tree logic is framework-agnostic and lives in the shared
649
+ * {@link createBlocksApiForEditor} core — the SAME implementation behind React's
650
+ * and Vue's `useBlocks`, so all three adapters expose the identical 28-method
651
+ * surface and cannot drift. This wrapper supplies only Angular's reactivity:
652
+ *
653
+ * - A private `version` signal is bumped on every `block changed`. The shared API
654
+ * is built with an `onRead` seam (`() => { version() }`) that every read method
655
+ * calls, so reading inside a `computed`/template tracks `version` and re-runs on
656
+ * each structural mutation.
657
+ * - The bound API is rebuilt (via `bindToEditor`, called eagerly on first call and
658
+ * then tracked by an `effect` for subsequent identity changes) when the editor
659
+ * IDENTITY changes; the returned facade is stable across that swap.
660
+ *
661
+ * Call it in an injection context (component constructor / field initializer),
662
+ * passing the editor signal (e.g. `BlokEditorComponent.instance` /
663
+ * `BlokContentDirective.instance`). Pre-ready (editor null) the bound API is the
664
+ * shared {@link EMPTY_API}: every mutator a no-op, reads empty/null — except
665
+ * `transact`/`transactWithoutCapture`, which still run their callback.
666
+ *
667
+ * Note: Angular 20's `effect()` is scheduled (not eager) — it does not fire
668
+ * synchronously on first call. The initial binding therefore runs synchronously
669
+ * in the `injectBlocks` body itself (mirroring Vue's `{ immediate: true }` watch),
670
+ * and the effect re-binds only on subsequent editor identity changes. When the
671
+ * effect fires for the first time with the same editor already bound, the guard
672
+ * `ed === sub.editor` makes it a no-op, so no double-subscription occurs.
673
+ *
674
+ * @param editor - a signal of the Blok instance, or null pre-ready
675
+ */
676
+ declare function injectBlocks(editor: Signal<Blok | null>): UseBlocksApi;
677
+
678
+ /** How the caller names the DOM scope to observe. */
679
+ type BlokReadyScope = (() => Element | ElementRef<Element> | null | undefined) | ElementRef<Element> | Element | null;
680
+ /** Options accepted by {@link injectBlokReady}. */
681
+ interface InjectBlokReadyOptions {
682
+ /**
683
+ * Restrict the wait to editors mounted inside this element. Accepts an
684
+ * element, an `ElementRef`, or a getter/signal returning either — the getter
685
+ * form lets you pass `() => this.scopeRef?.nativeElement` from a field
686
+ * initializer, before the view exists. It is re-read on every readiness
687
+ * change.
688
+ *
689
+ * Omit it to observe every editor on the page. Passing it while it still
690
+ * resolves to null reports NOT ready — an unresolved scope must never fall
691
+ * back to the page-global one.
692
+ */
693
+ within?: BlokReadyScope;
694
+ /**
695
+ * `'ready'` (default) settles when each editor has finished booting.
696
+ * `'rendered'` also waits for its content to be in the DOM and re-arms on
697
+ * every post-boot re-render.
698
+ */
699
+ settleOn?: 'ready' | 'rendered';
700
+ }
701
+ /**
702
+ * Angular factory exposing the live readiness of the Blok editors in a DOM
703
+ * scope as a boolean signal.
704
+ *
705
+ * The readiness logic itself is framework-agnostic and lives in core
706
+ * (`Blok.readyState` / `Blok.subscribeReady`) — the SAME implementation behind
707
+ * React's and Vue's `useBlokReady`, so the three adapters cannot drift. This
708
+ * wrapper supplies only Angular's reactivity: a signal re-read on every
709
+ * registry change, unsubscribed through the injector's `DestroyRef`.
710
+ *
711
+ * Call it in an injection context (component constructor / field initializer).
712
+ * The signal starts `false` and takes its first real reading in
713
+ * `afterNextRender` — browser-only by contract, and late enough for a
714
+ * `@ViewChild` scope to exist. Over-waiting is safe; under-waiting is a bug.
715
+ * @param options - scope and readiness depth
716
+ */
717
+ declare function injectBlokReady(options?: InjectBlokReadyOptions): Signal<boolean>;
718
+
719
+ /**
720
+ * Context handed to a `createAngularBlock` component. Delivered via DI (the
721
+ * BLOK_BLOCK_CONTEXT token) rather than @Input, because core constructs the tool
722
+ * outside Angular and the signal-input() form does not compile under the repo's
723
+ * JIT test harness. The ONLY data write path is `commit`.
724
+ */
725
+ interface AngularBlockRenderContext<Data> {
726
+ /** Reactive, FROZEN snapshot of the block data. Read `data()`; never mutate. */
727
+ data: Signal<Readonly<Data>>;
728
+ /** The ONLY data write path: merge a partial patch and sync once. */
729
+ commit: (patch: Partial<Data>) => void;
730
+ /** This block's per-block API (id, getChildren, dispatchChange…). */
731
+ block: BlockAPI;
732
+ /**
733
+ * Reactive read-only flag. Read `readOnly()` in the template to disable
734
+ * editing (drop `contenteditable`, hide controls). Toggled IN PLACE by core's
735
+ * read-only switch — the component reacts without a remount, so ephemeral
736
+ * state survives. A block that ignores it stays interactive when the editor is
737
+ * read-only (same contract as a vanilla tool's `setReadOnly`).
738
+ */
739
+ readOnly: Signal<boolean>;
740
+ /**
741
+ * Container blocks only: append this block's real child holders into `host`
742
+ * (a `data-blok-nested` element the author owns). Call it once in
743
+ * `ngAfterViewInit`; the factory re-runs the same mount on every data change
744
+ * so late-added children appear. Angular must NOT manage these child holders.
745
+ */
746
+ mountChildren: (host: HTMLElement) => void;
747
+ }
748
+ /** DI token carrying the per-block render context into the authored component. */
749
+ declare const BLOK_BLOCK_CONTEXT: InjectionToken<AngularBlockRenderContext<unknown>>;
750
+
751
+ export { BLOK_BLOCK_CONTEXT, BLOK_DEFAULT_CONFIG, BlokContentDirective, BlokEditorComponent, createAngularBlock, injectBlocks, injectBlokReady, provideBlok };
752
+ export type { AngularBlockRenderContext, BlockNode, BlokAngularConfig, BlokReadyScope, CaretTarget, CreateAngularBlockSpec, InjectBlokReadyOptions, InsertPosition, InsertSpec, MoveTarget, PropSchema, PropSchemaEntry, TreeInsertSpec, UseBlocksApi };