@bloklabs/angular 1.3.0 → 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.
package/README.md ADDED
@@ -0,0 +1,65 @@
1
+ # @bloklabs/angular
2
+
3
+ The Angular block editor bindings for [Blok](https://blokeditor.com) — a headless, Notion-like rich text editor whose content is JSON blocks, not HTML.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @bloklabs/angular @bloklabs/core
9
+ ```
10
+
11
+ `@bloklabs/core` and `@angular/core` / `@angular/common` / `@angular/forms` are peer dependencies.
12
+
13
+ ## Usage
14
+
15
+ `BlokEditorComponent` is a standalone component: it mounts the editor, manages its lifecycle, and takes every editor config option as an input.
16
+
17
+ ```typescript
18
+ import { Component } from '@angular/core';
19
+ import { BlokEditorComponent } from '@bloklabs/angular';
20
+ import { Header, Paragraph, List } from '@bloklabs/core/tools';
21
+
22
+ @Component({
23
+ selector: 'app-editor',
24
+ standalone: true,
25
+ imports: [BlokEditorComponent],
26
+ template: `<blok-editor [tools]="tools" />`,
27
+ })
28
+ export class EditorComponent {
29
+ tools = {
30
+ paragraph: Paragraph,
31
+ header: { class: Header, placeholder: 'Enter a heading' },
32
+ list: List,
33
+ };
34
+ }
35
+ ```
36
+
37
+ ## Saving
38
+
39
+ The `(save)` output fires with the latest content on every change, so you never have to call `save()` by hand.
40
+
41
+ ```typescript
42
+ import type { OutputData } from '@bloklabs/core';
43
+
44
+ // template: <blok-editor [tools]="tools" (save)="onSave($event)" />
45
+ onSave(data: OutputData) {
46
+ console.log(data.blocks);
47
+ }
48
+ ```
49
+
50
+ The component also implements `ControlValueAccessor`, so it works with `[(ngModel)]` and reactive forms.
51
+
52
+ ## Also exported
53
+
54
+ - `BlokContentDirective` (`[blokContent]`) — the mount point, if you drive the editor yourself.
55
+ - `injectBlocks()` — a reactive snapshot of the block tree plus a manipulation API.
56
+ - `createAngularBlock` — author block tools as Angular components.
57
+ - `provideBlok` / `BLOK_DEFAULT_CONFIG` — share default editor config through DI.
58
+
59
+ ## Docs
60
+
61
+ - [Quick start](https://blokeditor.com/docs/quick-start?framework=angular)
62
+ - [`BlokEditor` reference](https://blokeditor.com/docs/blok-editor?framework=angular)
63
+ - [`useBlocks` reference](https://blokeditor.com/docs/use-blocks?framework=angular)
64
+
65
+ Licensed under Apache-2.0.
@@ -1754,9 +1754,15 @@ const DATA_ATTR = {
1754
1754
  /** Editor or element is empty */
1755
1755
  empty: 'data-blok-empty',
1756
1756
  /** Present on the editor wrapper while read-only mode is active.
1757
- * Public styling hook — lets hosts (and Blok's own CSS, e.g. the
1758
- * gutter auto-collapse) key rules off the editing state without JS. */
1757
+ * Public styling hook — lets hosts key rules off the editing state
1758
+ * without JS. Deliberately does NOT collapse the gutter: plain
1759
+ * read-only still shows the block-hover copy-link control there, and
1760
+ * in-place readOnly.set() flips must not shift the layout. */
1759
1761
  readonly: 'data-blok-readonly',
1762
+ /** Present on the editor wrapper while read-only mode hides ALL editor
1763
+ * controls (readOnly: { hideControls: true }). Public styling hook —
1764
+ * drives the gutter auto-collapse for genuinely chromeless read-only. */
1765
+ controlsHidden: 'data-blok-controls-hidden',
1760
1766
  /** Present on the editor wrapper when config.hideToolbar is true.
1761
1767
  * Public styling hook — drives the gutter auto-collapse (the gutter
1762
1768
  * exists solely to house the toolbar's +/⠿ controls). */
@@ -2067,8 +2073,16 @@ const mountChildBlocks = (container, children) => {
2067
2073
  * `BlokEditor`). Delegates instance lifecycle to an internal `BlokContentDirective`
2068
2074
  * and layers the typed reactive input/output API on top.
2069
2075
  *
2070
- * Reactive inputs (`readOnly`, `theme`, `width`, `placeholder`, `autofocus`) are
2071
- * synced in place after mount via effects; everything else seeds construction.
2076
+ * Reactive inputs (`readOnly`, `hideToolbar`, `inlineToolbar`, `theme`,
2077
+ * `width`, `placeholder`, `autofocus`, `styleTokens`, `i18n`, `data`) are
2078
+ * synced in place after mount via effects — `hideToolbar` through
2079
+ * `editor.toolbar.setHidden()`, `inlineToolbar` through
2080
+ * `editor.tools.setInlineToolbar()` (content-compared),
2081
+ * `styleTokens` through `editor.tokens.set()` (replace semantics),
2082
+ * `i18n` through `editor.i18n.update()` (deep-equal–deduped; seeded at
2083
+ * construction so the locale resolves during boot) and `data`
2084
+ * through `editor.render()` (deep-equal–deduped); everything else seeds
2085
+ * construction.
2072
2086
  *
2073
2087
  * Implementation note: classic `@Input()`/`@ViewChild()` decorators are used
2074
2088
  * (not signal `input()`/`viewChild()`) for JIT compatibility — see
@@ -2117,11 +2131,24 @@ class BlokEditorComponent {
2117
2131
  // ---- Reactive inputs (signal-backed setters; synced in place, never recreate) ----
2118
2132
  // Default undefined (not false) so an unset input falls back to a provideBlok
2119
2133
  // default; the readOnly effect / buildConfig coerce undefined to false.
2134
+ // Accepts the object form (`{ hideControls }`) too — it flows through
2135
+ // buildConfig unchanged and the effect expands it to
2136
+ // `readOnly.set(enabled, { hideControls })`.
2120
2137
  readOnly$ = signal(undefined, /* @ts-ignore */
2121
2138
  ...(ngDevMode ? [{ debugName: "readOnly$" }] : /* istanbul ignore next */ []));
2122
2139
  set readOnly(value) {
2123
2140
  this.readOnly$.set(value);
2124
2141
  }
2142
+ hideToolbar$ = signal(undefined, /* @ts-ignore */
2143
+ ...(ngDevMode ? [{ debugName: "hideToolbar$" }] : /* istanbul ignore next */ []));
2144
+ set hideToolbar(value) {
2145
+ this.hideToolbar$.set(value);
2146
+ }
2147
+ inlineToolbar$ = signal(undefined, /* @ts-ignore */
2148
+ ...(ngDevMode ? [{ debugName: "inlineToolbar$" }] : /* istanbul ignore next */ []));
2149
+ set inlineToolbar(value) {
2150
+ this.inlineToolbar$.set(value);
2151
+ }
2125
2152
  theme$ = signal(undefined, /* @ts-ignore */
2126
2153
  ...(ngDevMode ? [{ debugName: "theme$" }] : /* istanbul ignore next */ []));
2127
2154
  set theme(value) {
@@ -2147,6 +2174,18 @@ class BlokEditorComponent {
2147
2174
  set styleTokens(value) {
2148
2175
  this.styleTokens$.set(value);
2149
2176
  }
2177
+ /**
2178
+ * Locale, host message overrides and text direction. `config.i18n` was
2179
+ * consumed once at boot, so a host driving a language switcher had to
2180
+ * recreate the editor (losing caret, focus and undo stack) to relabel the
2181
+ * UI; this drives the runtime `i18n.update` API instead. `defaultLocale` is
2182
+ * not forwarded — it only affects the INITIAL locale resolution.
2183
+ */
2184
+ i18n$ = signal(undefined, /* @ts-ignore */
2185
+ ...(ngDevMode ? [{ debugName: "i18n$" }] : /* istanbul ignore next */ []));
2186
+ set i18n(value) {
2187
+ this.i18n$.set(value);
2188
+ }
2150
2189
  autofocus$ = signal(false, /* @ts-ignore */
2151
2190
  ...(ngDevMode ? [{ debugName: "autofocus$" }] : /* istanbul ignore next */ []));
2152
2191
  set autofocus(value) {
@@ -2162,7 +2201,7 @@ class BlokEditorComponent {
2162
2201
  tools;
2163
2202
  /**
2164
2203
  * Escape hatch: a full config object for keys without a dedicated input
2165
- * (i18n, sanitizer, minHeight, inlineToolbar, …). Layered between provideBlok
2204
+ * (sanitizer, minHeight, …). Layered between provideBlok
2166
2205
  * defaults and the discrete inputs.
2167
2206
  */
2168
2207
  config;
@@ -2176,6 +2215,12 @@ class BlokEditorComponent {
2176
2215
  lastRenderedData;
2177
2216
  /** Last token set pushed through `tokens.set`, for deep-equal deduping. */
2178
2217
  appliedTokens;
2218
+ /** Last i18n config pushed through `i18n.update`, for deep-equal deduping. */
2219
+ appliedI18n;
2220
+ /** Editor instance the i18n baseline above belongs to. */
2221
+ i18nAppliedFor = null;
2222
+ /** Last value pushed through `tools.setInlineToolbar`, for content-compare deduping. */
2223
+ appliedInlineToolbar;
2179
2224
  seededEditor = null;
2180
2225
  renderChain = Promise.resolve();
2181
2226
  /** Registered by `ControlValueAccessor.registerOnChange` (Angular forms). */
@@ -2210,7 +2255,17 @@ class BlokEditorComponent {
2210
2255
  buildConfig() {
2211
2256
  // Precedence: provideBlok defaults < [config] escape hatch < discrete inputs.
2212
2257
  const cfg = { ...this.defaults, ...this.config };
2258
+ // The object form ({ hideControls }) is preserved as-is; the readOnly
2259
+ // effect expands it into `readOnly.set(enabled, { hideControls })`.
2213
2260
  cfg.readOnly = this.readOnly$() ?? this.config?.readOnly ?? this.defaults.readOnly ?? false;
2261
+ const hideToolbar = this.hideToolbar$();
2262
+ if (hideToolbar !== undefined) {
2263
+ cfg.hideToolbar = hideToolbar;
2264
+ }
2265
+ const inlineToolbar = this.inlineToolbar$();
2266
+ if (inlineToolbar !== undefined) {
2267
+ cfg.inlineToolbar = inlineToolbar;
2268
+ }
2214
2269
  // tools registries are MERGED (not replaced) across all three layers so a
2215
2270
  // shared registry composes with per-instance additions.
2216
2271
  if (this.tools !== undefined ||
@@ -2234,6 +2289,12 @@ class BlokEditorComponent {
2234
2289
  if (placeholder !== undefined) {
2235
2290
  cfg.placeholder = placeholder;
2236
2291
  }
2292
+ // Seeded at construction (not applied by the i18n effect) so the locale is
2293
+ // resolved during boot instead of flashing the default language first.
2294
+ const i18n = this.i18n$();
2295
+ if (i18n !== undefined) {
2296
+ cfg.i18n = i18n;
2297
+ }
2237
2298
  // Opt-in: only attach each core callback when the consumer actually consumes
2238
2299
  // it (an observed output, a registered forms hook, or a provided transform).
2239
2300
  // Their mere presence makes the core serialize / run hooks on every change.
@@ -2282,11 +2343,39 @@ class BlokEditorComponent {
2282
2343
  // (the Angular analog of React's `editor` effect-dependency).
2283
2344
  effect(() => {
2284
2345
  const editor = this.instance();
2285
- const readOnly = this.readOnly$();
2346
+ const readOnly = this.readOnly$() ?? this.config?.readOnly;
2286
2347
  if (editor) {
2287
- void editor.readOnly.set(readOnly ?? normalizeReadOnlyConfig(this.config?.readOnly).enabled);
2348
+ const { enabled, hideControls } = normalizeReadOnlyConfig(readOnly);
2349
+ // Only the object form carries options; the boolean form keeps the
2350
+ // one-argument call so it never clobbers a config-seeded hideControls.
2351
+ if (typeof readOnly === 'object' && readOnly !== null) {
2352
+ void editor.readOnly.set(enabled, { hideControls });
2353
+ }
2354
+ else {
2355
+ void editor.readOnly.set(enabled);
2356
+ }
2357
+ }
2358
+ });
2359
+ effect(() => {
2360
+ const editor = this.instance();
2361
+ const hideToolbar = this.hideToolbar$();
2362
+ if (editor && hideToolbar !== undefined) {
2363
+ editor.toolbar.setHidden(hideToolbar);
2288
2364
  }
2289
2365
  });
2366
+ // Content-compared (not identity): a new array with the same tool names is
2367
+ // a no-op, so template-inline array literals don't thrash the assignment.
2368
+ effect(() => {
2369
+ const editor = this.instance();
2370
+ const inlineToolbar = this.inlineToolbar$();
2371
+ if (!editor ||
2372
+ inlineToolbar === undefined ||
2373
+ deepEqual(inlineToolbar, this.appliedInlineToolbar)) {
2374
+ return;
2375
+ }
2376
+ this.appliedInlineToolbar = Array.isArray(inlineToolbar) ? [...inlineToolbar] : inlineToolbar;
2377
+ editor.tools.setInlineToolbar(inlineToolbar);
2378
+ });
2290
2379
  effect(() => {
2291
2380
  const editor = this.instance();
2292
2381
  const theme = this.theme$();
@@ -2317,6 +2406,34 @@ class BlokEditorComponent {
2317
2406
  this.appliedTokens = { ...tokens };
2318
2407
  editor.tokens.set(tokens);
2319
2408
  });
2409
+ effect(() => {
2410
+ const editor = this.instance();
2411
+ const i18n = this.i18n$() ?? this.config?.i18n;
2412
+ if (!editor) {
2413
+ return;
2414
+ }
2415
+ /*
2416
+ * A freshly constructed editor already resolved this config during its
2417
+ * own boot (buildConfig seeds it), so record the baseline and push
2418
+ * nothing — re-applying it here would reload the locale after mount and
2419
+ * flash the default language.
2420
+ */
2421
+ if (this.i18nAppliedFor !== editor) {
2422
+ this.i18nAppliedFor = editor;
2423
+ this.appliedI18n = i18n === undefined ? undefined : { ...i18n };
2424
+ return;
2425
+ }
2426
+ if (i18n === undefined || deepEqual(i18n, this.appliedI18n)) {
2427
+ return;
2428
+ }
2429
+ this.appliedI18n = { ...i18n };
2430
+ const { locale, messages, direction } = i18n;
2431
+ void editor.i18n.update({
2432
+ ...(locale === undefined ? {} : { locale }),
2433
+ ...(messages === undefined ? {} : { messages }),
2434
+ ...(direction === undefined ? {} : { direction }),
2435
+ });
2436
+ });
2320
2437
  effect(() => {
2321
2438
  const editor = this.instance();
2322
2439
  if (editor && this.autofocus$()) {
@@ -2397,7 +2514,7 @@ class BlokEditorComponent {
2397
2514
  this.readOnly$.set(isDisabled);
2398
2515
  }
2399
2516
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: BlokEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2400
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.0.7", type: BlokEditorComponent, isStandalone: true, selector: "blok-editor", inputs: { onBeforeRender: "onBeforeRender", onBeforePaste: "onBeforePaste", readOnly: "readOnly", theme: "theme", width: "width", placeholder: "placeholder", styleTokens: "styleTokens", autofocus: "autofocus", data: "data", tools: "tools", config: "config", recreateKey: "recreateKey" }, outputs: { ready: "ready", dataChange: "dataChange", save: "save", change: "change", afterRender: "afterRender", themeChange: "themeChange", blocksRendered: "blocksRendered", blockRendered: "blockRendered" }, providers: [
2517
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.0.7", type: BlokEditorComponent, isStandalone: true, selector: "blok-editor", inputs: { onBeforeRender: "onBeforeRender", onBeforePaste: "onBeforePaste", readOnly: "readOnly", hideToolbar: "hideToolbar", inlineToolbar: "inlineToolbar", theme: "theme", width: "width", placeholder: "placeholder", styleTokens: "styleTokens", i18n: "i18n", autofocus: "autofocus", data: "data", tools: "tools", config: "config", recreateKey: "recreateKey" }, outputs: { ready: "ready", dataChange: "dataChange", save: "save", change: "change", afterRender: "afterRender", themeChange: "themeChange", blocksRendered: "blocksRendered", blockRendered: "blockRendered" }, providers: [
2401
2518
  {
2402
2519
  provide: NG_VALUE_ACCESSOR,
2403
2520
  useExisting: forwardRef(() => BlokEditorComponent),
@@ -2447,6 +2564,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
2447
2564
  type: Input
2448
2565
  }], readOnly: [{
2449
2566
  type: Input
2567
+ }], hideToolbar: [{
2568
+ type: Input
2569
+ }], inlineToolbar: [{
2570
+ type: Input
2450
2571
  }], theme: [{
2451
2572
  type: Input
2452
2573
  }], width: [{
@@ -2455,6 +2576,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
2455
2576
  type: Input
2456
2577
  }], styleTokens: [{
2457
2578
  type: Input
2579
+ }], i18n: [{
2580
+ type: Input
2458
2581
  }], autofocus: [{
2459
2582
  type: Input
2460
2583
  }], data: [{
@@ -2786,9 +2909,58 @@ function injectBlocks(editor) {
2786
2909
  };
2787
2910
  }
2788
2911
 
2912
+ // packages/angular/src/useBlokReady.ts
2913
+ /**
2914
+ * Resolves the caller's scope description to an element, or null when it is
2915
+ * not available yet.
2916
+ * @param within - the scope description passed in options
2917
+ */
2918
+ function resolveScope(within) {
2919
+ const value = typeof within === 'function' ? within() : within;
2920
+ if (value === null || value === undefined) {
2921
+ return null;
2922
+ }
2923
+ return value instanceof ElementRef ? value.nativeElement : value;
2924
+ }
2925
+ /**
2926
+ * Angular factory exposing the live readiness of the Blok editors in a DOM
2927
+ * scope as a boolean signal.
2928
+ *
2929
+ * The readiness logic itself is framework-agnostic and lives in core
2930
+ * (`Blok.readyState` / `Blok.subscribeReady`) — the SAME implementation behind
2931
+ * React's and Vue's `useBlokReady`, so the three adapters cannot drift. This
2932
+ * wrapper supplies only Angular's reactivity: a signal re-read on every
2933
+ * registry change, unsubscribed through the injector's `DestroyRef`.
2934
+ *
2935
+ * Call it in an injection context (component constructor / field initializer).
2936
+ * The signal starts `false` and takes its first real reading in
2937
+ * `afterNextRender` — browser-only by contract, and late enough for a
2938
+ * `@ViewChild` scope to exist. Over-waiting is safe; under-waiting is a bug.
2939
+ * @param options - scope and readiness depth
2940
+ */
2941
+ function injectBlokReady(options = {}) {
2942
+ const { within, settleOn } = options;
2943
+ const ready = signal(false, /* @ts-ignore */
2944
+ ...(ngDevMode ? [{ debugName: "ready" }] : /* istanbul ignore next */ []));
2945
+ const read = () => {
2946
+ const scope = within === undefined ? null : resolveScope(within);
2947
+ // A scope that was asked for but has not resolved yet is NOT the global
2948
+ // scope — reporting ready here would gate on the wrong set of editors.
2949
+ ready.set(within !== undefined && scope === null
2950
+ ? false
2951
+ : Blok.readyState({
2952
+ within: scope,
2953
+ settleOn,
2954
+ }).ready);
2955
+ };
2956
+ inject(DestroyRef).onDestroy(Blok.subscribeReady(read));
2957
+ afterNextRender(read);
2958
+ return ready.asReadonly();
2959
+ }
2960
+
2789
2961
  /**
2790
2962
  * Generated bundle index. Do not edit.
2791
2963
  */
2792
2964
 
2793
- export { BLOK_BLOCK_CONTEXT, BLOK_DEFAULT_CONFIG, BlokContentDirective, BlokEditorComponent, createAngularBlock, injectBlocks, provideBlok };
2965
+ export { BLOK_BLOCK_CONTEXT, BLOK_DEFAULT_CONFIG, BlokContentDirective, BlokEditorComponent, createAngularBlock, injectBlocks, injectBlokReady, provideBlok };
2794
2966
  //# sourceMappingURL=bloklabs-angular.mjs.map