@bloklabs/angular 1.7.0 → 1.9.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bloklabs/angular",
3
- "version": "1.7.0",
3
+ "version": "1.9.0",
4
4
  "description": "Angular block editor — official Angular bindings for Blok, the headless Notion-like rich text editor. Standalone components that render blocks and save to JSON (APF partial-Ivy bundle).",
5
5
  "license": "Apache-2.0",
6
6
  "author": "JackUait",
@@ -36,7 +36,7 @@
36
36
  "@angular/common": ">=20 <23",
37
37
  "@angular/core": ">=20 <23",
38
38
  "@angular/forms": ">=20 <23",
39
- "@bloklabs/core": "1.7.0"
39
+ "@bloklabs/core": "1.9.0"
40
40
  },
41
41
  "module": "fesm2022/bloklabs-angular.mjs",
42
42
  "typings": "./index.d.ts",
@@ -1,7 +1,7 @@
1
1
  import * as i0 from '@angular/core';
2
- import { AfterViewInit, EventEmitter, OnDestroy, InjectionToken, EnvironmentProviders, Type, Signal, ElementRef } from '@angular/core';
2
+ import { AfterViewInit, DoCheck, EventEmitter, OnDestroy, InjectionToken, EnvironmentProviders, Type, Signal, ElementRef } from '@angular/core';
3
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';
4
+ import { BlokConfig, EditorWidth, Blok, OutputData, API, BlockMutationEvent, ResolvedTheme, BlocksRenderedPayload, BlockRenderedPayload, OutputBlockData, ThemeMode, LooseOutputData, BlockToolData, BlockTuneData, MarkdownImportConfig, BlockOrigin, BlockToolConstructable, ToolboxConfig, BlockAPI, BlockToolConstructorOptions } from '@bloklabs/core';
5
5
 
6
6
  /**
7
7
  * Configuration for the Angular adapter. Same as `BlokConfig` but without
@@ -26,15 +26,15 @@ type BlokAngularConfig = Omit<BlokConfig, 'holder'> & {
26
26
  * `styleTokens` through `editor.tokens.set()` (replace semantics),
27
27
  * `i18n` through `editor.i18n.update()` (deep-equal–deduped; seeded at
28
28
  * construction so the locale resolves during boot) and `data`
29
- * through `editor.render()` (deep-equal–deduped); everything else seeds
30
- * construction.
29
+ * through `editor.render()` (content-deduped via `equalsOutputData`); everything
30
+ * else seeds construction.
31
31
  *
32
32
  * Implementation note: classic `@Input()`/`@ViewChild()` decorators are used
33
33
  * (not signal `input()`/`viewChild()`) for JIT compatibility — see
34
34
  * `BlokContentDirective`. Each reactive input is backed by an internal `signal`
35
35
  * so effect-based syncing still works; the public template API is unchanged.
36
36
  */
37
- declare class BlokEditorComponent implements AfterViewInit, ControlValueAccessor {
37
+ declare class BlokEditorComponent implements AfterViewInit, DoCheck, ControlValueAccessor {
38
38
  private readonly ngZone;
39
39
  /** App-wide defaults from `provideBlok()`; merged UNDER per-instance inputs. */
40
40
  private readonly defaults;
@@ -124,9 +124,12 @@ declare class BlokEditorComponent implements AfterViewInit, ControlValueAccessor
124
124
  /** Changing this input's identity destroys and recreates the editor (≙ React `deps`). */
125
125
  recreateKey: unknown;
126
126
  /**
127
- * Content the editor currently reflects. Set when seeded or rendered (the data
128
- * effect below). A controlled `data` echo that deep-equals this baseline is a
129
- * no-op, so it won't clobber the caret. Renders are serialized via `renderChain`.
127
+ * Content the editor currently reflects. Set when seeded, when the data effect
128
+ * renders, when the editor emits its own output (`coreOnSave`) and when the
129
+ * imperative `render()` facade lands. A controlled `data` echo that
130
+ * content-equals this baseline is a no-op, so it won't clobber the caret.
131
+ * Renders are serialized via `renderChain`. `undefined` means "nothing recorded
132
+ * yet" — never an empty document.
130
133
  */
131
134
  private lastRenderedData?;
132
135
  /** Last token set pushed through `tokens.set`, for deep-equal deduping. */
@@ -148,8 +151,40 @@ declare class BlokEditorComponent implements AfterViewInit, ControlValueAccessor
148
151
  * it straight back into `data` deep-equals the baseline and is deduped to a
149
152
  * no-op (no redundant render, no caret reset). Re-enters the Angular zone since
150
153
  * the editor runs outside it.
154
+ *
155
+ * Falls back to an escape-hatch `[config]` callback when nothing Angular-side
156
+ * consumes the save — the baseline is recorded either way, so the controlled
157
+ * `data` dedupe works on both paths.
151
158
  */
152
159
  private readonly coreOnSave;
160
+ /**
161
+ * Stable wrappers for the remaining live callbacks. Each resolves its target
162
+ * at call time (an observed output, else the `[config]` escape hatch), so a
163
+ * pushed handler never goes stale and only PRESENCE has to be synced.
164
+ */
165
+ private readonly coreOnChange;
166
+ private readonly coreOnAfterRender;
167
+ private readonly coreOnBeforeRender;
168
+ private readonly coreOnEnter;
169
+ private readonly coreOnSubmit;
170
+ /** provideBlok defaults merged under the `[config]` escape hatch. */
171
+ private escapeHatchConfig;
172
+ /** True when an output, a two-way binding or Angular forms consumes `onSave`. */
173
+ private hasSaveConsumer;
174
+ /**
175
+ * The live callbacks this component currently wires into core, keyed by
176
+ * handler name — `undefined` for the ones nothing consumes.
177
+ *
178
+ * PRESENCE is the semantics in core (an `onSubmit` turns Enter into
179
+ * serialize-and-submit, an `onSave` arms the change-observation pipeline), so
180
+ * an unconsumed callback must stay absent. One source of truth for both the
181
+ * construction config and the runtime sync in `ngDoCheck`, so the two cannot
182
+ * drift.
183
+ * @returns the wrappers to install, `undefined` where nothing is wired
184
+ */
185
+ private liveHandlers;
186
+ /** Handler presence installed on the live editor (the sync dedupe baseline). */
187
+ private appliedHandlers;
153
188
  /** Live Blok instance, or null until `isReady` resolves / after destroy. */
154
189
  readonly instance: i0.Signal<Blok | null>;
155
190
  /** Guards `ready` to one emission per editor instance. */
@@ -161,11 +196,38 @@ declare class BlokEditorComponent implements AfterViewInit, ControlValueAccessor
161
196
  */
162
197
  buildConfig(): Partial<BlokAngularConfig>;
163
198
  ngAfterViewInit(): void;
199
+ /**
200
+ * Reactive callback presence.
201
+ *
202
+ * Callback wiring was decided once, at construction, and in core the presence
203
+ * of a handler IS the semantics: an `onSubmit` makes Enter serialize-and-submit
204
+ * instead of splitting the block, an `onSave` arms the whole change-observation
205
+ * pipeline. A `[config]` swap, an `*ngIf`-gated `(save)` output or a
206
+ * `registerOnChange` from Angular forms arriving after mount therefore needed a
207
+ * `recreateKey` bump — destroying the editor and losing caret and undo history.
208
+ *
209
+ * Diff the wired handlers against what is installed and push genuine flips
210
+ * through the runtime `handlers.set` API, writing `undefined` for a handler
211
+ * that lost its consumer so the change stays reversible. `ngDoCheck` (not an
212
+ * `effect`) because `EventEmitter.observed` and the plain `@Input` transforms
213
+ * are not signals; the comparison is six identity checks against stable
214
+ * wrappers, so an unchanged cycle pushes nothing.
215
+ */
216
+ ngDoCheck(): void;
164
217
  /** Serialize the current content. Resolves undefined until the editor is ready. */
165
218
  save$(): Promise<OutputData> | undefined;
166
219
  /** Move the caret into the editor. */
167
220
  focus(atEnd?: boolean): void;
168
- /** Replace the editor content. Resolves undefined until the editor is ready. */
221
+ /**
222
+ * Replace the editor content. Resolves undefined until the editor is ready.
223
+ *
224
+ * Safe to mix with a controlled `[data]` input: the rendered document becomes
225
+ * the content baseline once the call lands, so a later `[data]` change back to
226
+ * the previous document still re-renders instead of being deduped against a
227
+ * baseline the editor no longer reflects. Recorded only on the resolved path —
228
+ * a failed render leaves the editor on its previous content.
229
+ * @param data - the document to render
230
+ */
169
231
  render(data: OutputData | LooseOutputData): Promise<void> | undefined;
170
232
  constructor();
171
233
  /** Seeds/renders an externally-set form value through the same dedup machinery. */
@@ -597,6 +659,37 @@ interface PropSchemaEntry {
597
659
  */
598
660
  type PropSchema = Record<string, PropSchemaEntry>;
599
661
 
662
+ /**
663
+ * Every STATIC member of core's block-tool contract an Angular block may declare
664
+ * for itself — `ownsChildren`, `keepsChildrenOnEnter`, `conversionConfig`,
665
+ * `pasteConfig`, `sanitize`, `shortcut`, `upgradeData`, and whatever core adds
666
+ * next. Derived from
667
+ * `BlockToolConstructable` rather than enumerated, so a new core static needs no
668
+ * adapter change to become reachable.
669
+ *
670
+ * `toolbox` and `isReadOnlySupported` are excluded because the factory owns
671
+ * them: `toolbox` is authored as {@link CreateAngularBlockSpec.toolbox}, and
672
+ * in-place read-only support is unconditional.
673
+ */
674
+ type BlockToolStatics = Omit<BlockToolConstructable, 'toolbox' | 'isReadOnlySupported'>;
675
+ /**
676
+ * Second argument of {@link CreateAngularBlockSpec.onMounted} and
677
+ * {@link CreateAngularBlockSpec.onCreated} — everything the block cannot read
678
+ * off its own `BlockAPI`.
679
+ */
680
+ interface AngularBlockMountedContext {
681
+ /**
682
+ * Why this block instance was constructed: a CREATION origin (`user`, `api`,
683
+ * `convert`) means the author just made it, so seeding default children is
684
+ * correct; a RESTORE origin (`load`, `replay`, `paste`) means the document
685
+ * already says what the children are. `probe` is an off-tree instance built
686
+ * only to read a tool's default data — it must not touch the block tree at
687
+ * all. Defaults to `'api'` when the constructor was handed no origin.
688
+ */
689
+ origin: BlockOrigin;
690
+ /** The editor-level API (`api.blocks`, `api.caret`, `api.events`…). */
691
+ api: API;
692
+ }
600
693
  /** Spec for {@link createAngularBlock}. Authored as a standalone component. */
601
694
  interface CreateAngularBlockSpec<Data = BlockToolData> {
602
695
  /** Tool type name (registered key). */
@@ -610,8 +703,90 @@ interface CreateAngularBlockSpec<Data = BlockToolData> {
610
703
  * per-block context via `inject(BLOK_BLOCK_CONTEXT)`.
611
704
  */
612
705
  component: Type<unknown>;
706
+ /**
707
+ * Static members of core's tool contract, forwarded verbatim onto the
708
+ * generated tool class — the single channel for everything core reads off the
709
+ * CLASS rather than the instance (`ownsChildren`, `keepsChildrenOnEnter`,
710
+ * `conversionConfig`, `pasteConfig`, `sanitize`, `shortcut`, `upgradeData`…).
711
+ * Without it the only way to declare one was to subclass the generated class.
712
+ *
713
+ * `keepsChildrenOnEnter` is the per-tool Enter POLICY: declare it and Enter on
714
+ * this container's empty LAST child creates the new line INSIDE the container
715
+ * instead of escaping to the container's parent (Blok's default, which is
716
+ * Notion's callout behaviour). Core cannot read that off the DOM — a callout
717
+ * renders the same `data-blok-nested-blocks` slot as a column yet wants the
718
+ * escape — so before it existed a layout container (a card, a `steps` block)
719
+ * had to hijack the editor-global `config.onEnter` and re-derive containment.
720
+ *
721
+ * `toolbox` and `isReadOnlySupported` are owned by the factory and cannot be
722
+ * overridden here (see {@link BlockToolStatics}).
723
+ */
724
+ statics?: BlockToolStatics;
725
+ /**
726
+ * The element the +/drag toolbar should vertically center on — core's
727
+ * `getToolbarAnchorElement` hook, resolved against this block's host element
728
+ * on every call (never cached, so it tracks re-renders).
729
+ *
730
+ * A container block whose own chrome is not editable needs it: with no anchor,
731
+ * core centers the toolbar on the first `[contenteditable]` under the host,
732
+ * which for a container is its FIRST CHILD BLOCK. Return `null`/`undefined`
733
+ * (or omit the field) to keep core's default.
734
+ * @param host - this block's mutation-free host element
735
+ * @param block - this block's per-block API
736
+ */
737
+ getToolbarAnchorElement?: (host: HTMLElement, block: BlockAPI) => HTMLElement | null | undefined;
613
738
  /** Optional lifecycle callbacks mapped from Blok's block hooks. */
614
739
  onRendered?: (block: BlockAPI) => void;
740
+ /**
741
+ * Fired ONCE per block instance, once the component's DOM (and, for a
742
+ * container, the child holders its `ctx.mountChildren` adopted) exists.
743
+ * Angular mounts the block synchronously while core is still inside
744
+ * `render()`, so this lands with `rendered()` — the first hook at which the
745
+ * host is also in the document. The React and Vue adapters spell the same
746
+ * contract; there it is genuinely LATER than `onRendered`, because their
747
+ * portals commit a frame after core returns.
748
+ *
749
+ * It is also the create-vs-restore signal: `context.origin` says whether the
750
+ * author just made this block (`user`/`api`/`convert`) or the document is
751
+ * being re-materialised (`load`/`replay`/`paste`) — so a container can seed
752
+ * its default children here exactly once, without the "children are
753
+ * transiently empty during a replay" trap.
754
+ * @example
755
+ * ```ts
756
+ * onMounted: (block, { origin, api }) => {
757
+ * if (origin === 'user' && block.getChildren().length === 0) {
758
+ * api.blocks.insertInsideParent(block.id);
759
+ * }
760
+ * }
761
+ * ```
762
+ */
763
+ onMounted?: (block: BlockAPI, context: AngularBlockMountedContext) => void;
764
+ /**
765
+ * The SEEDING hook: `onMounted`, narrowed to a genuine creation. Fired ONCE
766
+ * per block instance, once the component's DOM exists, and only when this
767
+ * instance is the author making a new block (`origin` of `user`, `api` or
768
+ * `convert`) — never for a `load`/`replay`/`paste` restore, and never for the
769
+ * off-tree `probe` instance core builds to read a tool's default data.
770
+ *
771
+ * That predicate is why the hook exists rather than leaving every block to
772
+ * read `context.origin` in `onMounted`: the intuitive `origin === 'user'` test
773
+ * is wrong. It drops `api.blocks.insert('steps')` and turn-into, so a
774
+ * container seeded that way comes up empty for every path except a keystroke.
775
+ * Core refused to ship that axis into its own `column`/`column_list`; this
776
+ * encodes the correct one once, here.
777
+ *
778
+ * The `context` is the same object {@link CreateAngularBlockSpec.onMounted}
779
+ * receives, so a block that only seeds can read `origin` for finer decisions.
780
+ * @example
781
+ * ```ts
782
+ * onCreated: (block, { api }) => {
783
+ * if (block.getChildren().length === 0) {
784
+ * api.blocks.insertInsideParent(block.id);
785
+ * }
786
+ * }
787
+ * ```
788
+ */
789
+ onCreated?: (block: BlockAPI, context: AngularBlockMountedContext) => void;
615
790
  onMoved?: (block: BlockAPI) => void;
616
791
  onRemoved?: (block: BlockAPI) => void;
617
792
  }
@@ -636,11 +811,12 @@ declare function createAngularBlock<Data = BlockToolData>(spec: CreateAngularBlo
636
811
  save(): BlockToolData;
637
812
  setData(newData: BlockToolData): Promise<boolean>;
638
813
  setReadOnly(state: boolean): void;
814
+ getToolbarAnchorElement(): HTMLElement | undefined;
639
815
  rendered(): void;
640
816
  moved(): void;
641
817
  removed(): void;
642
818
  destroy(): void;
643
- }) & {
819
+ }) & BlockToolStatics & {
644
820
  readonly __isBlokAngularBlock: true;
645
821
  readonly toolbox: ToolboxConfig | undefined;
646
822
  readonly isReadOnlySupported: boolean;
@@ -723,6 +899,14 @@ interface InjectBlokReadyOptions {
723
899
  */
724
900
  declare function injectBlokReady(options?: InjectBlokReadyOptions): Signal<boolean>;
725
901
 
902
+ /**
903
+ * One child's decoration: attribute name → value. `null`/`undefined` removes the
904
+ * attribute; a boolean or number is stringified (so `false` writes
905
+ * `data-active="false"`, which CSS can select, rather than dropping the hook).
906
+ */
907
+ type ChildAttributes = Record<string, string | number | boolean | null | undefined>;
908
+ /** The per-child decorator accepted by {@link AngularBlockRenderContext.mountChildren}. */
909
+ type ChildAttributesFn = (child: BlockAPI, index: number) => ChildAttributes;
726
910
  /**
727
911
  * Context handed to a `createAngularBlock` component. Delivered via DI (the
728
912
  * BLOK_BLOCK_CONTEXT token) rather than @Input, because core constructs the tool
@@ -736,6 +920,17 @@ interface AngularBlockRenderContext<Data> {
736
920
  commit: (patch: Partial<Data>) => void;
737
921
  /** This block's per-block API (id, getChildren, dispatchChange…). */
738
922
  block: BlockAPI;
923
+ /**
924
+ * The EDITOR-level API this block belongs to (`api.blocks`, `api.caret`,
925
+ * `api.toolbar`…) — the same object a vanilla tool receives in its
926
+ * constructor. Reach for it when a block has to drive the document around it
927
+ * instead of routing everything through `block.call()` string dispatch.
928
+ *
929
+ * For the reactive, id/parentId-relative view of the tree (and to re-render
930
+ * when your own children change), pair `injectBlocks` with
931
+ * `injectBlokInstance()` instead — the api handle itself is not reactive.
932
+ */
933
+ api: API;
739
934
  /**
740
935
  * Reactive read-only flag. Read `readOnly()` in the template to disable
741
936
  * editing (drop `contenteditable`, hide controls). Toggled IN PLACE by core's
@@ -749,11 +944,57 @@ interface AngularBlockRenderContext<Data> {
749
944
  * (a `data-blok-nested` element the author owns). Call it once in
750
945
  * `ngAfterViewInit`; the factory re-runs the same mount on every data change
751
946
  * so late-added children appear. Angular must NOT manage these child holders.
947
+ *
948
+ * `childAttributes` decorates each child's HOLDER after the holders are
949
+ * mounted, and is REMEMBERED — every later remount re-applies it. Named hooks
950
+ * (`data-step-index`, `data-active`…) replace positional `:nth-child()` CSS
951
+ * over Blok's holders, which breaks the moment a child is inserted, removed or
952
+ * reordered. The holders stay DIRECT children of `host` — core requires that
953
+ * (hierarchy reparenting and caret navigation compare `holder.parentElement`
954
+ * by identity), so decoration is attributes, never wrapper elements.
955
+ * Attributes the callback stops producing are removed on the next pass.
956
+ *
957
+ * Writing on a child's holder is inert by design: core's mutation filter drops
958
+ * a holder-targeted attribute record for the child block and suppresses it for
959
+ * the container. The guarantee stops at the holder and its
960
+ * `[data-blok-element-content]` wrapper — writing AT or BELOW a child's tool
961
+ * root DOES score as that child's edit.
752
962
  */
753
- mountChildren: (host: HTMLElement) => void;
963
+ mountChildren: (host: HTMLElement, childAttributes?: ChildAttributesFn) => void;
754
964
  }
755
965
  /** DI token carrying the per-block render context into the authored component. */
756
966
  declare const BLOK_BLOCK_CONTEXT: InjectionToken<AngularBlockRenderContext<unknown>>;
757
967
 
758
- export { BLOK_BLOCK_CONTEXT, BLOK_DEFAULT_CONFIG, BlokContentDirective, BlokEditorComponent, createAngularBlock, injectBlocks, injectBlokReady, provideBlok };
759
- export type { AngularBlockRenderContext, BlockNode, BlokAngularConfig, BlokReadyScope, CaretTarget, CreateAngularBlockSpec, InjectBlokReadyOptions, InsertPosition, InsertSpec, MoveTarget, PropSchema, PropSchemaEntry, TreeInsertSpec, UseBlocksApi };
968
+ /**
969
+ * DI token carrying the LIVE editor instance (as a signal) into every block
970
+ * mounted by `createAngularBlock`. `BlokContentDirective` publishes its own
971
+ * `instance` signal through it, so the value is null before the editor is ready
972
+ * and after teardown — matching the React/Vue adapters' pre-ready contract.
973
+ *
974
+ * Provided on the block's ELEMENT injector by the portal registry, so it is
975
+ * per-EDITOR: two editors on one page each publish their own instance.
976
+ */
977
+ declare const BLOK_EDITOR_INSTANCE: InjectionToken<Signal<Blok | null>>;
978
+ /**
979
+ * The live Blok instance the component is mounted inside, as a signal, or a
980
+ * signal of null before it exists. Inside a `createAngularBlock` component this
981
+ * is the block's OWN editor — so a block can drive the tree it lives in without
982
+ * the host prop-drilling the instance into it:
983
+ *
984
+ * ```ts
985
+ * private readonly editor = injectBlokInstance();
986
+ * private readonly blocks = injectBlocks(this.editor);
987
+ * ```
988
+ *
989
+ * That pairing is also what makes a container block REACTIVE to its own child
990
+ * tree: `injectBlocks` refreshes on the editor's `block changed` event, which
991
+ * core emits for every structural mutation — including children the adapter
992
+ * itself never sees (a pasted paragraph, a Tab-indent from the keyboard).
993
+ *
994
+ * Call it in an injection context (a field initializer or the constructor).
995
+ * Outside an editor it returns a signal of null.
996
+ */
997
+ declare function injectBlokInstance(): Signal<Blok | null>;
998
+
999
+ export { BLOK_BLOCK_CONTEXT, BLOK_DEFAULT_CONFIG, BLOK_EDITOR_INSTANCE, BlokContentDirective, BlokEditorComponent, createAngularBlock, injectBlocks, injectBlokInstance, injectBlokReady, provideBlok };
1000
+ export type { AngularBlockMountedContext, AngularBlockRenderContext, BlockNode, BlockToolStatics, BlokAngularConfig, BlokReadyScope, CaretTarget, ChildAttributes, ChildAttributesFn, CreateAngularBlockSpec, InjectBlokReadyOptions, InsertPosition, InsertSpec, MoveTarget, PropSchema, PropSchemaEntry, TreeInsertSpec, UseBlocksApi };