@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
|
@@ -42,6 +42,12 @@ export class ModificationsObserver extends Module {
|
|
|
42
42
|
*/
|
|
43
43
|
private readonly batchTime = modificationsObserverBatchTimeout;
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Set once the module is destroyed so an in-flight serialization (onSave)
|
|
47
|
+
* doesn't call back into a torn-down editor after its promise resolves.
|
|
48
|
+
*/
|
|
49
|
+
private destroyed = false;
|
|
50
|
+
|
|
45
51
|
/**
|
|
46
52
|
* Prepare the module
|
|
47
53
|
* @param options - options used by the modification observer module
|
|
@@ -103,7 +109,7 @@ export class ModificationsObserver extends Module {
|
|
|
103
109
|
* @param event - some of our custom change events
|
|
104
110
|
*/
|
|
105
111
|
private particularBlockChanged(event: BlockMutationEvent): void {
|
|
106
|
-
if (this.disabled || !isFunction(this.config.onChange)) {
|
|
112
|
+
if (this.disabled || (!isFunction(this.config.onChange) && !isFunction(this.config.onSave))) {
|
|
107
113
|
return;
|
|
108
114
|
}
|
|
109
115
|
|
|
@@ -127,14 +133,45 @@ export class ModificationsObserver extends Module {
|
|
|
127
133
|
? queuedEvents[0]
|
|
128
134
|
: queuedEvents;
|
|
129
135
|
|
|
130
|
-
if (this.config.onChange) {
|
|
136
|
+
if (isFunction(this.config.onChange)) {
|
|
131
137
|
this.config.onChange(this.Blok.API.methods, eventsToEmit);
|
|
132
138
|
}
|
|
133
139
|
|
|
140
|
+
if (isFunction(this.config.onSave)) {
|
|
141
|
+
this.emitOnSave();
|
|
142
|
+
}
|
|
143
|
+
|
|
134
144
|
this.batchingOnChangeQueue.clear();
|
|
135
145
|
}, this.batchTime);
|
|
136
146
|
}
|
|
137
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Serializes the editor and delivers the full OutputData to the consumer's
|
|
150
|
+
* `onSave` callback. Invoked once per batched change window, so a burst of
|
|
151
|
+
* edits results in a single serialization. Skips delivery if the module was
|
|
152
|
+
* destroyed while the (async) serialization was in flight.
|
|
153
|
+
*/
|
|
154
|
+
private emitOnSave(): void {
|
|
155
|
+
void this.Blok.Saver.save()
|
|
156
|
+
.then((data) => {
|
|
157
|
+
if (this.destroyed || data === undefined) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const { onSave } = this.config;
|
|
162
|
+
|
|
163
|
+
if (isFunction(onSave)) {
|
|
164
|
+
onSave(data, this.Blok.API.methods);
|
|
165
|
+
}
|
|
166
|
+
})
|
|
167
|
+
.catch(() => {
|
|
168
|
+
/**
|
|
169
|
+
* Serialization failed — the Saver already surfaces the error via its
|
|
170
|
+
* own channel, so swallow here to avoid an unhandled rejection.
|
|
171
|
+
*/
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
138
175
|
/**
|
|
139
176
|
* Cleans up the module: disconnects the MutationObserver and cancels any
|
|
140
177
|
* pending batching timeout. Called by the editor's destroy() chain so that
|
|
@@ -144,6 +181,7 @@ export class ModificationsObserver extends Module {
|
|
|
144
181
|
*/
|
|
145
182
|
public destroy(): void {
|
|
146
183
|
this.disabled = true;
|
|
184
|
+
this.destroyed = true;
|
|
147
185
|
this.mutationObserver.disconnect();
|
|
148
186
|
|
|
149
187
|
if (this.batchingTimeout !== null) {
|
|
@@ -113,7 +113,16 @@ export class Paste extends Module {
|
|
|
113
113
|
*/
|
|
114
114
|
public async processDataTransfer(dataTransfer: DataTransfer, pasteTarget?: Element): Promise<void> {
|
|
115
115
|
const plainData = dataTransfer.getData('text/plain');
|
|
116
|
-
|
|
116
|
+
|
|
117
|
+
// Give consumers a chance to transform or drop the raw clipboard HTML
|
|
118
|
+
// before any Blok preprocessing/sanitization runs. Returning a string
|
|
119
|
+
// replaces the HTML for the rest of the pipeline; returning null aborts the
|
|
120
|
+
// HTML paste path entirely (an empty string makes HtmlHandler bail, so the
|
|
121
|
+
// paste falls through to the plain-text handler).
|
|
122
|
+
const clipboardHtml = dataTransfer.getData('text/html');
|
|
123
|
+
const rawHtmlData = clipboardHtml && this.config.onBeforePaste
|
|
124
|
+
? this.config.onBeforePaste(clipboardHtml) ?? ''
|
|
125
|
+
: clipboardHtml;
|
|
117
126
|
|
|
118
127
|
// Native Blok clipboard data always wins. When absent, fall back to the
|
|
119
128
|
// lossless proprietary flavours of other editors (web→web paste): they
|
|
@@ -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,10 +20,21 @@ 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). For ad-hoc reloads you can still call
|
|
28
|
+
* `ref.current.render(newData)`.
|
|
29
|
+
*
|
|
30
|
+
* Pair `data` with `onSave` for a true controlled component: `onSave` is the
|
|
31
|
+
* output half, firing (debounced) with the full serialized `OutputData` on every
|
|
32
|
+
* content change — no manual `ref.current.save()` polling. Echoing that payload
|
|
33
|
+
* straight back via `onSave={setData}` is safe and caret-stable: the adapter
|
|
34
|
+
* records the editor's own emitted output as the content baseline, so the
|
|
35
|
+
* `onSave → setState → data` round-trip deep-equals that baseline and is deduped
|
|
36
|
+
* to a no-op (no re-render, no caret reset, no recursion). Genuine external `data`
|
|
37
|
+
* changes still re-render in place.
|
|
27
38
|
*/
|
|
28
39
|
export interface BlokEditorProps
|
|
29
40
|
extends Omit<UseBlokConfig, 'onReady'>,
|
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,12 @@ 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)
|
|
16
|
+
*
|
|
17
|
+
* The `onSave` config (inherited from `BlokConfig`) is the controlled output
|
|
18
|
+
* half: it fires with the full serialized `OutputData` on every content change,
|
|
19
|
+
* so `data` + `onSave` form a controlled component without manual `save()` calls.
|
|
14
20
|
*/
|
|
15
21
|
export interface UseBlokConfig extends Omit<BlokConfig, 'holder'> {
|
|
16
22
|
/** Editor content width mode. Synced reactively after mount via `editor.width.set()`. */
|