@bloklabs/react 1.2.1 → 1.2.2
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 +361 -140
- package/package.json +2 -2
- package/src/BlockPortalHost.tsx +58 -0
- package/src/BlokContent.tsx +13 -1
- package/src/BlokEditor.tsx +6 -0
- package/src/block-portal-registry.ts +105 -0
- package/src/createReactBlock.tsx +304 -0
- package/src/index.ts +14 -0
- package/src/live-tool-config.ts +112 -0
- package/src/registry-map.ts +21 -0
- package/src/useBlok.ts +67 -0
- package/types/index.d.ts +112 -0
package/src/useBlok.ts
CHANGED
|
@@ -3,6 +3,13 @@ import { Blok as BlokRuntime } from '@bloklabs/core';
|
|
|
3
3
|
import { setHolder, removeHolder } from './holder-map';
|
|
4
4
|
import { deepEqual } from './deep-equal';
|
|
5
5
|
import { BlokDefaultsContext, mergeBlokDefaults } from './provide-blok';
|
|
6
|
+
import {
|
|
7
|
+
createBlockPortalRegistry,
|
|
8
|
+
BLOK_PORTAL_REGISTRY_CONFIG_KEY,
|
|
9
|
+
type BlockPortalRegistry,
|
|
10
|
+
} from './block-portal-registry';
|
|
11
|
+
import { setRegistry, removeRegistry } from './registry-map';
|
|
12
|
+
import { bindLiveToolConfigFunctions } from './live-tool-config';
|
|
6
13
|
import { normalizeReadOnlyConfig } from '@bloklabs/core/adapters';
|
|
7
14
|
import type { Blok } from '@/types';
|
|
8
15
|
import type { UseBlokConfig } from './types';
|
|
@@ -18,6 +25,45 @@ interface EditorInstanceState {
|
|
|
18
25
|
depsToken: Record<string, unknown> | null;
|
|
19
26
|
}
|
|
20
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Inject the editor's portal registry into every `createReactBlock` tool's
|
|
30
|
+
* config (vanilla tools are left untouched), returning a NEW tools object so
|
|
31
|
+
* the consumer's config is never mutated. A react-block tool is constructed by
|
|
32
|
+
* CORE, outside any React render, so it cannot read context — this config
|
|
33
|
+
* bridge is how the tool reaches its editor-scoped registry.
|
|
34
|
+
*/
|
|
35
|
+
const injectPortalRegistry = (tools: unknown, registry: BlockPortalRegistry): unknown => {
|
|
36
|
+
if (tools === null || typeof tools !== 'object') {
|
|
37
|
+
return tools;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const result: Record<string, unknown> = {};
|
|
41
|
+
|
|
42
|
+
for (const [name, entry] of Object.entries(tools as Record<string, unknown>)) {
|
|
43
|
+
const toolClass = typeof entry === 'function' ? entry : (entry as { class?: unknown })?.class;
|
|
44
|
+
const isReactBlock =
|
|
45
|
+
typeof toolClass === 'function' &&
|
|
46
|
+
(toolClass as { __isBlokReactBlock?: boolean }).__isBlokReactBlock === true;
|
|
47
|
+
|
|
48
|
+
if (!isReactBlock) {
|
|
49
|
+
result[name] = entry;
|
|
50
|
+
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const base: Record<string, unknown> =
|
|
55
|
+
typeof entry === 'function' ? { class: entry } : { ...(entry as Record<string, unknown>) };
|
|
56
|
+
|
|
57
|
+
base.config = {
|
|
58
|
+
...((base.config as Record<string, unknown> | undefined) ?? {}),
|
|
59
|
+
[BLOK_PORTAL_REGISTRY_CONFIG_KEY]: registry,
|
|
60
|
+
};
|
|
61
|
+
result[name] = base;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return result;
|
|
65
|
+
};
|
|
66
|
+
|
|
21
67
|
/**
|
|
22
68
|
* React hook that manages a Blok editor instance lifecycle.
|
|
23
69
|
*
|
|
@@ -115,6 +161,7 @@ export function useBlok(configInput: UseBlokConfig, deps?: DependencyList): Blok
|
|
|
115
161
|
// Destroy leftover editor from a previous deps cycle
|
|
116
162
|
if (state.editor !== null && !state.isDestroyed) {
|
|
117
163
|
removeHolder(state.editor);
|
|
164
|
+
removeRegistry(state.editor);
|
|
118
165
|
try {
|
|
119
166
|
state.editor.destroy();
|
|
120
167
|
} catch {
|
|
@@ -136,8 +183,25 @@ export function useBlok(configInput: UseBlokConfig, deps?: DependencyList): Blok
|
|
|
136
183
|
|
|
137
184
|
// Wrap callbacks via ref so they never go stale
|
|
138
185
|
const currentConfig = configRef.current;
|
|
186
|
+
|
|
187
|
+
// Per-editor portal registry for `createReactBlock` tools: injected into
|
|
188
|
+
// each react-block tool's config (so the core-constructed tool can reach
|
|
189
|
+
// it) and associated with the editor below (so BlokContent can mount the
|
|
190
|
+
// shared portal host). Tool-config FUNCTIONS are first re-bound to stable
|
|
191
|
+
// wrappers that always call the latest render's closure — so consumers
|
|
192
|
+
// never need to freeze identities or recreate the editor to update them.
|
|
193
|
+
const registry = createBlockPortalRegistry();
|
|
194
|
+
const liveTools =
|
|
195
|
+
currentConfig.tools === undefined
|
|
196
|
+
? undefined
|
|
197
|
+
: injectPortalRegistry(
|
|
198
|
+
bindLiveToolConfigFunctions(currentConfig.tools, () => configRef.current.tools),
|
|
199
|
+
registry
|
|
200
|
+
);
|
|
201
|
+
|
|
139
202
|
const blokConfig = {
|
|
140
203
|
...currentConfig,
|
|
204
|
+
...(liveTools === undefined ? {} : { tools: liveTools as UseBlokConfig['tools'] }),
|
|
141
205
|
holder,
|
|
142
206
|
onReady: (...args: Parameters<NonNullable<UseBlokConfig['onReady']>>): void => {
|
|
143
207
|
configRef.current.onReady?.(...args);
|
|
@@ -182,6 +246,7 @@ export function useBlok(configInput: UseBlokConfig, deps?: DependencyList): Blok
|
|
|
182
246
|
// tell a genuine post-construction change from the construction value itself.
|
|
183
247
|
constructedDataRef.current = currentConfig.data;
|
|
184
248
|
setHolder(blok, holder);
|
|
249
|
+
setRegistry(blok, registry);
|
|
185
250
|
|
|
186
251
|
void blok.isReady
|
|
187
252
|
.then(() => {
|
|
@@ -193,6 +258,7 @@ export function useBlok(configInput: UseBlokConfig, deps?: DependencyList): Blok
|
|
|
193
258
|
.catch(() => {
|
|
194
259
|
if (state.editor === blok && !state.isDestroyed) {
|
|
195
260
|
removeHolder(blok);
|
|
261
|
+
removeRegistry(blok);
|
|
196
262
|
try {
|
|
197
263
|
blok.destroy();
|
|
198
264
|
} catch {
|
|
@@ -348,6 +414,7 @@ function deferDestroy(
|
|
|
348
414
|
state.destroyTimeout = setTimeout(() => {
|
|
349
415
|
if (state.editor !== null) {
|
|
350
416
|
removeHolder(state.editor);
|
|
417
|
+
removeRegistry(state.editor);
|
|
351
418
|
try {
|
|
352
419
|
state.editor.destroy();
|
|
353
420
|
} catch {
|
package/types/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { BlokConfig, BlockToolData, OutputBlockData, OutputData } from '@bloklabs/core';
|
|
2
|
+
import type { BlockAPI, BlockToolConstructable, ToolboxConfig } from '@bloklabs/core';
|
|
2
3
|
import type { Blok, EditorWidth, BlockRenderedPayload, BlocksRenderedPayload } from '@bloklabs/core';
|
|
3
4
|
import type { BlockTuneData } from '@bloklabs/core';
|
|
4
5
|
import type { MarkdownImportConfig } from '@bloklabs/core/markdown';
|
|
@@ -536,3 +537,114 @@ export interface UseBlocksApi {
|
|
|
536
537
|
* @param editor - the Blok instance from useBlok, or null before it is ready
|
|
537
538
|
*/
|
|
538
539
|
export declare function useBlocks(editor: Blok | null): UseBlocksApi;
|
|
540
|
+
|
|
541
|
+
// ---------------------------------------------------------------------------
|
|
542
|
+
// Block authoring (createReactBlock + portal host)
|
|
543
|
+
// ---------------------------------------------------------------------------
|
|
544
|
+
|
|
545
|
+
/** One field of a {@link CreateReactBlockSpec.propSchema}. */
|
|
546
|
+
export interface PropSchemaEntry {
|
|
547
|
+
default: unknown;
|
|
548
|
+
values?: readonly unknown[];
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Declarative data shape. Its keys are EXACTLY the keys `save()` returns to Yjs
|
|
553
|
+
* (a cleared field is written as its default, never dropped).
|
|
554
|
+
*/
|
|
555
|
+
export type PropSchema = Record<string, PropSchemaEntry>;
|
|
556
|
+
|
|
557
|
+
/** Props handed to a React block's `component` (the only data write path is `commit`). */
|
|
558
|
+
export interface ReactBlockRenderProps<Data> {
|
|
559
|
+
/** Frozen snapshot of the block data. Re-rendered with a new value on change; never mutate. */
|
|
560
|
+
data: Readonly<Data>;
|
|
561
|
+
/** The ONLY data write path: merge a partial patch and sync once. */
|
|
562
|
+
commit: (patch: Partial<Data>) => void;
|
|
563
|
+
/** This block's per-block API. */
|
|
564
|
+
block: BlockAPI;
|
|
565
|
+
/**
|
|
566
|
+
* Read-only flag. Toggled IN PLACE by core's read-only switch (no remount,
|
|
567
|
+
* ephemeral state survives). Honor it — a block that ignores it stays
|
|
568
|
+
* interactive when the editor is read-only.
|
|
569
|
+
*/
|
|
570
|
+
readOnly: boolean;
|
|
571
|
+
/** Engine-owned child slot — render `<BlockChildren />` for a container block. */
|
|
572
|
+
BlockChildren: React.ComponentType;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/** Spec for {@link createReactBlock}. */
|
|
576
|
+
export interface CreateReactBlockSpec<Data extends BlockToolData = BlockToolData> {
|
|
577
|
+
type: string;
|
|
578
|
+
toolbox?: ToolboxConfig;
|
|
579
|
+
propSchema: PropSchema;
|
|
580
|
+
component: React.ComponentType<ReactBlockRenderProps<Data>>;
|
|
581
|
+
onRendered?: (block: BlockAPI) => void;
|
|
582
|
+
onMoved?: (block: BlockAPI) => void;
|
|
583
|
+
onRemoved?: (block: BlockAPI) => void;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Author a first-party React block: returns a `BlockToolConstructable`
|
|
588
|
+
* registered exactly like a vanilla tool (`tools: { type: createReactBlock(...) }`).
|
|
589
|
+
* Every block renders through the editor's shared portal host INSIDE the React
|
|
590
|
+
* tree that renders `BlokContent`/`BlokEditor`, so app-level context (themes,
|
|
591
|
+
* design-system providers, stores) reaches block components with no bridge, and
|
|
592
|
+
* fresh render closures reach them with no editor recreation.
|
|
593
|
+
*
|
|
594
|
+
* @example
|
|
595
|
+
* ```tsx
|
|
596
|
+
* const Callout = createReactBlock<{ text: string }>({
|
|
597
|
+
* type: 'callout',
|
|
598
|
+
* propSchema: { text: { default: '' } },
|
|
599
|
+
* component: ({ data, commit }) => (
|
|
600
|
+
* <aside onClick={() => commit({ text: 'hi' })}>{data.text}</aside>
|
|
601
|
+
* ),
|
|
602
|
+
* });
|
|
603
|
+
* ```
|
|
604
|
+
*/
|
|
605
|
+
export declare function createReactBlock<Data extends BlockToolData = BlockToolData>(
|
|
606
|
+
spec: CreateReactBlockSpec<Data>
|
|
607
|
+
): BlockToolConstructable;
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Tool-config key carrying the editor's portal registry into a
|
|
611
|
+
* `createReactBlock` tool. Injected automatically by `useBlok`; exposed for
|
|
612
|
+
* advanced setups that construct tools outside `useBlok`.
|
|
613
|
+
*/
|
|
614
|
+
export declare const BLOK_PORTAL_REGISTRY_CONFIG_KEY: '__blokPortalRegistry';
|
|
615
|
+
|
|
616
|
+
/** One mounted React block in the portal registry. */
|
|
617
|
+
export interface BlockPortalEntry {
|
|
618
|
+
hostEl: HTMLElement;
|
|
619
|
+
component: React.ComponentType<Record<string, unknown>>;
|
|
620
|
+
props: Record<string, unknown>;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Per-editor registry of React blocks, shaped as an external store
|
|
625
|
+
* (`subscribe`/`getSnapshot`) consumed by `BlockPortalHost` via
|
|
626
|
+
* `useSyncExternalStore`. Snapshots are immutable and referentially stable
|
|
627
|
+
* between mutations.
|
|
628
|
+
*/
|
|
629
|
+
export interface BlockPortalRegistry {
|
|
630
|
+
register(id: string, entry: BlockPortalEntry): void;
|
|
631
|
+
unregister(id: string): void;
|
|
632
|
+
setProps(id: string, props: Record<string, unknown>): void;
|
|
633
|
+
subscribe(listener: () => void): () => void;
|
|
634
|
+
getSnapshot(): ReadonlyMap<string, BlockPortalEntry>;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/** Create a fresh portal registry (one per editor instance). */
|
|
638
|
+
export declare function createBlockPortalRegistry(): BlockPortalRegistry;
|
|
639
|
+
|
|
640
|
+
/** Props for {@link BlockPortalHost}. */
|
|
641
|
+
export interface BlockPortalHostProps {
|
|
642
|
+
registry: BlockPortalRegistry;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* The single shared render tree that portals each registered React block into
|
|
647
|
+
* its Blok-owned host element. Mounted automatically by `BlokContent`; exposed
|
|
648
|
+
* for advanced setups that manage the registry themselves.
|
|
649
|
+
*/
|
|
650
|
+
export declare function BlockPortalHost(props: BlockPortalHostProps): React.ReactElement;
|