@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.
- package/README.md +3 -1
- 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-BPeM6wKv.mjs → blok-1H9roBGf.mjs} +2271 -2249
- package/dist/chunks/{blok-Ci1zCOUg.cjs → blok-J3Lfa_bK.cjs} +13 -13
- package/dist/chunks/{constants-DR3sya6U.mjs → constants-BJ79cqMa.mjs} +288 -284
- package/dist/chunks/constants-BZUc1kAY.cjs +606 -0
- package/dist/chunks/{tools-B72rczVw.mjs → tools-ByOjWADT.mjs} +420 -418
- package/dist/chunks/{tools-DlOZpcVU.cjs → tools-OHDv2C7w.cjs} +2 -2
- package/dist/full.cjs +1 -1
- package/dist/full.mjs +3 -3
- package/dist/react.cjs +1 -1
- package/dist/react.mjs +54 -33
- 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/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 +17 -5
- package/src/react/config-keys.ts +2 -0
- package/src/react/deep-equal.ts +48 -0
- package/src/react/types.ts +2 -0
- package/src/react/useBlok.ts +37 -0
- package/types/api/events.d.ts +35 -5
- package/types/api/tools.d.ts +16 -1
- package/types/configs/blok-config.d.ts +37 -0
- package/types/events/editor-events.ts +41 -0
- package/types/index.d.ts +5 -1
- package/types/react.d.ts +13 -2
- 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
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
213
|
-
|
|
113
|
+
return 1;
|
|
114
|
+
}
|
|
214
115
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
}
|
|
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(
|
|
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(
|
|
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
|
package/src/react/BlokEditor.tsx
CHANGED
|
@@ -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
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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
|
-
/**
|
|
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);
|
package/src/react/config-keys.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/react/types.ts
CHANGED
|
@@ -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()`. */
|
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
|
|
|
@@ -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
|
|
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
|
}
|