@jackuait/blok 0.23.2 → 0.23.4
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 +3 -3
- package/dist/blok.cjs +1 -1
- package/dist/blok.iife.js +73 -73
- package/dist/blok.mjs +3 -3
- package/dist/blok.umd.js +53 -53
- package/dist/chunks/{blok-Cm1FH7mi.mjs → blok-BydOC-jj.mjs} +2281 -2252
- package/dist/chunks/{blok-9mk1DbT_.cjs → blok-qwtrt5R3.cjs} +13 -13
- package/dist/chunks/{constants-BTnQPoO-.mjs → constants-DZtdGLlr.mjs} +288 -284
- package/dist/chunks/constants-Y3Af_xZ7.cjs +606 -0
- package/dist/chunks/{tools-ZtOAeoEZ.cjs → tools-BmSYWm8L.cjs} +2 -2
- package/dist/chunks/{tools-CJIl8RiU.mjs → tools-DOZaLyYm.mjs} +420 -418
- package/dist/full.cjs +1 -1
- package/dist/full.mjs +3 -3
- package/dist/react.cjs +1 -1
- package/dist/react.mjs +68 -42
- package/dist/tools.cjs +1 -1
- package/dist/tools.mjs +2 -2
- package/package.json +1 -1
- package/src/blok.ts +12 -0
- package/src/components/block/tool-renderer.ts +2 -2
- package/src/components/blocks.ts +32 -10
- package/src/components/constants/test-ids.ts +17 -0
- package/src/components/constants.ts +5 -0
- package/src/components/events/BlockRendered.ts +9 -0
- package/src/components/events/BlocksRendered.ts +13 -0
- package/src/components/events/index.ts +8 -0
- package/src/components/inline-tools/inline-tool-link.ts +23 -9
- package/src/components/modules/api/events.ts +10 -10
- package/src/components/modules/api/index.ts +3 -0
- package/src/components/modules/api/tools.ts +3 -0
- package/src/components/modules/blockManager/blockManager.ts +1 -1
- package/src/components/modules/modificationsObserver.ts +40 -2
- package/src/components/modules/paste/index.ts +10 -1
- package/src/components/modules/renderer.ts +146 -131
- package/src/components/modules/toolbar/plus-button.ts +2 -1
- package/src/components/modules/toolbar/settings-toggler.ts +2 -2
- package/src/components/modules/tools.ts +18 -0
- package/src/components/tools/factory.ts +28 -1
- package/src/react/BlokEditor.tsx +15 -4
- package/src/react/config-keys.ts +3 -0
- package/src/react/deep-equal.ts +48 -0
- package/src/react/types.ts +6 -0
- package/src/react/useBlok.ts +60 -0
- package/types/api/events.d.ts +35 -5
- package/types/api/tools.d.ts +16 -1
- package/types/configs/blok-config.d.ts +54 -0
- package/types/events/editor-events.ts +41 -0
- package/types/index.d.ts +5 -1
- package/types/react.d.ts +2 -0
- package/dist/chunks/constants-CrepXIds.cjs +0 -606
package/src/react/useBlok.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useState, useEffect, useRef, useMemo, type DependencyList } from 'react';
|
|
2
2
|
import { Blok as BlokRuntime } from '../blok';
|
|
3
3
|
import { setHolder, removeHolder } from './holder-map';
|
|
4
|
+
import { deepEqual } from './deep-equal';
|
|
4
5
|
import type { Blok } from '@/types';
|
|
5
6
|
import type { UseBlokConfig } from './types';
|
|
6
7
|
|
|
@@ -42,6 +43,17 @@ export function useBlok(config: UseBlokConfig, deps?: DependencyList): Blok | nu
|
|
|
42
43
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
43
44
|
const depsToken = useMemo(() => ({}), deps ?? []);
|
|
44
45
|
|
|
46
|
+
// Tracks the data the editor currently reflects — set when content is seeded
|
|
47
|
+
// or rendered (the reactive `data` effect below) AND when the editor emits its
|
|
48
|
+
// own serialized content via `onSave`. The latter is what makes a controlled
|
|
49
|
+
// `onSave -> setData -> data` round-trip a no-op: the echoed payload deep-equals
|
|
50
|
+
// this baseline, so the data effect skips the redundant render() (which would
|
|
51
|
+
// otherwise reset the caret). Declared here so the `onSave` wrapper below can
|
|
52
|
+
// update it before the consumer's setState re-runs the data effect.
|
|
53
|
+
const lastRenderedDataRef = useRef(config.data);
|
|
54
|
+
const seededEditorRef = useRef<Blok | null>(null);
|
|
55
|
+
const renderChainRef = useRef<Promise<void>>(Promise.resolve());
|
|
56
|
+
|
|
45
57
|
// Main lifecycle effect
|
|
46
58
|
useEffect(() => {
|
|
47
59
|
if (typeof window === 'undefined') {
|
|
@@ -98,6 +110,19 @@ export function useBlok(config: UseBlokConfig, deps?: DependencyList): Blok | nu
|
|
|
98
110
|
},
|
|
99
111
|
};
|
|
100
112
|
|
|
113
|
+
// Only attach onSave when the consumer opted in: its mere presence makes the
|
|
114
|
+
// core serialize on every change batch, so an absent prop must stay absent.
|
|
115
|
+
// The wrapper reads through the ref so the latest callback is always used.
|
|
116
|
+
if (currentConfig.onSave) {
|
|
117
|
+
blokConfig.onSave = (...args: Parameters<NonNullable<UseBlokConfig['onSave']>>): void => {
|
|
118
|
+
// Record the editor's own serialized output as the rendered baseline so a
|
|
119
|
+
// controlled consumer echoing it straight back into `data` is a no-op —
|
|
120
|
+
// no redundant render(), no caret reset, no round-trip recursion.
|
|
121
|
+
lastRenderedDataRef.current = args[0];
|
|
122
|
+
configRef.current.onSave?.(...args);
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
101
126
|
const blok = new BlokRuntime(blokConfig) as unknown as Blok;
|
|
102
127
|
state.editor = blok;
|
|
103
128
|
setHolder(blok, holder);
|
|
@@ -173,6 +198,41 @@ export function useBlok(config: UseBlokConfig, deps?: DependencyList): Blok | nu
|
|
|
173
198
|
editor.placeholder.set(placeholder);
|
|
174
199
|
}, [editor, placeholder]);
|
|
175
200
|
|
|
201
|
+
// Reactive: data (controlled content)
|
|
202
|
+
//
|
|
203
|
+
// `data` seeds the editor at construction. Afterwards, changing the prop to
|
|
204
|
+
// new *content* re-renders the editor via the public render() API — no
|
|
205
|
+
// recreation. Updates are deep-equal–deduped against the editor's current
|
|
206
|
+
// content baseline (`lastRenderedDataRef`, declared above and also updated by
|
|
207
|
+
// the `onSave` wrapper) so a new reference with the same content — including
|
|
208
|
+
// the editor's own serialized output echoed back — is a no-op and won't
|
|
209
|
+
// clobber the caret. Renders are serialized so rapid changes can't overlap.
|
|
210
|
+
const { data } = config;
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
if (editor === null || data === undefined) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// A freshly created editor was already seeded with `data` at construction;
|
|
217
|
+
// record it without re-rendering.
|
|
218
|
+
if (seededEditorRef.current !== editor) {
|
|
219
|
+
seededEditorRef.current = editor;
|
|
220
|
+
lastRenderedDataRef.current = data;
|
|
221
|
+
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Unchanged content — skip the redundant render.
|
|
226
|
+
if (deepEqual(data, lastRenderedDataRef.current)) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
lastRenderedDataRef.current = data;
|
|
231
|
+
renderChainRef.current = renderChainRef.current
|
|
232
|
+
.catch(() => undefined)
|
|
233
|
+
.then(() => editor.render(data));
|
|
234
|
+
}, [editor, data]);
|
|
235
|
+
|
|
176
236
|
return editor;
|
|
177
237
|
}
|
|
178
238
|
|
package/types/api/events.d.ts
CHANGED
|
@@ -1,17 +1,39 @@
|
|
|
1
|
+
import { BlokEditorEventMap } from '../events/editor-events';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
|
-
* Describes Blok`s events API
|
|
4
|
+
* Describes Blok`s events API.
|
|
5
|
+
*
|
|
6
|
+
* Well-known editor lifecycle events (see {@link BlokEditorEventMap}, e.g.
|
|
7
|
+
* `'blocks:rendered'` and `'block:rendered'`) get fully typed payloads.
|
|
8
|
+
* Arbitrary string event names are still accepted for custom events.
|
|
3
9
|
*/
|
|
4
10
|
export interface Events {
|
|
5
11
|
/**
|
|
6
|
-
* Emits event
|
|
12
|
+
* Emits a typed editor lifecycle event.
|
|
13
|
+
*
|
|
14
|
+
* @param eventName - one of the well-known editor event names
|
|
15
|
+
* @param data - payload matching the event
|
|
16
|
+
*/
|
|
17
|
+
emit<Name extends keyof BlokEditorEventMap>(eventName: Name, data: BlokEditorEventMap[Name]): void;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Emits an event.
|
|
7
21
|
*
|
|
8
22
|
* @param {string} eventName
|
|
9
23
|
* @param {any} data
|
|
10
24
|
*/
|
|
11
|
-
emit(eventName: string, data
|
|
25
|
+
emit(eventName: string, data?: any): void;
|
|
12
26
|
|
|
13
27
|
/**
|
|
14
|
-
* Unsubscribe from event
|
|
28
|
+
* Unsubscribe from a typed editor lifecycle event.
|
|
29
|
+
*
|
|
30
|
+
* @param eventName - one of the well-known editor event names
|
|
31
|
+
* @param callback - the handler to remove
|
|
32
|
+
*/
|
|
33
|
+
off<Name extends keyof BlokEditorEventMap>(eventName: Name, callback: (data: BlokEditorEventMap[Name]) => void): void;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Unsubscribe from event.
|
|
15
37
|
*
|
|
16
38
|
* @param {string} eventName
|
|
17
39
|
* @param {(data: any) => void} callback
|
|
@@ -19,7 +41,15 @@ export interface Events {
|
|
|
19
41
|
off(eventName: string, callback: (data?: any) => void): void;
|
|
20
42
|
|
|
21
43
|
/**
|
|
22
|
-
* Subscribe to event
|
|
44
|
+
* Subscribe to a typed editor lifecycle event.
|
|
45
|
+
*
|
|
46
|
+
* @param eventName - one of the well-known editor event names
|
|
47
|
+
* @param callback - receives a typed payload
|
|
48
|
+
*/
|
|
49
|
+
on<Name extends keyof BlokEditorEventMap>(eventName: Name, callback: (data: BlokEditorEventMap[Name]) => void): void;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Subscribe to event.
|
|
23
53
|
*
|
|
24
54
|
* @param {string} eventName
|
|
25
55
|
* @param {(data: any) => void} callback
|
package/types/api/tools.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BlockToolAdapter } from '../tools/adapters/block-tool-adapter';
|
|
2
|
-
import { ToolConstructable, ToolSettings } from '../tools';
|
|
2
|
+
import { ToolConfig, ToolConstructable, ToolSettings } from '../tools';
|
|
3
3
|
import { ThemeMode } from './theme';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -26,4 +26,19 @@ export interface Tools {
|
|
|
26
26
|
* Useful for creating nested Blok editors with the same tools.
|
|
27
27
|
*/
|
|
28
28
|
getToolsConfig(): ToolsConfig;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Shallow-merges new configuration into an already-installed tool, without
|
|
32
|
+
* recreating the editor. Useful for swapping config at runtime — e.g. pointing
|
|
33
|
+
* an image tool at a new uploader function.
|
|
34
|
+
*
|
|
35
|
+
* The merge targets the tool's `config` object (the part passed to the tool
|
|
36
|
+
* constructor). Blocks created after the call (and the next operation on the
|
|
37
|
+
* live tool) pick up the new config; blocks already mounted keep their current
|
|
38
|
+
* config until they are re-rendered.
|
|
39
|
+
* @param name - registered tool name
|
|
40
|
+
* @param config - partial tool config to merge in
|
|
41
|
+
* @throws if `name` is not a registered tool
|
|
42
|
+
*/
|
|
43
|
+
update(name: string, config: Partial<ToolConfig>): void;
|
|
29
44
|
}
|
|
@@ -59,6 +59,17 @@ export interface BlokConfig {
|
|
|
59
59
|
*/
|
|
60
60
|
sanitizer?: SanitizerConfig;
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Transform or clean the raw clipboard HTML before Blok processes it.
|
|
64
|
+
* Runs on the unmodified `text/html` payload, before any Blok preprocessing
|
|
65
|
+
* or sanitization, so you no longer need a capture-phase paste interceptor.
|
|
66
|
+
*
|
|
67
|
+
* @param html - the raw `text/html` clipboard string
|
|
68
|
+
* @returns the transformed HTML to feed into the rest of the paste pipeline,
|
|
69
|
+
* or `null` to skip the HTML paste path (paste falls through to plain text).
|
|
70
|
+
*/
|
|
71
|
+
onBeforePaste?: (html: string) => string | null;
|
|
72
|
+
|
|
62
73
|
/**
|
|
63
74
|
* If true, toolbar won't be shown
|
|
64
75
|
*/
|
|
@@ -124,6 +135,32 @@ export interface BlokConfig {
|
|
|
124
135
|
*/
|
|
125
136
|
i18n?: I18nConfig;
|
|
126
137
|
|
|
138
|
+
/**
|
|
139
|
+
* Configures how the Link inline tool builds anchor elements, so consumers can
|
|
140
|
+
* control the created `<a>` instead of post-processing the rendered DOM.
|
|
141
|
+
*/
|
|
142
|
+
link?: {
|
|
143
|
+
/**
|
|
144
|
+
* `target` attribute applied to created anchors.
|
|
145
|
+
* @default '_blank'
|
|
146
|
+
*/
|
|
147
|
+
target?: string;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* `rel` attribute applied to created anchors.
|
|
151
|
+
* @default 'nofollow'
|
|
152
|
+
*/
|
|
153
|
+
rel?: string;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Transforms the href just before it is assigned to the anchor.
|
|
157
|
+
* Runs after URL validation/normalization, on the final value.
|
|
158
|
+
* @param href - the validated, protocol-normalized href
|
|
159
|
+
* @returns the href to set on the anchor
|
|
160
|
+
*/
|
|
161
|
+
transformHref?: (href: string) => string;
|
|
162
|
+
};
|
|
163
|
+
|
|
127
164
|
/**
|
|
128
165
|
* Notion-style link-paste behavior. Pasting a URL always opens a small menu
|
|
129
166
|
* (Plain link / Bookmark / Embed) instead of auto-inserting a block.
|
|
@@ -150,6 +187,23 @@ export interface BlokConfig {
|
|
|
150
187
|
*/
|
|
151
188
|
onChange?(api: API, event: BlockMutationEvent | BlockMutationEvent[]): void;
|
|
152
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Fires with the full serialized {@link OutputData} whenever the content
|
|
192
|
+
* changes. This is the "output half" of a controlled editor: pair it with the
|
|
193
|
+
* `data` config (or the React `data` prop) to mirror the editor state into your
|
|
194
|
+
* own store with a single callback instead of calling `saver.save()` by hand.
|
|
195
|
+
*
|
|
196
|
+
* Serialization is debounced through the same change-batching window as
|
|
197
|
+
* `onChange`, so a burst of edits produces a single `onSave` call. Only
|
|
198
|
+
* user-driven content changes trigger it — programmatic `render()` does not
|
|
199
|
+
* (the change observer is disabled during render), so a controlled
|
|
200
|
+
* `data → render → onSave → setData` round-trip won't recurse.
|
|
201
|
+
*
|
|
202
|
+
* @param data - the full serialized output of the editor
|
|
203
|
+
* @param api - blok.js api
|
|
204
|
+
*/
|
|
205
|
+
onSave?(data: OutputData, api: API): void;
|
|
206
|
+
|
|
153
207
|
/**
|
|
154
208
|
* Defines default toolbar for all tools.
|
|
155
209
|
*/
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public payloads and name map for editor lifecycle events observable via
|
|
3
|
+
* `blok.events.on(...)`.
|
|
4
|
+
*
|
|
5
|
+
* These complement the mutation events delivered through the `onChange`
|
|
6
|
+
* config callback ({@link ./block}). Use together with the exported event-name
|
|
7
|
+
* constants `BlockRendered` (`'block:rendered'`) and `BlocksRendered`
|
|
8
|
+
* (`'blocks:rendered'`).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Payload for the `block:rendered` event.
|
|
13
|
+
*/
|
|
14
|
+
export interface BlockRenderedPayload {
|
|
15
|
+
/**
|
|
16
|
+
* Id of the block that has just been rendered into the DOM.
|
|
17
|
+
* Use `blok.blocks.getById(blockId)` to access it.
|
|
18
|
+
*/
|
|
19
|
+
blockId: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Payload for the `blocks:rendered` event.
|
|
24
|
+
*/
|
|
25
|
+
export interface BlocksRenderedPayload {
|
|
26
|
+
/**
|
|
27
|
+
* Number of top-level blocks rendered in the completed batch.
|
|
28
|
+
*/
|
|
29
|
+
count: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Map of editor lifecycle event name -> payload.
|
|
34
|
+
*
|
|
35
|
+
* Subscribers get fully typed payloads for these well-known events while the
|
|
36
|
+
* `Events` API still accepts arbitrary string event names for custom events.
|
|
37
|
+
*/
|
|
38
|
+
export interface BlokEditorEventMap {
|
|
39
|
+
'block:rendered': BlockRenderedPayload;
|
|
40
|
+
'blocks:rendered': BlocksRenderedPayload;
|
|
41
|
+
}
|
package/types/index.d.ts
CHANGED
|
@@ -42,6 +42,7 @@ import { BlockAddedMutationType, BlockAddedEvent } from './events/block/BlockAdd
|
|
|
42
42
|
import { BlockChangedMutationType, BlockChangedEvent } from './events/block/BlockChanged';
|
|
43
43
|
import { BlockMovedMutationType, BlockMovedEvent } from './events/block/BlockMoved';
|
|
44
44
|
import { BlockRemovedMutationType, BlockRemovedEvent } from './events/block/BlockRemoved';
|
|
45
|
+
import { BlokEditorEventMap, BlockRenderedPayload, BlocksRenderedPayload } from './events/editor-events';
|
|
45
46
|
|
|
46
47
|
/**
|
|
47
48
|
* Interfaces used for development
|
|
@@ -144,6 +145,9 @@ export {
|
|
|
144
145
|
BlockMovedEvent,
|
|
145
146
|
BlockChangedMutationType,
|
|
146
147
|
BlockChangedEvent,
|
|
148
|
+
BlokEditorEventMap,
|
|
149
|
+
BlockRenderedPayload,
|
|
150
|
+
BlocksRenderedPayload,
|
|
147
151
|
}
|
|
148
152
|
|
|
149
153
|
/**
|
|
@@ -170,7 +174,7 @@ export interface API {
|
|
|
170
174
|
ui: Ui;
|
|
171
175
|
theme: Theme;
|
|
172
176
|
/** Read-only view of selected editor configuration. */
|
|
173
|
-
config: Readonly<Pick<BlokConfig, 'linkPaste'>>;
|
|
177
|
+
config: Readonly<Pick<BlokConfig, 'linkPaste' | 'link'>>;
|
|
174
178
|
rectangleSelection: {
|
|
175
179
|
cancelActiveSelection: () => void;
|
|
176
180
|
isRectActivated: () => boolean;
|
package/types/react.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ import type React from 'react';
|
|
|
12
12
|
* - `theme` — calls `editor.theme.set(value)`
|
|
13
13
|
* - `width` — calls `editor.width.set(value)`
|
|
14
14
|
* - `placeholder` — calls `editor.placeholder.set(value)`
|
|
15
|
+
* - `data` — re-renders via `editor.render(value)` when the content changes
|
|
16
|
+
* (deep-equal–deduped and serialized; seeds the initial content at creation)
|
|
15
17
|
*
|
|
16
18
|
* All other config is consumed once at editor creation.
|
|
17
19
|
*/
|