@jackuait/blok 0.23.1 → 0.23.3

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.
Files changed (49) hide show
  1. package/README.md +3 -1
  2. package/dist/blok.cjs +1 -1
  3. package/dist/blok.iife.js +73 -73
  4. package/dist/blok.mjs +3 -3
  5. package/dist/blok.umd.js +53 -53
  6. package/dist/chunks/{blok-BPeM6wKv.mjs → blok-1H9roBGf.mjs} +2271 -2249
  7. package/dist/chunks/{blok-Ci1zCOUg.cjs → blok-J3Lfa_bK.cjs} +13 -13
  8. package/dist/chunks/{constants-DR3sya6U.mjs → constants-BJ79cqMa.mjs} +288 -284
  9. package/dist/chunks/constants-BZUc1kAY.cjs +606 -0
  10. package/dist/chunks/{tools-B72rczVw.mjs → tools-ByOjWADT.mjs} +420 -418
  11. package/dist/chunks/{tools-DlOZpcVU.cjs → tools-OHDv2C7w.cjs} +2 -2
  12. package/dist/full.cjs +1 -1
  13. package/dist/full.mjs +3 -3
  14. package/dist/react.cjs +1 -1
  15. package/dist/react.mjs +54 -33
  16. package/dist/tools.cjs +1 -1
  17. package/dist/tools.mjs +2 -2
  18. package/package.json +1 -1
  19. package/src/blok.ts +12 -0
  20. package/src/components/block/tool-renderer.ts +2 -2
  21. package/src/components/blocks.ts +32 -10
  22. package/src/components/constants/test-ids.ts +17 -0
  23. package/src/components/constants.ts +5 -0
  24. package/src/components/events/BlockRendered.ts +9 -0
  25. package/src/components/events/BlocksRendered.ts +13 -0
  26. package/src/components/events/index.ts +8 -0
  27. package/src/components/inline-tools/inline-tool-link.ts +23 -9
  28. package/src/components/modules/api/events.ts +10 -10
  29. package/src/components/modules/api/index.ts +3 -0
  30. package/src/components/modules/api/tools.ts +3 -0
  31. package/src/components/modules/blockManager/blockManager.ts +1 -1
  32. package/src/components/modules/paste/index.ts +10 -1
  33. package/src/components/modules/renderer.ts +146 -131
  34. package/src/components/modules/toolbar/plus-button.ts +2 -1
  35. package/src/components/modules/toolbar/settings-toggler.ts +2 -2
  36. package/src/components/modules/tools.ts +18 -0
  37. package/src/components/tools/factory.ts +28 -1
  38. package/src/react/BlokEditor.tsx +17 -5
  39. package/src/react/config-keys.ts +2 -0
  40. package/src/react/deep-equal.ts +48 -0
  41. package/src/react/types.ts +2 -0
  42. package/src/react/useBlok.ts +37 -0
  43. package/types/api/events.d.ts +35 -5
  44. package/types/api/tools.d.ts +16 -1
  45. package/types/configs/blok-config.d.ts +37 -0
  46. package/types/events/editor-events.ts +41 -0
  47. package/types/index.d.ts +5 -1
  48. package/types/react.d.ts +13 -2
  49. package/dist/chunks/constants-Bkncy0y5.cjs +0 -606
@@ -13,6 +13,7 @@ import {
13
13
  type DataFormatAnalysis,
14
14
  } from '../utils/data-model-transform';
15
15
  import { migrateMarkColors } from '../utils/color-migration';
16
+ import { BlocksRendered } from '../events';
16
17
 
17
18
  /**
18
19
  * Map of legacy EditorJS tool names to their Blok equivalents.
@@ -86,146 +87,160 @@ export class Renderer extends Module {
86
87
  */
87
88
  public render(blocksData: OutputBlockData[]): Promise<void> {
88
89
  return new Promise((resolve) => {
89
- const { Tools, BlockManager } = this.Blok;
90
-
91
- if (blocksData.length === 0) {
92
- BlockManager.insert();
93
- } else {
94
- // Analyze and potentially transform the input data
95
- const dataModelConfig = this.config.dataModel || 'auto';
96
- const analysis = analyzeDataFormat(blocksData);
97
- this.detectedInputFormat = analysis.format;
98
-
99
- // Transform to hierarchical if config requires it
100
- const expandedBlocks = shouldExpandToHierarchical(dataModelConfig, analysis.format)
101
- ? expandToHierarchical(blocksData)
102
- : blocksData;
103
-
104
- // Recover migrated cells whose text a pre-fix save detached to root:
105
- // re-attach `cell-<row>-<col>`-id orphans back into their empty cell.
106
- // Runs before normalize so reclaimed refs get parented in the same pass.
107
- const reclaimedBlocks = reclaimDetachedTableCells(expandedBlocks);
108
-
109
- // Tables persist child references via `data.content[r][c].blocks = [<id>]`
110
- // rather than an explicit `parent` field on each child. Pre-normalize
111
- // those parent references so downstream code that gates on parentId
112
- // (read-only cell mounter, saver filter, hierarchy queries) correctly
113
- // recognizes the children as belonging to their table.
114
- const processedBlocks = normalizeTableChildParents(reclaimedBlocks);
115
-
116
- // Note: Yjs data layer is loaded via BlockManager.insertMany() with the correct block IDs
117
-
118
- /**
119
- * Track seen IDs to detect and resolve duplicates
120
- */
121
- const seenIds = new Set<string>();
122
-
123
- /**
124
- * Create Blocks instances
125
- */
126
- const blocks = processedBlocks.map((blockData: OutputBlockData) => {
127
- const { tunes, parent, content, lastEditedAt, lastEditedBy } = blockData;
128
- const hasDuplicateId = blockData.id !== undefined && seenIds.has(blockData.id);
129
-
130
- if (hasDuplicateId) {
131
- logLabeled(`Duplicate block id «${blockData.id}» replaced with a generated id to ensure uniqueness`, 'warn');
132
- }
133
-
134
- const id = hasDuplicateId ? generateBlockId() : blockData.id;
135
-
136
- if (id !== undefined) {
137
- seenIds.add(id);
138
- }
139
- const originalTool = blockData.type;
90
+ const renderedCount = this.insertRenderedBlocks(blocksData);
140
91
 
141
- /**
142
- * Validate that block data has the expected shape.
143
- * Since OutputBlockData<Data> defaults to `any` for Data, we need to narrow the type.
144
- */
145
- const isValidBlockData = (data: unknown): data is Record<string, unknown> => {
146
- return typeof data === 'object' && data !== null;
147
- };
92
+ /**
93
+ * Wait till browser will render inserted Blocks and resolve a promise
94
+ */
95
+ window.requestIdleCallback(() => {
96
+ this.eventsDispatcher.emit(BlocksRendered, { count: renderedCount });
97
+ resolve();
98
+ }, { timeout: 2000 });
99
+ });
100
+ }
148
101
 
149
- const blockToolData = isValidBlockData(blockData.data) ? blockData.data : {};
150
-
151
- const availabilityResult = (() => {
152
- if (Tools.available.has(originalTool)) {
153
- return {
154
- tool: originalTool,
155
- data: blockToolData,
156
- };
157
- }
158
-
159
- const aliasTarget = TOOL_ALIASES[originalTool];
160
-
161
- if (aliasTarget !== undefined && Tools.available.has(aliasTarget)) {
162
- return {
163
- tool: aliasTarget,
164
- data: blockToolData,
165
- };
166
- }
167
-
168
- logLabeled(`Tool «${originalTool}» is not found. Check 'tools' property at the Blok config.`, 'warn');
169
-
170
- return {
171
- tool: Tools.stubTool,
172
- data: this.composeStubDataForTool(originalTool, blockToolData, id),
173
- };
174
- })();
175
-
176
- const buildBlock = (tool: string, data: BlockToolData): Block => {
177
- try {
178
- return BlockManager.composeBlock({
179
- id,
180
- tool,
181
- data,
182
- tunes,
183
- parentId: parent,
184
- contentIds: content,
185
- lastEditedAt,
186
- lastEditedBy,
187
- });
188
- } catch (error) {
189
- log(`Block «${tool}» skipped because of plugins error`, 'error', {
190
- data,
191
- error,
192
- });
193
-
194
- /**
195
- * If tool throws an error during render, we should render stub instead of it
196
- */
197
- const stubData = this.composeStubDataForTool(tool, data, id);
198
-
199
- return BlockManager.composeBlock({
200
- id,
201
- tool: Tools.stubTool,
202
- data: stubData,
203
- tunes,
204
- parentId: parent,
205
- contentIds: content,
206
- lastEditedAt,
207
- lastEditedBy,
208
- });
209
- }
210
- };
102
+ /**
103
+ * Inserts the given blocks (or a single default block when the input is
104
+ * empty) and returns the number of top-level blocks rendered in this batch.
105
+ * @param blocksData - blocks to render
106
+ */
107
+ private insertRenderedBlocks(blocksData: OutputBlockData[]): number {
108
+ const { Tools, BlockManager } = this.Blok;
109
+
110
+ if (blocksData.length === 0) {
111
+ BlockManager.insert();
211
112
 
212
- return buildBlock(availabilityResult.tool, availabilityResult.data);
213
- });
113
+ return 1;
114
+ }
214
115
 
215
- /**
216
- * Insert batch of Blocks
217
- */
218
- BlockManager.insertMany(blocks);
219
- migrateMarkColors(this.Blok.UI.nodes.redactor);
116
+ // Analyze and potentially transform the input data
117
+ const dataModelConfig = this.config.dataModel || 'auto';
118
+ const analysis = analyzeDataFormat(blocksData);
119
+ this.detectedInputFormat = analysis.format;
120
+
121
+ // Transform to hierarchical if config requires it
122
+ const expandedBlocks = shouldExpandToHierarchical(dataModelConfig, analysis.format)
123
+ ? expandToHierarchical(blocksData)
124
+ : blocksData;
125
+
126
+ // Recover migrated cells whose text a pre-fix save detached to root:
127
+ // re-attach `cell-<row>-<col>`-id orphans back into their empty cell.
128
+ // Runs before normalize so reclaimed refs get parented in the same pass.
129
+ const reclaimedBlocks = reclaimDetachedTableCells(expandedBlocks);
130
+
131
+ // Tables persist child references via `data.content[r][c].blocks = [<id>]`
132
+ // rather than an explicit `parent` field on each child. Pre-normalize
133
+ // those parent references so downstream code that gates on parentId
134
+ // (read-only cell mounter, saver filter, hierarchy queries) correctly
135
+ // recognizes the children as belonging to their table.
136
+ const processedBlocks = normalizeTableChildParents(reclaimedBlocks);
137
+
138
+ // Note: Yjs data layer is loaded via BlockManager.insertMany() with the correct block IDs
139
+
140
+ /**
141
+ * Track seen IDs to detect and resolve duplicates
142
+ */
143
+ const seenIds = new Set<string>();
144
+
145
+ /**
146
+ * Create Blocks instances
147
+ */
148
+ const blocks = processedBlocks.map((blockData: OutputBlockData) => {
149
+ const { tunes, parent, content, lastEditedAt, lastEditedBy } = blockData;
150
+ const hasDuplicateId = blockData.id !== undefined && seenIds.has(blockData.id);
151
+
152
+ if (hasDuplicateId) {
153
+ logLabeled(`Duplicate block id «${blockData.id}» replaced with a generated id to ensure uniqueness`, 'warn');
220
154
  }
221
155
 
156
+ const id = hasDuplicateId ? generateBlockId() : blockData.id;
157
+
158
+ if (id !== undefined) {
159
+ seenIds.add(id);
160
+ }
161
+ const originalTool = blockData.type;
162
+
222
163
  /**
223
- * Wait till browser will render inserted Blocks and resolve a promise
164
+ * Validate that block data has the expected shape.
165
+ * Since OutputBlockData<Data> defaults to `any` for Data, we need to narrow the type.
224
166
  */
225
- window.requestIdleCallback(() => {
226
- resolve();
227
- }, { timeout: 2000 });
167
+ const isValidBlockData = (data: unknown): data is Record<string, unknown> => {
168
+ return typeof data === 'object' && data !== null;
169
+ };
170
+
171
+ const blockToolData = isValidBlockData(blockData.data) ? blockData.data : {};
172
+
173
+ const availabilityResult = (() => {
174
+ if (Tools.available.has(originalTool)) {
175
+ return {
176
+ tool: originalTool,
177
+ data: blockToolData,
178
+ };
179
+ }
180
+
181
+ const aliasTarget = TOOL_ALIASES[originalTool];
182
+
183
+ if (aliasTarget !== undefined && Tools.available.has(aliasTarget)) {
184
+ return {
185
+ tool: aliasTarget,
186
+ data: blockToolData,
187
+ };
188
+ }
189
+
190
+ logLabeled(`Tool «${originalTool}» is not found. Check 'tools' property at the Blok config.`, 'warn');
191
+
192
+ return {
193
+ tool: Tools.stubTool,
194
+ data: this.composeStubDataForTool(originalTool, blockToolData, id),
195
+ };
196
+ })();
197
+
198
+ const buildBlock = (tool: string, data: BlockToolData): Block => {
199
+ try {
200
+ return BlockManager.composeBlock({
201
+ id,
202
+ tool,
203
+ data,
204
+ tunes,
205
+ parentId: parent,
206
+ contentIds: content,
207
+ lastEditedAt,
208
+ lastEditedBy,
209
+ });
210
+ } catch (error) {
211
+ log(`Block «${tool}» skipped because of plugins error`, 'error', {
212
+ data,
213
+ error,
214
+ });
215
+
216
+ /**
217
+ * If tool throws an error during render, we should render stub instead of it
218
+ */
219
+ const stubData = this.composeStubDataForTool(tool, data, id);
220
+
221
+ return BlockManager.composeBlock({
222
+ id,
223
+ tool: Tools.stubTool,
224
+ data: stubData,
225
+ tunes,
226
+ parentId: parent,
227
+ contentIds: content,
228
+ lastEditedAt,
229
+ lastEditedBy,
230
+ });
231
+ }
232
+ };
233
+
234
+ return buildBlock(availabilityResult.tool, availabilityResult.data);
228
235
  });
236
+
237
+ /**
238
+ * Insert batch of Blocks
239
+ */
240
+ BlockManager.insertMany(blocks);
241
+ migrateMarkColors(this.Blok.UI.nodes.redactor);
242
+
243
+ return blocks.length;
229
244
  }
230
245
 
231
246
  /**
@@ -1,5 +1,6 @@
1
1
  import type { BlokModules } from '../../../types-internal/blok-modules';
2
2
  import type { Block } from '../../block';
3
+ import { DATA_ATTR, TEST_ID } from '../../constants';
3
4
  import { Dom as $ } from '../../dom';
4
5
  import { IconPlus } from '../../icons';
5
6
  import { SelectionUtils } from '../../selection/index';
@@ -126,7 +127,7 @@ export class PlusButtonHandler {
126
127
  innerHTML: IconPlus,
127
128
  });
128
129
 
129
- plusButton.setAttribute('data-blok-testid', 'plus-button');
130
+ plusButton.setAttribute(DATA_ATTR.testid, TEST_ID.plusButton);
130
131
 
131
132
  // eslint-disable-next-line no-param-reassign -- nodes is mutated by design
132
133
  nodes.plusButton = plusButton;
@@ -1,6 +1,6 @@
1
1
  import type { BlokModules } from '../../../types-internal/blok-modules';
2
2
  import type { Block } from '../../block';
3
- import { DATA_ATTR } from '../../constants';
3
+ import { DATA_ATTR, TEST_ID } from '../../constants';
4
4
  import { Dom as $ } from '../../dom';
5
5
  import { IconMenu } from '../../icons';
6
6
  import { getUserOS } from '../../utils';
@@ -136,7 +136,7 @@ export class SettingsTogglerHandler {
136
136
 
137
137
  settingsToggler.setAttribute(DATA_ATTR.settingsToggler, '');
138
138
  settingsToggler.setAttribute(DATA_ATTR.dragHandle, '');
139
- settingsToggler.setAttribute('data-blok-testid', 'settings-toggler');
139
+ settingsToggler.setAttribute(DATA_ATTR.testid, TEST_ID.settingsToggler);
140
140
 
141
141
  // Accessibility: make the drag handle accessible to screen readers
142
142
  // Using tabindex="-1" keeps it accessible but removes from tab order
@@ -212,6 +212,24 @@ export class Tools extends Module {
212
212
  this.prepareBlockTools();
213
213
  }
214
214
 
215
+ /**
216
+ * Shallow-merges new user configuration into an already-prepared tool.
217
+ *
218
+ * Lets consumers swap config (e.g. an uploader) at runtime without recreating
219
+ * the editor. The stored config is mutated in place, so the live tool adapter
220
+ * and any block created afterwards pick up the new values. Blocks already
221
+ * mounted keep their current config until they are re-rendered.
222
+ * @param name - tool name
223
+ * @param config - partial tool config to merge in
224
+ */
225
+ public updateToolConfig(name: string, config: Partial<ToolConfig>): void {
226
+ if (!this.available.has(name) && !this.unavailable.has(name)) {
227
+ throw new Error(`Tool "${name}" is not registered.`);
228
+ }
229
+
230
+ this.getFactory().updateConfig(name, config);
231
+ }
232
+
215
233
  /**
216
234
  * Return general Sanitizer config for all inline tools
217
235
  */
@@ -1,6 +1,6 @@
1
1
  import type { API as ApiMethods, I18n } from '../../../types';
2
2
  import type { BlokConfig } from '../../../types/configs';
3
- import type { ToolConstructable, ToolSettings } from '../../../types/tools';
3
+ import type { ToolConfig, ToolConstructable, ToolSettings } from '../../../types/tools';
4
4
  import type { API as ApiModule } from '../modules/api';
5
5
 
6
6
  import { InternalInlineToolSettings, InternalTuneSettings } from './base';
@@ -45,6 +45,33 @@ export class ToolsFactory {
45
45
  this.blokConfig = blokConfig;
46
46
  }
47
47
 
48
+ /**
49
+ * Shallow-merges new user configuration into a registered tool's stored config.
50
+ *
51
+ * The merge targets the tool's nested `config` object (the part passed to the
52
+ * tool constructor) and mutates it IN PLACE. Adapters read their config lazily
53
+ * and share this exact object reference, so the live adapter — and any freshly
54
+ * built adapter — both observe the new values immediately.
55
+ * @param name - tool name
56
+ * @param config - partial tool config to merge in
57
+ */
58
+ public updateConfig(name: string, config: Partial<ToolConfig>): void {
59
+ const settings = this.config[name];
60
+
61
+ if (settings === undefined) {
62
+ throw new Error(`Tool "${name}" is not registered.`);
63
+ }
64
+
65
+ // eslint-disable-next-line @typescript-eslint/no-deprecated -- Internal: mutating legacy nested config in place to keep live + future adapters in sync
66
+ if (settings.config === undefined) {
67
+ // eslint-disable-next-line @typescript-eslint/no-deprecated -- Internal: initialize nested config object before merging
68
+ settings.config = {};
69
+ }
70
+
71
+ // eslint-disable-next-line @typescript-eslint/no-deprecated -- Internal: in-place merge so the shared nested config reference reflects the update
72
+ Object.assign(settings.config, config);
73
+ }
74
+
48
75
  /**
49
76
  * Returns Tool object based on it's type
50
77
  * @param name - tool name
@@ -20,15 +20,22 @@ import type { UseBlokConfig } from './types';
20
20
  * keep their editor-config meaning (they collide with div attributes of the same
21
21
  * name), so the container is styled via `className` rather than an inline `style`.
22
22
  *
23
- * `data` is uncontrolled (seed-only): it sets the INITIAL content. After mount the
24
- * editor owns the document passing a new `data` reference does NOT reload it.
25
- * Read content via `onChange` or the ref (`ref.current.save()`); replace it
26
- * imperatively via `ref.current.render(newData)`.
23
+ * `data` is reactive (controlled-ish): it seeds the INITIAL content and, after
24
+ * mount, changing it to new *content* re-renders the editor via `render()` no
25
+ * recreation. Updates are deep-equal–deduped (a new reference with identical
26
+ * content is a no-op, so it won't clobber the caret) and serialized (rapid
27
+ * changes can't overlap). Read content via `onChange` or the ref
28
+ * (`ref.current.save()`). For ad-hoc reloads you can still call
29
+ * `ref.current.render(newData)`.
27
30
  */
28
31
  export interface BlokEditorProps
29
32
  extends Omit<UseBlokConfig, 'onReady'>,
30
33
  Omit<HTMLAttributes<HTMLDivElement>, 'style' | 'onChange'> {
31
- /** When any value changes, the editor is destroyed and recreated. */
34
+ /**
35
+ * When any value changes, the editor is destroyed and recreated. Keep each
36
+ * value referentially stable (primitives or useMemo-stable objects) — a dep
37
+ * whose identity changes every render recreates the editor each time.
38
+ */
32
39
  deps?: DependencyList;
33
40
  /** Test id forwarded to the editor container element (via data-testid). */
34
41
  'data-testid'?: string;
@@ -46,6 +53,11 @@ const CONFIG_KEY_SET = new Set<string>(USE_BLOK_CONFIG_KEYS);
46
53
  * The recommended way to embed Blok in React. Internally wires `useBlok` and
47
54
  * `BlokContent`, and forwards a ref to the live `Blok` instance.
48
55
  *
56
+ * Don't wrap this component in `styled()` or any HOC that reserves the `theme`
57
+ * prop — styled-components claims `theme` for its own `ThemeProvider`, so it
58
+ * never reaches the editor and theme sync silently breaks. Render it directly
59
+ * and style the container via `className`.
60
+ *
49
61
  * @example
50
62
  * ```tsx
51
63
  * const ref = useRef<Blok | null>(null);
@@ -25,7 +25,9 @@ export const USE_BLOK_CONFIG_KEYS = [
25
25
  'logLevel',
26
26
  'readOnly',
27
27
  'i18n',
28
+ 'link',
28
29
  'linkPaste',
30
+ 'onBeforePaste',
29
31
  'onReady',
30
32
  'onChange',
31
33
  'inlineToolbar',
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Structural deep equality for JSON-like values (Blok `OutputData`).
3
+ *
4
+ * Used to dedupe reactive `data` updates so identical content does not trigger
5
+ * a redundant re-render that would clobber the caret/selection. Handles the
6
+ * value shapes that appear in editor data: plain objects, arrays, and
7
+ * primitives (including `null`/`undefined`).
8
+ * @param a - first value to compare
9
+ * @param b - second value to compare
10
+ * @returns true when both values are structurally equal
11
+ */
12
+ export function deepEqual(a: unknown, b: unknown): boolean {
13
+ if (a === b) {
14
+ return true;
15
+ }
16
+
17
+ if (typeof a !== 'object' || a === null || typeof b !== 'object' || b === null) {
18
+ return false;
19
+ }
20
+
21
+ const aIsArray = Array.isArray(a);
22
+ const bIsArray = Array.isArray(b);
23
+
24
+ if (aIsArray !== bIsArray) {
25
+ return false;
26
+ }
27
+
28
+ if (aIsArray && bIsArray) {
29
+ if (a.length !== b.length) {
30
+ return false;
31
+ }
32
+
33
+ return a.every((item, index) => deepEqual(item, b[index]));
34
+ }
35
+
36
+ const aObj = a as Record<string, unknown>;
37
+ const bObj = b as Record<string, unknown>;
38
+ const aKeys = Object.keys(aObj);
39
+ const bKeys = Object.keys(bObj);
40
+
41
+ if (aKeys.length !== bKeys.length) {
42
+ return false;
43
+ }
44
+
45
+ return aKeys.every(
46
+ (key) => Object.prototype.hasOwnProperty.call(bObj, key) && deepEqual(aObj[key], bObj[key])
47
+ );
48
+ }
@@ -11,6 +11,8 @@ import type { BlokConfig, Blok, EditorWidth } from '@/types';
11
11
  * - `theme` — calls `editor.theme.set(value)`
12
12
  * - `width` — calls `editor.width.set(value)`
13
13
  * - `placeholder` — calls `editor.placeholder.set(value)`
14
+ * - `data` — re-renders via `editor.render(value)` when content changes
15
+ * (deep-equal–deduped and serialized; seeds the initial content at creation)
14
16
  */
15
17
  export interface UseBlokConfig extends Omit<BlokConfig, 'holder'> {
16
18
  /** Editor content width mode. Synced reactively after mount via `editor.width.set()`. */
@@ -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
 
@@ -173,6 +174,42 @@ export function useBlok(config: UseBlokConfig, deps?: DependencyList): Blok | nu
173
174
  editor.placeholder.set(placeholder);
174
175
  }, [editor, placeholder]);
175
176
 
177
+ // Reactive: data (controlled content)
178
+ //
179
+ // `data` seeds the editor at construction. Afterwards, changing the prop to
180
+ // new *content* re-renders the editor via the public render() API — no
181
+ // recreation. Updates are deep-equal–deduped (so a new reference with the
182
+ // same content is a no-op and won't clobber the caret) and serialized (so
183
+ // rapid changes can't trigger overlapping render passes).
184
+ const { data } = config;
185
+ const lastRenderedDataRef = useRef(config.data);
186
+ const seededEditorRef = useRef<Blok | null>(null);
187
+ const renderChainRef = useRef<Promise<void>>(Promise.resolve());
188
+ useEffect(() => {
189
+ if (editor === null || data === undefined) {
190
+ return;
191
+ }
192
+
193
+ // A freshly created editor was already seeded with `data` at construction;
194
+ // record it without re-rendering.
195
+ if (seededEditorRef.current !== editor) {
196
+ seededEditorRef.current = editor;
197
+ lastRenderedDataRef.current = data;
198
+
199
+ return;
200
+ }
201
+
202
+ // Unchanged content — skip the redundant render.
203
+ if (deepEqual(data, lastRenderedDataRef.current)) {
204
+ return;
205
+ }
206
+
207
+ lastRenderedDataRef.current = data;
208
+ renderChainRef.current = renderChainRef.current
209
+ .catch(() => undefined)
210
+ .then(() => editor.render(data));
211
+ }, [editor, data]);
212
+
176
213
  return editor;
177
214
  }
178
215
 
@@ -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: any): void;
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
@@ -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
  }