@bloklabs/react 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/react",
3
- "version": "1.7.0",
3
+ "version": "1.9.0",
4
4
  "description": "React block editor — official React bindings for Blok, the headless Notion-like rich text editor. Hooks and components that render blocks and save to JSON.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "JackUait",
@@ -54,7 +54,7 @@
54
54
  "test": "yarn run -T vitest run"
55
55
  },
56
56
  "peerDependencies": {
57
- "@bloklabs/core": "^1.7.0",
57
+ "@bloklabs/core": "^1.9.0",
58
58
  "react": "^18.0.0 || ^19.0.0",
59
59
  "react-dom": "^18.0.0 || ^19.0.0"
60
60
  }
@@ -2,6 +2,7 @@ import { useEffect, useRef, forwardRef } from 'react';
2
2
  import { getHolder } from './holder-map';
3
3
  import { getRegistry } from './registry-map';
4
4
  import { BlockPortalHost } from './BlockPortalHost';
5
+ import { BlokInstanceContext } from './blok-instance-context';
5
6
  import type { Blok } from '@/types';
6
7
  import type { BlokContentProps } from './types';
7
8
 
@@ -91,11 +92,20 @@ export const BlokContent = forwardRef<HTMLDivElement, BlokContentProps>(
91
92
  // component tree (via createPortal into each Blok-owned host element), so
92
93
  // app-level React context flows into block tools with no bridge. It emits
93
94
  // no DOM at this position, leaving the imperatively-adopted holder alone.
95
+ //
96
+ // The instance provider rides along for the same reason: this is the one
97
+ // place that holds BOTH the live editor and the portal host, so publishing
98
+ // it here is what lets a block component call `useBlocks(useBlokInstance())`
99
+ // without the host prop-drilling the instance into every block.
94
100
  const registry = editor === null ? undefined : getRegistry(editor);
95
101
 
96
102
  return (
97
103
  <div ref={setRefs} {...divProps}>
98
- {registry === undefined ? null : <BlockPortalHost registry={registry} />}
104
+ {registry === undefined ? null : (
105
+ <BlokInstanceContext.Provider value={editor}>
106
+ <BlockPortalHost registry={registry} />
107
+ </BlokInstanceContext.Provider>
108
+ )}
99
109
  </div>
100
110
  );
101
111
  }
package/src/BlokView.tsx CHANGED
@@ -24,6 +24,8 @@ export interface BlokViewProps extends Omit<HTMLAttributes<HTMLDivElement>, 'chi
24
24
  blockIds?: BlocksToHtmlOptions['blockIds'];
25
25
  /** Pure URL rewrite hook for block URLs + inline anchors, run before the unsafe-scheme strip. */
26
26
  transformUrl?: BlocksToHtmlOptions['transformUrl'];
27
+ /** Custom renderers for inline elements, keyed by lowercase tag name (equations, mentions). */
28
+ inlineRenderers?: BlocksToHtmlOptions['inlineRenderers'];
27
29
  /**
28
30
  * Render with the editor's presentational classes and per-block scaffolding
29
31
  * so the output matches a read-only editor render (default `true`). Needs
@@ -56,6 +58,7 @@ export const BlokView = ({
56
58
  toolAttributes,
57
59
  blockIds,
58
60
  transformUrl,
61
+ inlineRenderers,
59
62
  classes,
60
63
  ...divProps
61
64
  }: BlokViewProps): ReactNode => {
@@ -73,6 +76,7 @@ export const BlokView = ({
73
76
  toolAttributes,
74
77
  blockIds,
75
78
  transformUrl,
79
+ inlineRenderers,
76
80
  classes: classes ?? true,
77
81
  });
78
82
 
@@ -47,8 +47,17 @@ export interface BlockPortalRegistry {
47
47
  * for the same id replaces the entry rather than mounting a duplicate.
48
48
  */
49
49
  register(id: string, entry: BlockPortalEntry): void;
50
- /** Remove the entry for `id`. Safe (no-op) when absent. */
51
- unregister(id: string): void;
50
+ /**
51
+ * Remove the entry for `id`. Safe (no-op) when absent.
52
+ *
53
+ * Pass the caller's own `hostEl` to make the removal OWNERSHIP-CHECKED: the
54
+ * entry is deleted only when it is still the one that host registered. Core
55
+ * composes a replacement block (which registers under the SAME id) BEFORE it
56
+ * destroys the block it replaces, so a superseded tool's `removed()`/
57
+ * `destroy()` teardown arrives after the live entry is already in place —
58
+ * without the check it deletes that live entry and the holder stays empty.
59
+ */
60
+ unregister(id: string, hostEl?: HTMLElement): void;
52
61
  /** Merge `props` into the entry's props (in-place update, no remount). */
53
62
  setProps(id: string, props: Record<string, unknown>): void;
54
63
  /**
@@ -92,8 +101,15 @@ export const createBlockPortalRegistry = (): BlockPortalRegistry => {
92
101
  );
93
102
  commit(next);
94
103
  },
95
- unregister(id: string): void {
96
- if (!state.snapshot.has(id)) {
104
+ unregister(id: string, hostEl?: HTMLElement): void {
105
+ const current = state.snapshot.get(id);
106
+
107
+ if (current === undefined) {
108
+ return;
109
+ }
110
+
111
+ // A superseded owner may not evict the entry that replaced it.
112
+ if (hostEl !== undefined && current.hostEl !== hostEl) {
97
113
  return;
98
114
  }
99
115
 
@@ -0,0 +1,35 @@
1
+ import { createContext, useContext } from 'react';
2
+ import type { Blok } from '@/types';
3
+
4
+ /**
5
+ * Carries the LIVE editor instance down to everything `BlokContent` renders —
6
+ * which, because `createPortal` preserves the COMPONENT tree's context (not the
7
+ * DOM target's), includes every `createReactBlock` block mounted through the
8
+ * editor's shared `BlockPortalHost`.
9
+ *
10
+ * Null until the instance exists (SSR, first render, between recreations),
11
+ * matching `useBlok`'s own pre-ready contract.
12
+ */
13
+ export const BlokInstanceContext = createContext<Blok | null>(null);
14
+
15
+ /**
16
+ * The live Blok instance a component is rendered inside, or null before it
17
+ * exists. Inside a `createReactBlock` component this is the block's OWN editor —
18
+ * so a block can drive the tree it lives in without the host prop-drilling the
19
+ * instance into it:
20
+ *
21
+ * ```tsx
22
+ * const blocks = useBlocks(useBlokInstance());
23
+ * const children = blocks.getChildren(block.id);
24
+ * ```
25
+ *
26
+ * That pairing is also what makes a container block REACTIVE to its own child
27
+ * tree: `useBlocks` re-renders on the editor's `block changed` event, which core
28
+ * emits for every structural mutation — including children the adapter itself
29
+ * never sees (a pasted paragraph, a Tab-indent from the keyboard).
30
+ *
31
+ * Outside a `BlokContent`/`BlokEditor` subtree it returns null.
32
+ */
33
+ export function useBlokInstance(): Blok | null {
34
+ return useContext(BlokInstanceContext);
35
+ }
@@ -49,6 +49,7 @@ export const USE_BLOK_CONFIG_KEYS = [
49
49
  'width',
50
50
  'migrations',
51
51
  'uploader',
52
+ 'captureClicksBelowEditor',
52
53
  ] as const satisfies readonly (keyof UseBlokConfig)[];
53
54
 
54
55
  /**
@@ -0,0 +1,38 @@
1
+ import type { LooseOutputData, OutputData } from '@/types';
2
+
3
+ /**
4
+ * The controlled-content bookkeeping `useBlok` keeps for one editor: the
5
+ * baseline it believes the editor currently reflects, plus the window of
6
+ * payloads the editor emitted through `onSave`.
7
+ *
8
+ * The declarative `data` effect dedupes against both, which is only sound while
9
+ * that effect is the ONLY thing changing the content. An imperative
10
+ * `useBlokHandle().clear()` / `.render()` moves the editor out from under the
11
+ * bookkeeping: without this channel, restoring a document the editor itself
12
+ * emitted is dismissed as an echo and never rendered — the editor stays empty
13
+ * while React state says otherwise. Registered per instance (mirrors the holder
14
+ * / portal-registry WeakMaps) so the handle can reach it from the editor alone.
15
+ */
16
+ export interface ContentBaseline {
17
+ /**
18
+ * Records `content` as what the editor now reflects and drops every remembered
19
+ * emitted payload — content the editor emitted BEFORE an out-of-band change is
20
+ * no longer content it holds, so it must not no-op a later controlled restore.
21
+ * @param content - the document the editor now reflects
22
+ */
23
+ markRendered(content: OutputData | LooseOutputData | null): void;
24
+ }
25
+
26
+ const baselines = new WeakMap<WeakKey, ContentBaseline>();
27
+
28
+ export function setContentBaseline(editor: WeakKey, baseline: ContentBaseline): void {
29
+ baselines.set(editor, baseline);
30
+ }
31
+
32
+ export function getContentBaseline(editor: WeakKey): ContentBaseline | undefined {
33
+ return baselines.get(editor);
34
+ }
35
+
36
+ export function removeContentBaseline(editor: WeakKey): void {
37
+ baselines.delete(editor);
38
+ }