@bloklabs/react 1.16.1 → 2.0.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 +23 -0
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +365 -353
- package/package.json +3 -4
- package/types/index.d.ts +63 -4
- package/src/BlockPortalHost.tsx +0 -58
- package/src/BlokContent.tsx +0 -112
- package/src/BlokEditor.tsx +0 -201
- package/src/BlokView.tsx +0 -106
- package/src/block-portal-registry.ts +0 -161
- package/src/blocks-snapshot.ts +0 -27
- package/src/blok-instance-context.ts +0 -35
- package/src/config-keys.ts +0 -72
- package/src/content-baseline-map.ts +0 -38
- package/src/createReactBlock.tsx +0 -1062
- package/src/createReactInlineTool.tsx +0 -406
- package/src/deep-equal.ts +0 -7
- package/src/holder-map.ts +0 -17
- package/src/index.ts +0 -62
- package/src/live-tool-config.ts +0 -154
- package/src/provide-blok.tsx +0 -70
- package/src/registry-map.ts +0 -21
- package/src/types.ts +0 -38
- package/src/useBlocks.ts +0 -119
- package/src/useBlok.ts +0 -908
- package/src/useBlokHandle.ts +0 -141
- package/src/useBlokReady.ts +0 -60
- package/src/useBlokView.ts +0 -70
- package/src/view-nodes-to-react.ts +0 -153
|
@@ -1,406 +0,0 @@
|
|
|
1
|
-
import { createContext, useContext, useMemo, type ComponentType, type ReactElement } from 'react';
|
|
2
|
-
import { markSanitizerConfig } from '@bloklabs/core/adapters';
|
|
3
|
-
import {
|
|
4
|
-
BLOK_PORTAL_REGISTRY_CONFIG_KEY,
|
|
5
|
-
BLOK_TOOL_NAME_CONFIG_KEY,
|
|
6
|
-
type BlockPortalRegistry,
|
|
7
|
-
} from './block-portal-registry';
|
|
8
|
-
import type { API, SanitizerConfig } from '@/types';
|
|
9
|
-
import type { MarkSnapshot, MarkSpec } from '@/types';
|
|
10
|
-
import type { InlineTool, InlineToolConstructorOptions, MenuConfig } from '@/types/tools';
|
|
11
|
-
|
|
12
|
-
/** Adapter-internal config keys, stripped before the config reaches the component. */
|
|
13
|
-
const INTERNAL_CONFIG_KEYS: readonly string[] = [
|
|
14
|
-
BLOK_PORTAL_REGISTRY_CONFIG_KEY,
|
|
15
|
-
BLOK_TOOL_NAME_CONFIG_KEY,
|
|
16
|
-
];
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Unique portal-registry key per tool instance (inline tools have no block id
|
|
20
|
-
* to key on, and multiple instances of the same tool can coexist — e.g. the
|
|
21
|
-
* visible toolbar's instance plus a shortcut render-probe).
|
|
22
|
-
* @param toolName - name the tool is registered under
|
|
23
|
-
*/
|
|
24
|
-
const generatePortalKey = (toolName: string): string =>
|
|
25
|
-
`__blok-inline-tool__:${toolName}:${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
26
|
-
|
|
27
|
-
/** Props handed to a React inline tool's `component` (its toolbar icon/UI). */
|
|
28
|
-
export interface ReactInlineToolRenderProps<Config = Record<string, unknown>> {
|
|
29
|
-
/**
|
|
30
|
-
* Whether the tool's formatting is active for the current selection —
|
|
31
|
-
* derived from the spec's `checkState` every time the toolbar queries it.
|
|
32
|
-
* Pushed IN PLACE (no remount), so the icon restyles live.
|
|
33
|
-
*/
|
|
34
|
-
active: boolean;
|
|
35
|
-
/**
|
|
36
|
-
* The tool's `config` from the consumer's `tools` map (adapter-internal
|
|
37
|
-
* keys stripped) — the channel for host props (palette, locale…).
|
|
38
|
-
*/
|
|
39
|
-
config: Readonly<Partial<Config>>;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** The spec's mark operations, bound to the live selection via `api.marks`. */
|
|
43
|
-
export interface InlineToolMarkOps<State = void> {
|
|
44
|
-
/** Whether the whole selection currently carries the mark. */
|
|
45
|
-
has(): boolean;
|
|
46
|
-
/** Toggle the mark on the selection; returns true when now applied. */
|
|
47
|
-
toggle(state?: State): boolean;
|
|
48
|
-
/** Apply (or update in place) the mark on the selection. */
|
|
49
|
-
apply(state?: State): HTMLElement[];
|
|
50
|
-
/** Remove the mark from the selection. */
|
|
51
|
-
remove(): HTMLElement[];
|
|
52
|
-
/** Read the mark's current declared values at the selection. */
|
|
53
|
-
read(): MarkSnapshot | null;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Everything a React inline tool's component (and any component nested in it —
|
|
58
|
-
* swatches, popovers) can reach via {@link useInlineTool} without prop-drilling.
|
|
59
|
-
*/
|
|
60
|
-
export interface InlineToolHandle<Config = Record<string, unknown>, State = void> {
|
|
61
|
-
/** Live active state, same value as the component's `active` prop. */
|
|
62
|
-
active: boolean;
|
|
63
|
-
/** The tool's config, same value as the component's `config` prop. */
|
|
64
|
-
config: Readonly<Partial<Config>>;
|
|
65
|
-
/** The editor API handed to the tool's constructor (undefined outside core). */
|
|
66
|
-
api: API | undefined;
|
|
67
|
-
/**
|
|
68
|
-
* Operations bound to the spec's `mark` — null when the spec declares no
|
|
69
|
-
* mark or the editor api provides no `marks` surface.
|
|
70
|
-
*/
|
|
71
|
-
mark: InlineToolMarkOps<State> | null;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const InlineToolContext = createContext<InlineToolHandle | null>(null);
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Reach the enclosing React inline tool from any component inside its
|
|
78
|
-
* portaled UI: live `active` state, the tool `config`, the editor `api`, and
|
|
79
|
-
* the spec's mark operations bound to the live selection.
|
|
80
|
-
*/
|
|
81
|
-
export function useInlineTool<Config = Record<string, unknown>, State = void>(): InlineToolHandle<Config, State> {
|
|
82
|
-
const handle = useContext(InlineToolContext);
|
|
83
|
-
|
|
84
|
-
if (handle === null) {
|
|
85
|
-
throw new Error('useInlineTool() must be called from a component rendered by createReactInlineTool');
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return handle as InlineToolHandle<Config, State>;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/** Spec for {@link createReactInlineTool}. */
|
|
92
|
-
export interface CreateReactInlineToolSpec<Config = Record<string, unknown>, State = void> {
|
|
93
|
-
/** Tool type name — the MenuConfig item name (fallback when the tools-map key is unavailable). */
|
|
94
|
-
type: string;
|
|
95
|
-
/** Optional toolbar item title (shown in overflow/search contexts). */
|
|
96
|
-
title?: string;
|
|
97
|
-
/**
|
|
98
|
-
* Translation key for the toolbar label, resolved by core i18n as
|
|
99
|
-
* `toolNames.{titleKey}` (or used verbatim when it contains a dot). Without
|
|
100
|
-
* it a custom tool's label is only localizable through the legacy
|
|
101
|
-
* capitalized-tool-name fallback.
|
|
102
|
-
*/
|
|
103
|
-
titleKey?: string;
|
|
104
|
-
/** The component rendered as the tool's toolbar icon/UI. */
|
|
105
|
-
component: ComponentType<ReactInlineToolRenderProps<Config>>;
|
|
106
|
-
/**
|
|
107
|
-
* Declarative description of the wrapper this tool produces. When present,
|
|
108
|
-
* `surround` (toggle), `checkState` (whole-range coverage) and `sanitize`
|
|
109
|
-
* are derived from it — range-aware splitting, adjacent-run semantics and
|
|
110
|
-
* the trailing-whitespace fix come from the editor's mark engine
|
|
111
|
-
* (`api.marks`) with no DOM plumbing in the tool.
|
|
112
|
-
*/
|
|
113
|
-
mark?: MarkSpec<State>;
|
|
114
|
-
/**
|
|
115
|
-
* Applies/removes the formatting on the LIVE selection's range (captured at
|
|
116
|
-
* activation time). Receives the editor API as its second argument.
|
|
117
|
-
* Takes precedence over the `mark` derivation.
|
|
118
|
-
*/
|
|
119
|
-
surround?: (range: Range, api: API | undefined) => void;
|
|
120
|
-
/**
|
|
121
|
-
* Reports whether the formatting is active for the given selection.
|
|
122
|
-
* Receives the editor API as its second argument.
|
|
123
|
-
* Takes precedence over the `mark` derivation.
|
|
124
|
-
*/
|
|
125
|
-
checkState?: (selection: Selection | null, api: API | undefined) => boolean;
|
|
126
|
-
/** Keyboard shortcut (e.g. `CMD+SHIFT+C`). */
|
|
127
|
-
shortcut?: string;
|
|
128
|
-
/**
|
|
129
|
-
* Inline sanitizer config declaring the markup this tool produces.
|
|
130
|
-
* Defaults to the config derived from `mark` when one is declared.
|
|
131
|
-
*/
|
|
132
|
-
sanitize?: SanitizerConfig;
|
|
133
|
-
/** Whether the tool stays available in read-only mode. */
|
|
134
|
-
isReadOnlySupported?: boolean;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Author a first-party React inline tool. Returns an inline-tool constructable
|
|
139
|
-
* registered exactly like a vanilla tool (`tools: { color: createReactInlineTool(...) }`).
|
|
140
|
-
*
|
|
141
|
-
* Mirrors the `createReactBlock` portal pattern: `render()` returns a
|
|
142
|
-
* `MenuConfig` whose `icon` is a Blok-owned host element
|
|
143
|
-
* (`data-blok-mutation-free`) the component is portaled into via the editor's
|
|
144
|
-
* shared `BlockPortalHost` tree — app-level context (themes, styled-components
|
|
145
|
-
* providers) reaches the icon with no consumer-managed `createRoot`, and
|
|
146
|
-
* `destroy()` (called by the inline toolbar on close/teardown) unregisters the
|
|
147
|
-
* portal so the component unmounts deterministically — no leaked React roots.
|
|
148
|
-
*
|
|
149
|
-
* The editor `api` handed to the constructor is kept: it reaches `surround`/
|
|
150
|
-
* `checkState` as their second argument, powers the `mark` derivation, and is
|
|
151
|
-
* exposed to the portaled component tree via {@link useInlineTool}.
|
|
152
|
-
*
|
|
153
|
-
* Without an injected registry (vanilla core, no `useBlok`), the tool still
|
|
154
|
-
* renders a valid MenuConfig — the host element just stays empty.
|
|
155
|
-
*/
|
|
156
|
-
export function createReactInlineTool<Config = Record<string, unknown>, State = void>(
|
|
157
|
-
spec: CreateReactInlineToolSpec<Config, State>
|
|
158
|
-
): (new (options: InlineToolConstructorOptions) => InlineTool) & {
|
|
159
|
-
readonly __isBlokReactInlineTool: true;
|
|
160
|
-
readonly isInline: true;
|
|
161
|
-
readonly type: string;
|
|
162
|
-
readonly title: string | undefined;
|
|
163
|
-
readonly titleKey: string;
|
|
164
|
-
readonly shortcut: string | undefined;
|
|
165
|
-
readonly mark: MarkSpec<State> | undefined;
|
|
166
|
-
readonly sanitize: SanitizerConfig | undefined;
|
|
167
|
-
readonly isReadOnlySupported: boolean;
|
|
168
|
-
} {
|
|
169
|
-
return class ReactInlineTool implements InlineTool {
|
|
170
|
-
/** Marker so `useBlok` can detect react inline tools and inject the registry. */
|
|
171
|
-
public static readonly __isBlokReactInlineTool = true as const;
|
|
172
|
-
public static readonly isInline = true as const;
|
|
173
|
-
|
|
174
|
-
/** The tool's type name — also the default translation key and mark tag context. */
|
|
175
|
-
public static get type(): string {
|
|
176
|
-
return spec.type;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
public static get title(): string | undefined {
|
|
180
|
-
return spec.title;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Translation key core i18n resolves as `toolNames.{titleKey}`. Defaults to
|
|
185
|
-
* the tool `type` so a custom mark localizes through the conventional
|
|
186
|
-
* `toolNames.{type}` key (as built-in tools do) instead of silently falling
|
|
187
|
-
* to the un-translated capitalized tool name. When the consumer supplies no
|
|
188
|
-
* matching i18n entry, core still falls through to that same label — the
|
|
189
|
-
* default is a no-op there and never a regression.
|
|
190
|
-
*/
|
|
191
|
-
public static get titleKey(): string {
|
|
192
|
-
return spec.titleKey ?? spec.type;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
public static get shortcut(): string | undefined {
|
|
196
|
-
return spec.shortcut;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* The declared mark spec (if any). Exposed as a static so consumers can
|
|
201
|
-
* assert a tool's mark in unit tests without mocking the factory.
|
|
202
|
-
*/
|
|
203
|
-
public static get mark(): MarkSpec<State> | undefined {
|
|
204
|
-
return spec.mark;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
public static get sanitize(): SanitizerConfig | undefined {
|
|
208
|
-
if (spec.sanitize !== undefined) {
|
|
209
|
-
return spec.sanitize;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
return spec.mark !== undefined ? markSanitizerConfig(spec.mark) : undefined;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
public static get isReadOnlySupported(): boolean {
|
|
216
|
-
return spec.isReadOnlySupported ?? false;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
private readonly registry: BlockPortalRegistry | undefined;
|
|
220
|
-
/** The editor API — kept (not discarded) so the tool has the same reach as a vanilla one. */
|
|
221
|
-
private readonly api: API | undefined;
|
|
222
|
-
/** Name this tool is registered under in the consumer's `tools` map. */
|
|
223
|
-
private readonly toolName: string;
|
|
224
|
-
/** Unique portal-registry key — inline tools have no block id. */
|
|
225
|
-
private readonly portalKey: string;
|
|
226
|
-
/** The consumer's tool config with adapter-internal keys stripped. */
|
|
227
|
-
private readonly toolConfig: Readonly<Partial<Config>>;
|
|
228
|
-
/**
|
|
229
|
-
* Stable provider component wrapping `spec.component` with the
|
|
230
|
-
* {@link useInlineTool} context. Created once per instance so prop pushes
|
|
231
|
-
* (setProps) re-render without remounting the consumer's component.
|
|
232
|
-
*/
|
|
233
|
-
private readonly portalComponent: ComponentType<ReactInlineToolRenderProps<Config>>;
|
|
234
|
-
/** Last active state pushed to the mounted component (change-dedup baseline). */
|
|
235
|
-
private active = false;
|
|
236
|
-
/** True while this instance owns a registered portal entry. */
|
|
237
|
-
private registered = false;
|
|
238
|
-
|
|
239
|
-
public constructor(options: InlineToolConstructorOptions) {
|
|
240
|
-
const config: Record<string, unknown> = options?.config ?? {};
|
|
241
|
-
|
|
242
|
-
this.api = options?.api;
|
|
243
|
-
this.registry = config[BLOK_PORTAL_REGISTRY_CONFIG_KEY] as BlockPortalRegistry | undefined;
|
|
244
|
-
|
|
245
|
-
const toolName = config[BLOK_TOOL_NAME_CONFIG_KEY];
|
|
246
|
-
|
|
247
|
-
this.toolName = typeof toolName === 'string' ? toolName : spec.type;
|
|
248
|
-
this.portalKey = generatePortalKey(this.toolName);
|
|
249
|
-
|
|
250
|
-
const publicConfig: Record<string, unknown> = {};
|
|
251
|
-
|
|
252
|
-
for (const [key, value] of Object.entries(config)) {
|
|
253
|
-
if (!INTERNAL_CONFIG_KEYS.includes(key)) {
|
|
254
|
-
publicConfig[key] = value;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
this.toolConfig = Object.freeze(publicConfig) as Readonly<Partial<Config>>;
|
|
258
|
-
this.portalComponent = this.createPortalComponent();
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
public render(): MenuConfig {
|
|
262
|
-
// The Blok-owned host: React reconciles the icon portaled INTO it, and
|
|
263
|
-
// this attribute makes core's MutationObserver ignore those mutations.
|
|
264
|
-
const host = document.createElement('div');
|
|
265
|
-
|
|
266
|
-
host.setAttribute('data-blok-mutation-free', 'true');
|
|
267
|
-
|
|
268
|
-
if (this.registry !== undefined) {
|
|
269
|
-
this.registry.register(this.portalKey, {
|
|
270
|
-
hostEl: host,
|
|
271
|
-
component: this.portalComponent as unknown as ComponentType<Record<string, unknown>>,
|
|
272
|
-
props: { active: this.active, config: this.toolConfig },
|
|
273
|
-
toolName: this.toolName,
|
|
274
|
-
});
|
|
275
|
-
this.registered = true;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
return {
|
|
279
|
-
name: this.toolName,
|
|
280
|
-
title: spec.title,
|
|
281
|
-
icon: host,
|
|
282
|
-
onActivate: (): void => {
|
|
283
|
-
this.activate();
|
|
284
|
-
},
|
|
285
|
-
isActive: (): boolean => this.refreshActiveState(),
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
/**
|
|
290
|
-
* Idempotent teardown, called by the inline toolbar when this instance is
|
|
291
|
-
* dropped (toolbar close, editor destroy, or a throwaway render probe).
|
|
292
|
-
* Unregisters the portal entry so React unmounts the icon component —
|
|
293
|
-
* the unmount signal consumer-managed `createRoot` hacks never had.
|
|
294
|
-
*/
|
|
295
|
-
public destroy(): void {
|
|
296
|
-
if (!this.registered) {
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
this.registered = false;
|
|
301
|
-
this.registry?.unregister(this.portalKey);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
/**
|
|
305
|
-
* Run the tool's formatting action: an explicit `surround` on the LIVE
|
|
306
|
-
* selection's range (captured at activation time, matching
|
|
307
|
-
* wrapLegacyInlineTool — never a range snapshotted at render), or the
|
|
308
|
-
* mark derivation (`api.marks.toggle`) when only a `mark` is declared.
|
|
309
|
-
*/
|
|
310
|
-
private activate(): void {
|
|
311
|
-
if (typeof spec.surround === 'function') {
|
|
312
|
-
const selection = window.getSelection();
|
|
313
|
-
|
|
314
|
-
if (!selection || selection.rangeCount === 0) {
|
|
315
|
-
return;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
spec.surround(selection.getRangeAt(0), this.api);
|
|
319
|
-
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
if (spec.mark !== undefined && this.api?.marks !== undefined) {
|
|
324
|
-
this.api.marks.toggle(spec.mark);
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
/**
|
|
329
|
-
* Recompute the active state from the live selection and push it to the
|
|
330
|
-
* mounted component ONLY when it actually changed (no redundant
|
|
331
|
-
* external-store notifications).
|
|
332
|
-
*/
|
|
333
|
-
private refreshActiveState(): boolean {
|
|
334
|
-
const active = this.computeActiveState();
|
|
335
|
-
|
|
336
|
-
if (active !== this.active) {
|
|
337
|
-
this.active = active;
|
|
338
|
-
|
|
339
|
-
if (this.registered) {
|
|
340
|
-
this.registry?.setProps(this.portalKey, { active });
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
return active;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
/**
|
|
348
|
-
* Explicit `checkState` wins; otherwise the mark derivation answers
|
|
349
|
-
* "does the WHOLE selection carry the mark" via `api.marks.has`.
|
|
350
|
-
*/
|
|
351
|
-
private computeActiveState(): boolean {
|
|
352
|
-
if (typeof spec.checkState === 'function') {
|
|
353
|
-
return Boolean(spec.checkState(window.getSelection(), this.api));
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
if (spec.mark !== undefined && this.api?.marks !== undefined) {
|
|
357
|
-
return this.api.marks.has(spec.mark);
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
return false;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
/**
|
|
364
|
-
* Build the per-instance provider component: the consumer's component
|
|
365
|
-
* rendered inside the {@link useInlineTool} context, so nested
|
|
366
|
-
* swatches/popovers reach the tool without prop-drilling.
|
|
367
|
-
*/
|
|
368
|
-
private createPortalComponent(): ComponentType<ReactInlineToolRenderProps<Config>> {
|
|
369
|
-
const api = this.api;
|
|
370
|
-
const mark = spec.mark;
|
|
371
|
-
const Component = spec.component;
|
|
372
|
-
|
|
373
|
-
const markOps: InlineToolMarkOps<State> | null = mark !== undefined && api?.marks !== undefined
|
|
374
|
-
? {
|
|
375
|
-
has: (): boolean => api.marks.has(mark),
|
|
376
|
-
toggle: (state?: State): boolean => api.marks.toggle(mark, state),
|
|
377
|
-
apply: (state?: State): HTMLElement[] => api.marks.apply(mark, state),
|
|
378
|
-
remove: (): HTMLElement[] => api.marks.remove(mark),
|
|
379
|
-
read: (): MarkSnapshot | null => api.marks.read(mark),
|
|
380
|
-
}
|
|
381
|
-
: null;
|
|
382
|
-
|
|
383
|
-
const PortalComponent = (props: ReactInlineToolRenderProps<Config>): ReactElement => {
|
|
384
|
-
const handle = useMemo<InlineToolHandle>(
|
|
385
|
-
() => ({
|
|
386
|
-
active: props.active,
|
|
387
|
-
config: props.config,
|
|
388
|
-
api,
|
|
389
|
-
mark: markOps as InlineToolMarkOps | null,
|
|
390
|
-
}),
|
|
391
|
-
[props.active, props.config]
|
|
392
|
-
);
|
|
393
|
-
|
|
394
|
-
return (
|
|
395
|
-
<InlineToolContext.Provider value={handle}>
|
|
396
|
-
<Component active={props.active} config={props.config} />
|
|
397
|
-
</InlineToolContext.Provider>
|
|
398
|
-
);
|
|
399
|
-
};
|
|
400
|
-
|
|
401
|
-
PortalComponent.displayName = `BlokInlineTool(${this.toolName})`;
|
|
402
|
-
|
|
403
|
-
return PortalComponent;
|
|
404
|
-
}
|
|
405
|
-
};
|
|
406
|
-
}
|
package/src/deep-equal.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Re-export of the framework-agnostic deep equality used to dedupe reactive
|
|
3
|
-
* `data` updates. The implementation now lives in `src/shared/deep-equal.ts`
|
|
4
|
-
* (shared with the Angular adapter); this module is kept for back-compat with
|
|
5
|
-
* existing `./deep-equal` importers in the React adapter.
|
|
6
|
-
*/
|
|
7
|
-
export { deepEqual } from '@bloklabs/core/adapters';
|
package/src/holder-map.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Module-level WeakMap associating editor instances with their detached holder divs.
|
|
3
|
-
* WeakMap ensures no memory leaks — when the editor is garbage collected, the holder reference is released.
|
|
4
|
-
*/
|
|
5
|
-
const holders = new WeakMap<WeakKey, HTMLDivElement>();
|
|
6
|
-
|
|
7
|
-
export function setHolder(editor: WeakKey, holder: HTMLDivElement): void {
|
|
8
|
-
holders.set(editor, holder);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function getHolder(editor: WeakKey): HTMLDivElement | undefined {
|
|
12
|
-
return holders.get(editor);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function removeHolder(editor: WeakKey): void {
|
|
16
|
-
holders.delete(editor);
|
|
17
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
export { useBlok } from './useBlok';
|
|
2
|
-
// Host-i18n bridge (#41): re-export the direction/normalization helpers so a
|
|
3
|
-
// host that owns its locale state can compute `dir` and validate tags without
|
|
4
|
-
// deep-importing core. `<BlokEditor locale>` pairs with these.
|
|
5
|
-
export { getDirection, normalizeLocale } from '@bloklabs/core/locales';
|
|
6
|
-
export { BlokContent } from './BlokContent';
|
|
7
|
-
export { BlokEditor } from './BlokEditor';
|
|
8
|
-
export { useBlokHandle } from './useBlokHandle';
|
|
9
|
-
export type { BlokEditorHandle } from './useBlokHandle';
|
|
10
|
-
export { BlokProvider, useBlokDefaults } from './provide-blok';
|
|
11
|
-
export { useBlokInstance } from './blok-instance-context';
|
|
12
|
-
export type { UseBlokConfig, BlokContentProps } from './types';
|
|
13
|
-
// The config/DOM prop split `BlokEditor` applies: a key not in this list is
|
|
14
|
-
// spread onto the container div instead of routing into the editor config.
|
|
15
|
-
// Hosts that filter props themselves must read it from here, not copy it.
|
|
16
|
-
export { USE_BLOK_CONFIG_KEYS } from './config-keys';
|
|
17
|
-
export type { BlokEditorProps } from './BlokEditor';
|
|
18
|
-
export { useBlocks } from './useBlocks';
|
|
19
|
-
export type { UseBlocksOptions } from './useBlocks';
|
|
20
|
-
export { useBlokReady } from './useBlokReady';
|
|
21
|
-
export type { UseBlokReadyOptions } from './useBlokReady';
|
|
22
|
-
export { BlokView } from './BlokView';
|
|
23
|
-
export type { BlokViewProps } from './BlokView';
|
|
24
|
-
export { useBlokView } from './useBlokView';
|
|
25
|
-
export { createReactBlock } from './createReactBlock';
|
|
26
|
-
export type {
|
|
27
|
-
BlockChildrenProps,
|
|
28
|
-
BlockToolStatics,
|
|
29
|
-
ChildAttributes,
|
|
30
|
-
CreateReactBlockSpec,
|
|
31
|
-
ReactBlockMountedContext,
|
|
32
|
-
ReactBlockRenderProps,
|
|
33
|
-
ReactBlockViewProps,
|
|
34
|
-
ReactToolboxConfig,
|
|
35
|
-
ReactToolboxConfigEntry,
|
|
36
|
-
PropSchema,
|
|
37
|
-
PropSchemaEntry,
|
|
38
|
-
} from './createReactBlock';
|
|
39
|
-
export { createReactInlineTool, useInlineTool } from './createReactInlineTool';
|
|
40
|
-
export type {
|
|
41
|
-
CreateReactInlineToolSpec,
|
|
42
|
-
ReactInlineToolRenderProps,
|
|
43
|
-
InlineToolHandle,
|
|
44
|
-
InlineToolMarkOps,
|
|
45
|
-
} from './createReactInlineTool';
|
|
46
|
-
export {
|
|
47
|
-
createBlockPortalRegistry,
|
|
48
|
-
BLOK_PORTAL_REGISTRY_CONFIG_KEY,
|
|
49
|
-
BLOK_TOOL_NAME_CONFIG_KEY,
|
|
50
|
-
} from './block-portal-registry';
|
|
51
|
-
export type { BlockPortalRegistry, BlockPortalEntry } from './block-portal-registry';
|
|
52
|
-
export { BlockPortalHost } from './BlockPortalHost';
|
|
53
|
-
export type { BlockPortalHostProps } from './BlockPortalHost';
|
|
54
|
-
export type {
|
|
55
|
-
BlockNode,
|
|
56
|
-
CaretTarget,
|
|
57
|
-
InsertSpec,
|
|
58
|
-
TreeInsertSpec,
|
|
59
|
-
InsertPosition,
|
|
60
|
-
MoveTarget,
|
|
61
|
-
UseBlocksApi,
|
|
62
|
-
} from './blocks-snapshot';
|
package/src/live-tool-config.ts
DELETED
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Live tool-config functions: the reason a consumer should never have to freeze
|
|
3
|
-
* callback identities (`useState(() => ...)`) or recreate the editor to get
|
|
4
|
-
* fresh closures into tool configs.
|
|
5
|
-
*
|
|
6
|
-
* Core snapshots each tool's config when a block instance is constructed
|
|
7
|
-
* (`ToolClass.settings` builds a merged copy), so a function passed in `tools`
|
|
8
|
-
* at editor construction would otherwise be pinned forever — consumers worked
|
|
9
|
-
* around it by freezing identities and smuggling live values through external
|
|
10
|
-
* stores. Instead, `useBlok` hands core STABLE wrapper functions that delegate
|
|
11
|
-
* to whatever closure the LATEST render passed at the same config path. New
|
|
12
|
-
* render → new closure → the wrapper calls it; the editor is never recreated.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
type AnyFunction = (...args: unknown[]) => unknown;
|
|
16
|
-
|
|
17
|
-
const isPlainObject = (value: unknown): value is Record<string, unknown> => {
|
|
18
|
-
if (typeof value !== 'object' || value === null) {
|
|
19
|
-
return false;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const proto: unknown = Object.getPrototypeOf(value);
|
|
23
|
-
|
|
24
|
-
return proto === Object.prototype || proto === null;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
/** Walk `root` down `path`, returning undefined the moment the shape diverges. */
|
|
28
|
-
const resolveAtPath = (root: unknown, path: readonly (string | number)[]): unknown =>
|
|
29
|
-
path.reduce<unknown>((node, key) => {
|
|
30
|
-
if (Array.isArray(node)) {
|
|
31
|
-
return typeof key === 'number' ? node[key] : undefined;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if (isPlainObject(node)) {
|
|
35
|
-
return node[key];
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return undefined;
|
|
39
|
-
}, root);
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Build the component-facing `config` for a react block from the LATEST render's
|
|
43
|
-
* raw config, preferring the stable live wrapper from `wrappedConfig` wherever
|
|
44
|
-
* both sides hold a function at the same path. The result carries the newest
|
|
45
|
-
* non-function VALUES while keeping function identities stable across renders
|
|
46
|
-
* (wrappers already delegate to the latest closure), so a deep-equal dedupe of
|
|
47
|
-
* successive results only fires when a value genuinely changed.
|
|
48
|
-
*/
|
|
49
|
-
export const buildLiveBlockConfig = (
|
|
50
|
-
wrappedConfig: unknown,
|
|
51
|
-
latestConfig: unknown
|
|
52
|
-
): Record<string, unknown> => {
|
|
53
|
-
const merge = (latest: unknown, wrapped: unknown): unknown => {
|
|
54
|
-
if (typeof latest === 'function') {
|
|
55
|
-
return typeof wrapped === 'function' ? wrapped : latest;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (Array.isArray(latest)) {
|
|
59
|
-
const wrappedArray = Array.isArray(wrapped) ? wrapped : [];
|
|
60
|
-
|
|
61
|
-
return latest.map((item, index) => merge(item, wrappedArray[index]));
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (isPlainObject(latest)) {
|
|
65
|
-
const wrappedObject = isPlainObject(wrapped) ? wrapped : {};
|
|
66
|
-
const result: Record<string, unknown> = {};
|
|
67
|
-
|
|
68
|
-
for (const [key, value] of Object.entries(latest)) {
|
|
69
|
-
result[key] = merge(value, wrappedObject[key]);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return result;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return latest;
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
const merged = merge(latestConfig, wrappedConfig);
|
|
79
|
-
|
|
80
|
-
return isPlainObject(merged) ? merged : {};
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Return a copy of `tools` where every function inside a tool's `config` is
|
|
85
|
-
* replaced by a stable wrapper that calls the function found at the same path
|
|
86
|
-
* in `getLatestTools()` (falling back to the construction-time original when
|
|
87
|
-
* the latest config no longer carries one). Tool classes and non-function
|
|
88
|
-
* values pass through untouched; the consumer's objects are never mutated.
|
|
89
|
-
*/
|
|
90
|
-
export const bindLiveToolConfigFunctions = (tools: unknown, getLatestTools: () => unknown): unknown => {
|
|
91
|
-
if (!isPlainObject(tools)) {
|
|
92
|
-
return tools;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const result: Record<string, unknown> = {};
|
|
96
|
-
|
|
97
|
-
for (const [name, entry] of Object.entries(tools)) {
|
|
98
|
-
if (!isPlainObject(entry) || !isPlainObject(entry.config)) {
|
|
99
|
-
result[name] = entry;
|
|
100
|
-
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const seen = new WeakSet();
|
|
105
|
-
|
|
106
|
-
const wrapNode = (node: unknown, path: readonly (string | number)[]): unknown => {
|
|
107
|
-
if (typeof node === 'function') {
|
|
108
|
-
const original = node as AnyFunction;
|
|
109
|
-
|
|
110
|
-
return function liveConfigFunction(this: unknown, ...args: unknown[]): unknown {
|
|
111
|
-
const latestTools = getLatestTools();
|
|
112
|
-
const latestEntry = isPlainObject(latestTools) ? latestTools[name] : undefined;
|
|
113
|
-
const latestConfig = isPlainObject(latestEntry) ? latestEntry.config : undefined;
|
|
114
|
-
const latest = resolveAtPath(latestConfig, path);
|
|
115
|
-
const target = typeof latest === 'function' ? (latest as AnyFunction) : original;
|
|
116
|
-
|
|
117
|
-
// Preserve the call-site `this` (config functions may be invoked as
|
|
118
|
-
// methods of their config object).
|
|
119
|
-
return Reflect.apply(target, this, args);
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (Array.isArray(node)) {
|
|
124
|
-
if (seen.has(node)) {
|
|
125
|
-
return node;
|
|
126
|
-
}
|
|
127
|
-
seen.add(node);
|
|
128
|
-
|
|
129
|
-
return node.map((item, index) => wrapNode(item, [...path, index]));
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
if (isPlainObject(node)) {
|
|
133
|
-
if (seen.has(node)) {
|
|
134
|
-
return node;
|
|
135
|
-
}
|
|
136
|
-
seen.add(node);
|
|
137
|
-
|
|
138
|
-
const wrapped: Record<string, unknown> = {};
|
|
139
|
-
|
|
140
|
-
for (const [key, value] of Object.entries(node)) {
|
|
141
|
-
wrapped[key] = wrapNode(value, [...path, key]);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
return wrapped;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return node;
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
result[name] = { ...entry, config: wrapNode(entry.config, []) };
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return result;
|
|
154
|
-
};
|