@bloklabs/react 1.7.0 → 1.8.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/dist/index.cjs +1 -1
- package/dist/index.mjs +426 -287
- package/package.json +2 -2
- package/src/BlokContent.tsx +11 -1
- package/src/block-portal-registry.ts +20 -4
- package/src/blok-instance-context.ts +35 -0
- package/src/config-keys.ts +1 -0
- package/src/content-baseline-map.ts +38 -0
- package/src/createReactBlock.tsx +431 -16
- package/src/index.ts +5 -0
- package/src/useBlok.ts +185 -56
- package/src/useBlokHandle.ts +40 -4
- package/types/index.d.ts +160 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bloklabs/react",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.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.
|
|
57
|
+
"@bloklabs/core": "^1.8.0",
|
|
58
58
|
"react": "^18.0.0 || ^19.0.0",
|
|
59
59
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
60
60
|
}
|
package/src/BlokContent.tsx
CHANGED
|
@@ -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 :
|
|
104
|
+
{registry === undefined ? null : (
|
|
105
|
+
<BlokInstanceContext.Provider value={editor}>
|
|
106
|
+
<BlockPortalHost registry={registry} />
|
|
107
|
+
</BlokInstanceContext.Provider>
|
|
108
|
+
)}
|
|
99
109
|
</div>
|
|
100
110
|
);
|
|
101
111
|
}
|
|
@@ -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
|
-
/**
|
|
51
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/src/config-keys.ts
CHANGED
|
@@ -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
|
+
}
|