@bloklabs/react 1.16.1 → 2.1.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.
@@ -1,161 +0,0 @@
1
- import type { ComponentType } from 'react';
2
-
3
- /**
4
- * Tool-config key carrying the editor's portal registry into a
5
- * `createReactBlock` tool. The tool is constructed by CORE (outside any React
6
- * render), so it cannot read context — `useBlok` injects the editor-scoped
7
- * registry through each react-block tool's `config`, and the tool reads it here.
8
- */
9
- export const BLOK_PORTAL_REGISTRY_CONFIG_KEY = '__blokPortalRegistry';
10
-
11
- /**
12
- * Tool-config key carrying the name the tool was registered under in the
13
- * consumer's `tools` map (injected next to the registry). The factory needs it
14
- * to associate portal entries with their tool so live config updates can be
15
- * routed; it is stripped (with the registry key) before the sanitized config
16
- * reaches the component.
17
- */
18
- export const BLOK_TOOL_NAME_CONFIG_KEY = '__blokToolName';
19
-
20
- /**
21
- * One mounted React block in the portal registry. `hostEl` is the Blok-owned
22
- * `toolRenderedElement` (tagged `data-blok-mutation-free`) the component is
23
- * portaled into; `props` is replaced immutably by `setProps` so the mounted
24
- * component re-renders in place (no remount).
25
- */
26
- export interface BlockPortalEntry {
27
- hostEl: HTMLElement;
28
- component: ComponentType<Record<string, unknown>>;
29
- props: Record<string, unknown>;
30
- /** Name the tool is registered under — routes per-tool live config updates. */
31
- toolName?: string;
32
- }
33
-
34
- /**
35
- * Per-editor registry of React blocks. A single shared `BlockPortalHost`
36
- * renders one `createPortal` per entry, so every block lives in ONE React tree
37
- * (shared context, one reconciler) — the TipTap ReactNodeViewRenderer pattern,
38
- * not a `createRoot` per block.
39
- *
40
- * Shaped as an external store (`subscribe`/`getSnapshot`) so the host can read
41
- * it through `useSyncExternalStore`. The snapshot is immutable: every mutation
42
- * replaces it, and `getSnapshot` is referentially stable between mutations.
43
- */
44
- export interface BlockPortalRegistry {
45
- /**
46
- * Register (or replace) the entry for `id`. Idempotent: a second register
47
- * for the same id replaces the entry rather than mounting a duplicate.
48
- */
49
- register(id: string, entry: BlockPortalEntry): 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;
61
- /** Merge `props` into the entry's props (in-place update, no remount). */
62
- setProps(id: string, props: Record<string, unknown>): void;
63
- /**
64
- * Replace the `config` prop of every entry registered under `toolName`, and
65
- * remember it so entries registered LATER start from this latest config
66
- * instead of their construction-time snapshot.
67
- */
68
- setToolConfig(toolName: string, config: Record<string, unknown>): void;
69
- /** Subscribe to snapshot changes; returns the unsubscribe function. */
70
- subscribe(listener: () => void): () => void;
71
- /** Current immutable snapshot (stable identity until the next mutation). */
72
- getSnapshot(): ReadonlyMap<string, BlockPortalEntry>;
73
- }
74
-
75
- /**
76
- * Create a fresh portal registry. One per editor instance (associated via the
77
- * registry map, mirroring the holder WeakMap), so multiple editors on a page
78
- * never cross-render each other's blocks.
79
- */
80
- export const createBlockPortalRegistry = (): BlockPortalRegistry => {
81
- const state = {
82
- snapshot: new Map<string, BlockPortalEntry>() as ReadonlyMap<string, BlockPortalEntry>,
83
- };
84
- const listeners = new Set<() => void>();
85
- /** Latest per-tool config, applied to mounted entries and future registers. */
86
- const toolConfigs = new Map<string, Record<string, unknown>>();
87
-
88
- const commit = (next: Map<string, BlockPortalEntry>): void => {
89
- state.snapshot = next;
90
- listeners.forEach(listener => listener());
91
- };
92
-
93
- return {
94
- register(id: string, entry: BlockPortalEntry): void {
95
- const next = new Map(state.snapshot);
96
- const liveConfig = entry.toolName === undefined ? undefined : toolConfigs.get(entry.toolName);
97
-
98
- next.set(
99
- id,
100
- liveConfig === undefined ? entry : { ...entry, props: { ...entry.props, config: liveConfig } }
101
- );
102
- commit(next);
103
- },
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) {
113
- return;
114
- }
115
-
116
- const next = new Map(state.snapshot);
117
-
118
- next.delete(id);
119
- commit(next);
120
- },
121
- setProps(id: string, props: Record<string, unknown>): void {
122
- const current = state.snapshot.get(id);
123
-
124
- if (current === undefined) {
125
- return;
126
- }
127
-
128
- const next = new Map(state.snapshot);
129
-
130
- next.set(id, { ...current, props: { ...current.props, ...props } });
131
- commit(next);
132
- },
133
- setToolConfig(toolName: string, config: Record<string, unknown>): void {
134
- toolConfigs.set(toolName, config);
135
-
136
- const matching = [...state.snapshot].filter(([, entry]) => entry.toolName === toolName);
137
-
138
- if (matching.length === 0) {
139
- return;
140
- }
141
-
142
- const next = new Map(state.snapshot);
143
-
144
- for (const [id, entry] of matching) {
145
- next.set(id, { ...entry, props: { ...entry.props, config } });
146
- }
147
-
148
- commit(next);
149
- },
150
- subscribe(listener: () => void): () => void {
151
- listeners.add(listener);
152
-
153
- return (): void => {
154
- listeners.delete(listener);
155
- };
156
- },
157
- getSnapshot(): ReadonlyMap<string, BlockPortalEntry> {
158
- return state.snapshot;
159
- },
160
- };
161
- };
@@ -1,27 +0,0 @@
1
- // packages/react/src/blocks-snapshot.ts
2
- //
3
- // The block-tree connection types and pure resolution helpers are
4
- // framework-agnostic and now live in a shared core module so the React, Vue,
5
- // and Angular adapters — and core's own per-block `BlockAPI` — resolve flat
6
- // insert/move indices through ONE battle-tested implementation (the parity
7
- // requirement: author once, reuse everywhere). This file re-exports them so
8
- // existing React imports (`./blocks-snapshot`) keep working unchanged.
9
- export {
10
- snapshotNodes,
11
- resolveInsertIndex,
12
- resolveMoveIndex,
13
- parentMap,
14
- isDescendantOf,
15
- } from '@bloklabs/core/adapters';
16
-
17
- export type {
18
- BlockNode,
19
- CaretTarget,
20
- InsertPosition,
21
- InsertSpec,
22
- TreeInsertSpec,
23
- MoveTarget,
24
- BlocksReader,
25
- IndexReader,
26
- UseBlocksApi,
27
- } from '@bloklabs/core/adapters';
@@ -1,35 +0,0 @@
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
- }
@@ -1,72 +0,0 @@
1
- import type { UseBlokConfig } from './types';
2
-
3
- /**
4
- * Keys that belong to the editor config (consumed by `useBlok`). Any prop on
5
- * `BlokEditor` whose key is NOT in this set is treated as a container div
6
- * attribute and spread onto the underlying element (mirrors `BlokContent`).
7
- *
8
- * Keep in sync with `UseBlokConfig`. The two compile-time guards below fail the
9
- * `yarn lint:types` typecheck if this list drifts.
10
- */
11
- export const USE_BLOK_CONFIG_KEYS = [
12
- 'autofocus',
13
- 'defaultBlock',
14
- 'dataModel',
15
- 'placeholder',
16
- 'sanitizer',
17
- 'hideToolbar',
18
- 'toolbarPosition',
19
- 'maxHistoryLength',
20
- 'historyDebounceTime',
21
- 'newGroupDelay',
22
- 'globalUndoRedo',
23
- 'tools',
24
- 'data',
25
- 'minHeight',
26
- 'loader',
27
- 'logLevel',
28
- 'readOnly',
29
- 'i18n',
30
- 'link',
31
- 'linkPaste',
32
- 'onBeforePaste',
33
- 'onReady',
34
- 'onChange',
35
- 'onSave',
36
- 'onError',
37
- 'onImageFailure',
38
- 'onEnter',
39
- 'onSubmit',
40
- 'onBeforeRender',
41
- 'onAfterRender',
42
- 'inlineToolbar',
43
- 'inlineEmoji',
44
- 'find',
45
- 'tunes',
46
- 'style',
47
- 'theme',
48
- 'onThemeChange',
49
- 'scrollToBlock',
50
- 'user',
51
- 'resolveUser',
52
- 'notifierPosition',
53
- 'notifier',
54
- 'width',
55
- 'migrations',
56
- 'uploader',
57
- 'media',
58
- 'server',
59
- 'ticket',
60
- 'persistence',
61
- 'collaboration',
62
- 'documentId',
63
- 'tabSync',
64
- 'captureClicksBelowEditor',
65
- ] as const satisfies readonly (keyof UseBlokConfig)[];
66
-
67
- /**
68
- * Compile error if a `UseBlokConfig` key is missing from the list above.
69
- * (`satisfies` only proves each entry is a valid key; this proves coverage.)
70
- */
71
- type _MissingConfigKey = Exclude<keyof UseBlokConfig, (typeof USE_BLOK_CONFIG_KEYS)[number]>;
72
- const _exhaustive: _MissingConfigKey extends never ? true : never = true;
@@ -1,38 +0,0 @@
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
- }