@bloklabs/angular 1.4.2 → 1.4.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.
|
@@ -359,6 +359,77 @@ const generateId = (prefix = '') => {
|
|
|
359
359
|
return `${prefix}${(Math.floor(Math.random() * ID_RANDOM_MULTIPLIER)).toString(HEXADECIMAL_RADIX)}`;
|
|
360
360
|
};
|
|
361
361
|
|
|
362
|
+
/**
|
|
363
|
+
* Flatten a hierarchical block spec (nodes with `children`) into the flat DFS
|
|
364
|
+
* pre-order `OutputBlockData[]` Blok stores, wiring every node's `parent` and
|
|
365
|
+
* `content` id links.
|
|
366
|
+
*
|
|
367
|
+
* This is the pure counterpart of the live `insertTree` mutation: the same DFS,
|
|
368
|
+
* without an editor. Use it to seed nested content — columns, tables, a whole
|
|
369
|
+
* document — without hand-authoring `parent`/`content` arrays:
|
|
370
|
+
*
|
|
371
|
+
* @example
|
|
372
|
+
* new Blok({
|
|
373
|
+
* data: {
|
|
374
|
+
* blocks: flattenTree([
|
|
375
|
+
* { type: 'column_list', children: [
|
|
376
|
+
* { type: 'column', children: [{ type: 'paragraph', data: { text: 'L' } }] },
|
|
377
|
+
* { type: 'column', children: [{ type: 'paragraph', data: { text: 'R' } }] },
|
|
378
|
+
* ] },
|
|
379
|
+
* ]),
|
|
380
|
+
* },
|
|
381
|
+
* });
|
|
382
|
+
*
|
|
383
|
+
* @param spec - a single root node or an array of root nodes.
|
|
384
|
+
* @param options - `parentId` sets the `parent` of the root node(s);
|
|
385
|
+
* `generateId` overrides id generation for nodes that omit an `id` (default:
|
|
386
|
+
* Blok's nanoid scheme).
|
|
387
|
+
* @returns DFS pre-order blocks with `parent`/`content` wired. Leaves omit the
|
|
388
|
+
* empty `content` array, matching the documented `OutputBlockData` shape.
|
|
389
|
+
* @throws if an explicit `id` is reused within the spec — a duplicate id would
|
|
390
|
+
* corrupt every id-keyed lookup, so it is surfaced loudly rather than encoded.
|
|
391
|
+
*/
|
|
392
|
+
function flattenTree(spec, options = {}) {
|
|
393
|
+
const generateId = options.generateId ?? generateBlockId;
|
|
394
|
+
const rootParent = options.parentId ?? undefined;
|
|
395
|
+
const flat = [];
|
|
396
|
+
const usedIds = new Set();
|
|
397
|
+
// Pre-order DFS: push self FIRST, then recurse each child, so the flat array
|
|
398
|
+
// is DFS-contiguous. `content` is filled as each child is visited and returns
|
|
399
|
+
// its id; it is omitted for leaves.
|
|
400
|
+
const visit = (node, parent) => {
|
|
401
|
+
const id = node.id ?? generateId();
|
|
402
|
+
if (usedIds.has(id)) {
|
|
403
|
+
throw new Error(`flattenTree: duplicate block id "${id}" — every block id must be unique.`);
|
|
404
|
+
}
|
|
405
|
+
usedIds.add(id);
|
|
406
|
+
const childIds = [];
|
|
407
|
+
const flatNode = {
|
|
408
|
+
id,
|
|
409
|
+
// `type` is OMITTED (not present-with-undefined) when absent so core's
|
|
410
|
+
// `type || defaultBlock` fallback resolves the default block cleanly.
|
|
411
|
+
...(node.type !== undefined ? { type: node.type } : {}),
|
|
412
|
+
data: node.data ?? {},
|
|
413
|
+
...(node.tunes !== undefined ? { tunes: node.tunes } : {}),
|
|
414
|
+
...(parent !== undefined ? { parent } : {}),
|
|
415
|
+
};
|
|
416
|
+
flat.push(flatNode);
|
|
417
|
+
for (const child of node.children ?? []) {
|
|
418
|
+
childIds.push(visit(child, id));
|
|
419
|
+
}
|
|
420
|
+
// Only wire `content` when there are children — leaves stay clean.
|
|
421
|
+
if (childIds.length > 0) {
|
|
422
|
+
flatNode.content = childIds;
|
|
423
|
+
}
|
|
424
|
+
return id;
|
|
425
|
+
};
|
|
426
|
+
const roots = Array.isArray(spec) ? spec : [spec];
|
|
427
|
+
for (const root of roots) {
|
|
428
|
+
visit(root, rootParent);
|
|
429
|
+
}
|
|
430
|
+
return flat;
|
|
431
|
+
}
|
|
432
|
+
|
|
362
433
|
/**
|
|
363
434
|
* Enumerate the editor's blocks once into BlockNode records, in flat order,
|
|
364
435
|
* deriving each node's contentIds from the children that name it as parent.
|
|
@@ -1218,54 +1289,41 @@ const createBlocksApiForEditor = (editor, onRead = () => undefined) => {
|
|
|
1218
1289
|
return null;
|
|
1219
1290
|
}
|
|
1220
1291
|
}
|
|
1221
|
-
//
|
|
1222
|
-
//
|
|
1223
|
-
//
|
|
1224
|
-
|
|
1225
|
-
//
|
|
1226
|
-
//
|
|
1227
|
-
//
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
// Mutable property on a const holder (no `let`): flipped from inside the
|
|
1232
|
-
// recursive visit when a colliding explicit id is seen.
|
|
1233
|
-
const usedIds = new Set();
|
|
1234
|
-
const collision = { hit: false };
|
|
1235
|
-
// Pre-order DFS: push self FIRST, then recurse each child, so the flat
|
|
1236
|
-
// array is DFS-contiguous (the invariant core's insertMany + the hook's
|
|
1237
|
-
// getChildren depend on). `content` is the live child-id array, filled as
|
|
1238
|
-
// each child is visited and returns its id. The root's `parent` is the
|
|
1239
|
-
// resolved placement parent (undefined for root level); children's `parent`
|
|
1240
|
-
// is their enclosing node's id.
|
|
1241
|
-
const visit = (node, parent) => {
|
|
1242
|
-
if (node.id !== undefined && (usedIds.has(node.id) || getById(node.id) !== null)) {
|
|
1243
|
-
collision.hit = true;
|
|
1292
|
+
// Flatten the nested spec to a DFS pre-order array with wired
|
|
1293
|
+
// `parent`/`content` links — the same transform used to seed nested data,
|
|
1294
|
+
// shared as the pure `flattenTree` helper. `parentId` becomes the root's
|
|
1295
|
+
// `parent`. flattenTree throws on an id reused WITHIN the spec (a duplicate
|
|
1296
|
+
// would corrupt every id-keyed lookup); a tree insert always creates fresh
|
|
1297
|
+
// blocks (NOT insert-if-absent), so that is rejected up front — null,
|
|
1298
|
+
// mirroring insert's null contract.
|
|
1299
|
+
const flat = (() => {
|
|
1300
|
+
try {
|
|
1301
|
+
return flattenTree(spec, { parentId: parentId ?? undefined });
|
|
1244
1302
|
}
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
const content = [];
|
|
1248
|
-
const flatNode = {
|
|
1249
|
-
id,
|
|
1250
|
-
// `type` is OMITTED (not present-with-undefined) when absent so core's
|
|
1251
|
-
// `type || defaultBlock` fallback resolves the default block cleanly.
|
|
1252
|
-
...(node.type !== undefined ? { type: node.type } : {}),
|
|
1253
|
-
data: node.data ?? {},
|
|
1254
|
-
content,
|
|
1255
|
-
...(node.tunes !== undefined ? { tunes: node.tunes } : {}),
|
|
1256
|
-
...(parent !== undefined ? { parent } : {}),
|
|
1257
|
-
};
|
|
1258
|
-
flat.push(flatNode);
|
|
1259
|
-
for (const child of node.children ?? []) {
|
|
1260
|
-
content.push(visit(child, id));
|
|
1303
|
+
catch {
|
|
1304
|
+
return null;
|
|
1261
1305
|
}
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
const rootId = visit(spec, parentId ?? undefined);
|
|
1265
|
-
// A colliding explicit id is rejected before any insert (see usedIds doc).
|
|
1266
|
-
if (collision.hit) {
|
|
1306
|
+
})();
|
|
1307
|
+
if (flat === null) {
|
|
1267
1308
|
return null;
|
|
1268
1309
|
}
|
|
1310
|
+
// External collision: an explicit id already present in the live tree would
|
|
1311
|
+
// also create a duplicate-id block. Snapshot the existing ids once and
|
|
1312
|
+
// reject if any flat node reuses one. Generated ids never collide.
|
|
1313
|
+
const existingIds = new Set(snapshotNodes(reader).map((n) => n.id));
|
|
1314
|
+
if (flat.some((node) => existingIds.has(node.id))) {
|
|
1315
|
+
return null;
|
|
1316
|
+
}
|
|
1317
|
+
const rootId = flat[0].id;
|
|
1318
|
+
// flattenTree omits `content` on leaves (clean seed-data shape). insertMany
|
|
1319
|
+
// wants every node to carry an explicit child-id array — a leaf's
|
|
1320
|
+
// `contentIds: []` states "no children" rather than leaving it undefined —
|
|
1321
|
+
// so stamp the empty array back on before handing the batch to core.
|
|
1322
|
+
for (const node of flat) {
|
|
1323
|
+
if (node.content === undefined) {
|
|
1324
|
+
node.content = [];
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1269
1327
|
const flatIndex = resolveInsertIndex(reader, parentId, position);
|
|
1270
1328
|
// ONE transact for the whole subtree → a single atomic undo step. Core's
|
|
1271
1329
|
// insertMany composes EVERY node before inserting any, so an unknown tool
|
|
@@ -1677,6 +1735,226 @@ function deepEqual(a, b) {
|
|
|
1677
1735
|
return aKeys.every((key) => Object.prototype.hasOwnProperty.call(bObj, key) && deepEqual(aObj[key], bObj[key]));
|
|
1678
1736
|
}
|
|
1679
1737
|
|
|
1738
|
+
/**
|
|
1739
|
+
* Public utilities for working with saved editor documents (`OutputData`).
|
|
1740
|
+
*
|
|
1741
|
+
* Consumers persisting editor content need two recurring predicates that the
|
|
1742
|
+
* editor itself already relies on internally:
|
|
1743
|
+
*
|
|
1744
|
+
* - structural equality, to dedupe the `data → render → onSave → data` echo
|
|
1745
|
+
* round-trip without clobbering the caret;
|
|
1746
|
+
* - emptiness, to decide whether a document carries any user content
|
|
1747
|
+
* (placeholder states, "save" button gating, skipping empty submissions).
|
|
1748
|
+
*
|
|
1749
|
+
* Both accept the loose wire shape (`data: null`, `id: null`, nullish
|
|
1750
|
+
* documents) so backend DTOs can be passed as-is.
|
|
1751
|
+
*/
|
|
1752
|
+
/**
|
|
1753
|
+
* Compares two blocks structurally. The `id` participates only when BOTH
|
|
1754
|
+
* blocks carry one: the editor mints a fresh id whenever content arrives
|
|
1755
|
+
* without one (see {@link normalizeOutputBlocks}), so an id-less origin
|
|
1756
|
+
* document must still compare equal to its saved echo.
|
|
1757
|
+
* @param a - first block to compare
|
|
1758
|
+
* @param b - second block to compare
|
|
1759
|
+
* @returns true when the blocks are structurally equal
|
|
1760
|
+
*/
|
|
1761
|
+
function equalsOutputBlock(a, b) {
|
|
1762
|
+
const { id: idA, ...restA } = a;
|
|
1763
|
+
const { id: idB, ...restB } = b;
|
|
1764
|
+
const hasIdA = typeof idA === 'string' && idA !== '';
|
|
1765
|
+
const hasIdB = typeof idB === 'string' && idB !== '';
|
|
1766
|
+
if (hasIdA && hasIdB && idA !== idB) {
|
|
1767
|
+
return false;
|
|
1768
|
+
}
|
|
1769
|
+
return deepEqual(restA, restB);
|
|
1770
|
+
}
|
|
1771
|
+
/**
|
|
1772
|
+
* The tool a pristine editor seeds its first (empty) block with. An empty block
|
|
1773
|
+
* of this type is UI scaffolding, not user content — see
|
|
1774
|
+
* {@link EqualsOutputDataOptions.ignoreEmptyDefaultBlocks}.
|
|
1775
|
+
*/
|
|
1776
|
+
const DEFAULT_BLOCK_TYPE = 'paragraph';
|
|
1777
|
+
/**
|
|
1778
|
+
* True for an empty block of the default paragraph tool — the editor's own
|
|
1779
|
+
* scaffolding, not user content.
|
|
1780
|
+
* @param block - block to inspect
|
|
1781
|
+
* @returns true when the block is an empty default paragraph
|
|
1782
|
+
*/
|
|
1783
|
+
function isEmptyDefaultBlock(block) {
|
|
1784
|
+
return block.type === DEFAULT_BLOCK_TYPE && isEmptyValue(block.data);
|
|
1785
|
+
}
|
|
1786
|
+
/**
|
|
1787
|
+
* Resolves the blocks to compare for a document, dropping empty default blocks
|
|
1788
|
+
* when {@link EqualsOutputDataOptions.ignoreEmptyDefaultBlocks} is set.
|
|
1789
|
+
* @param data - document whose blocks are collected
|
|
1790
|
+
* @param options - comparison options
|
|
1791
|
+
* @returns the blocks to compare
|
|
1792
|
+
*/
|
|
1793
|
+
function comparableBlocks(data, options) {
|
|
1794
|
+
const blocks = data?.blocks ?? [];
|
|
1795
|
+
if (options?.ignoreEmptyDefaultBlocks === true) {
|
|
1796
|
+
return blocks.filter((block) => !isEmptyDefaultBlock(block));
|
|
1797
|
+
}
|
|
1798
|
+
return blocks;
|
|
1799
|
+
}
|
|
1800
|
+
/**
|
|
1801
|
+
* Structural equality for saved documents. Compares the `blocks` arrays
|
|
1802
|
+
* deeply; the volatile `time` and `version` envelope fields are ignored, so a
|
|
1803
|
+
* document round-tripped through `save()` compares equal to its echo. Block
|
|
1804
|
+
* ids are compared only when both sides carry one — the editor mints fresh
|
|
1805
|
+
* ids for id-less content, so a legacy document still equals its saved echo.
|
|
1806
|
+
* Nullish documents compare equal to `{ blocks: [] }`.
|
|
1807
|
+
*
|
|
1808
|
+
* Pass `{ ignoreEmptyDefaultBlocks: true }` for dirty-vs-baseline checks so a
|
|
1809
|
+
* pristine editor (one empty default paragraph) equals a saved-empty baseline —
|
|
1810
|
+
* see {@link EqualsOutputDataOptions}.
|
|
1811
|
+
* @param a - first document to compare
|
|
1812
|
+
* @param b - second document to compare
|
|
1813
|
+
* @param options - comparison options
|
|
1814
|
+
* @returns true when both documents hold structurally equal blocks
|
|
1815
|
+
*/
|
|
1816
|
+
function equalsOutputData(a, b, options) {
|
|
1817
|
+
const blocksA = comparableBlocks(a, options);
|
|
1818
|
+
const blocksB = comparableBlocks(b, options);
|
|
1819
|
+
return blocksA.length === blocksB.length && blocksA.every((block, index) => equalsOutputBlock(block, blocksB[index]));
|
|
1820
|
+
}
|
|
1821
|
+
/**
|
|
1822
|
+
* A bounded window of the editor's recently emitted `onSave` payloads, used by
|
|
1823
|
+
* the framework adapters to recognize controlled-`data` echoes. Deduping
|
|
1824
|
+
* against only the LAST emitted payload is not enough: a host that persists on
|
|
1825
|
+
* save and refetches can hand the adapter a STALE echo — an earlier save
|
|
1826
|
+
* arriving after a newer one already replaced the baseline — and re-rendering
|
|
1827
|
+
* it would clobber the caret and any content typed since. Matching is
|
|
1828
|
+
* structural ({@link equalsOutputData}), so envelopes reshaped in transit
|
|
1829
|
+
* (fresh `time`, stripped ids) still count as echoes.
|
|
1830
|
+
* @param capacity - number of payloads retained before the oldest is evicted
|
|
1831
|
+
* @returns the echo window
|
|
1832
|
+
*/
|
|
1833
|
+
function createEmittedEchoWindow(capacity = 20) {
|
|
1834
|
+
const emitted = [];
|
|
1835
|
+
return {
|
|
1836
|
+
record(data) {
|
|
1837
|
+
emitted.push(data);
|
|
1838
|
+
if (emitted.length > capacity) {
|
|
1839
|
+
emitted.shift();
|
|
1840
|
+
}
|
|
1841
|
+
},
|
|
1842
|
+
matches(data) {
|
|
1843
|
+
return emitted.some((payload) => equalsOutputData(payload, data));
|
|
1844
|
+
},
|
|
1845
|
+
clear() {
|
|
1846
|
+
emitted.length = 0;
|
|
1847
|
+
},
|
|
1848
|
+
};
|
|
1849
|
+
}
|
|
1850
|
+
/**
|
|
1851
|
+
* A shared, deeply frozen empty document. Consumers persisting editor content
|
|
1852
|
+
* repeatedly hand-write `{ blocks: [] }` for cleared/pristine baselines; this
|
|
1853
|
+
* is the canonical value to use instead. Frozen (blocks array included) so a
|
|
1854
|
+
* shared reference can never be mutated into a stale non-empty baseline.
|
|
1855
|
+
*/
|
|
1856
|
+
const EMPTY_OUTPUT_DATA = Object.freeze({
|
|
1857
|
+
// Freeze the array too, so a shared reference can't be mutated. The public
|
|
1858
|
+
// `blocks` type is mutable (`OutputBlockData[]`), so the frozen (readonly)
|
|
1859
|
+
// array is re-widened through `unknown` — runtime stays frozen.
|
|
1860
|
+
blocks: Object.freeze([]),
|
|
1861
|
+
});
|
|
1862
|
+
/**
|
|
1863
|
+
* Maps a controlled `data` value to something the editor's `render()` accepts.
|
|
1864
|
+
* A whole-document `null` — a controlled "clear to empty" — becomes
|
|
1865
|
+
* `{ blocks: [] }`; any real document passes through untouched.
|
|
1866
|
+
*
|
|
1867
|
+
* The framework adapters (React/Vue/Angular) route their reactive `data` path
|
|
1868
|
+
* through this so a `null` never reaches `render()`, whose strict guard reads
|
|
1869
|
+
* `data.blocks` and would throw on `null`. `undefined` (uncontrolled) is handled
|
|
1870
|
+
* by the adapters before this point, so it is intentionally not an input here.
|
|
1871
|
+
* @param data - a controlled document, or `null` for an empty document
|
|
1872
|
+
* @returns a document `render()` can consume
|
|
1873
|
+
*/
|
|
1874
|
+
function toRenderableData(data) {
|
|
1875
|
+
return data === null ? { blocks: [] } : data;
|
|
1876
|
+
}
|
|
1877
|
+
/**
|
|
1878
|
+
* Normalizes blocks from the loose wire shape into the strict saved shape at
|
|
1879
|
+
* the editor's input boundaries: a `null`/missing `data` becomes `{}`, a
|
|
1880
|
+
* `null`/empty `id` is dropped so the block factory generates a fresh one.
|
|
1881
|
+
* Idempotent — strict blocks pass through unchanged (shallow-copied).
|
|
1882
|
+
* @param blocks - blocks in either the strict or the loose wire shape
|
|
1883
|
+
* @returns blocks in the strict saved shape
|
|
1884
|
+
*/
|
|
1885
|
+
function normalizeOutputBlocks(blocks) {
|
|
1886
|
+
return blocks.map((block) => {
|
|
1887
|
+
const { id, data, ...rest } = block;
|
|
1888
|
+
return {
|
|
1889
|
+
...rest,
|
|
1890
|
+
...(typeof id === 'string' && id !== '' ? { id } : {}),
|
|
1891
|
+
data: data ?? {},
|
|
1892
|
+
};
|
|
1893
|
+
});
|
|
1894
|
+
}
|
|
1895
|
+
/**
|
|
1896
|
+
* Normalizes a whole loose wire document into the strict saved
|
|
1897
|
+
* {@link OutputData} shape at an input boundary. A nullish document becomes
|
|
1898
|
+
* `{ blocks: [] }`; `null` envelope fields (`time`/`version`) are dropped; each
|
|
1899
|
+
* block is passed through {@link normalizeOutputBlocks}, so `null`/missing
|
|
1900
|
+
* `data` becomes `{}` and `null`/empty ids are dropped for regeneration.
|
|
1901
|
+
*
|
|
1902
|
+
* Unlike a hand-written `blocks.map(...)` mapper, this preserves every block
|
|
1903
|
+
* passthrough field — `tunes`, `parent`, `content`, `indent`, edit metadata —
|
|
1904
|
+
* so a backend DTO can be turned into strict `OutputData` without silently
|
|
1905
|
+
* losing hierarchy or tunes. Idempotent: a strict document passes through
|
|
1906
|
+
* unchanged (shallow-copied).
|
|
1907
|
+
* @param data - a document in the strict or loose wire shape, or nullish
|
|
1908
|
+
* @returns the document in the strict saved shape
|
|
1909
|
+
*/
|
|
1910
|
+
function normalizeOutputData(data) {
|
|
1911
|
+
if (data === null || data === undefined) {
|
|
1912
|
+
return { blocks: [] };
|
|
1913
|
+
}
|
|
1914
|
+
return {
|
|
1915
|
+
...(typeof data.version === 'string' ? { version: data.version } : {}),
|
|
1916
|
+
...(typeof data.time === 'number' ? { time: data.time } : {}),
|
|
1917
|
+
blocks: normalizeOutputBlocks(data.blocks),
|
|
1918
|
+
};
|
|
1919
|
+
}
|
|
1920
|
+
/**
|
|
1921
|
+
* True when the value carries no user content: blank/whitespace-only strings,
|
|
1922
|
+
* empty arrays, plain objects whose values are all empty, and nullish values.
|
|
1923
|
+
* Numbers and booleans are presentation metadata (`level`, `checked`, styles)
|
|
1924
|
+
* and never count as content on their own.
|
|
1925
|
+
* @param value - block data value to inspect
|
|
1926
|
+
* @returns true when the value carries no user content
|
|
1927
|
+
*/
|
|
1928
|
+
function isEmptyValue(value) {
|
|
1929
|
+
if (value === null || value === undefined) {
|
|
1930
|
+
return true;
|
|
1931
|
+
}
|
|
1932
|
+
if (typeof value === 'string') {
|
|
1933
|
+
return value.trim() === '';
|
|
1934
|
+
}
|
|
1935
|
+
if (Array.isArray(value)) {
|
|
1936
|
+
return value.every(isEmptyValue);
|
|
1937
|
+
}
|
|
1938
|
+
if (typeof value === 'object') {
|
|
1939
|
+
return Object.values(value).every(isEmptyValue);
|
|
1940
|
+
}
|
|
1941
|
+
// Numbers, booleans and other primitives are metadata, not content.
|
|
1942
|
+
return true;
|
|
1943
|
+
}
|
|
1944
|
+
/**
|
|
1945
|
+
* True when the document carries no user content: it is nullish, has no
|
|
1946
|
+
* blocks, or every block's data holds only empty values (see
|
|
1947
|
+
* {@link isEmptyValue}). Note that content-less visual blocks (e.g. a divider,
|
|
1948
|
+
* whose data is `{}`) count as empty — check `blocks.length` when mere block
|
|
1949
|
+
* presence matters.
|
|
1950
|
+
* @param data - document to inspect
|
|
1951
|
+
* @returns true when the document carries no user content
|
|
1952
|
+
*/
|
|
1953
|
+
function isEmptyOutputData(data) {
|
|
1954
|
+
const blocks = data?.blocks ?? [];
|
|
1955
|
+
return blocks.every((block) => isEmptyValue(block.data));
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1680
1958
|
// src/shared/prop-schema.ts
|
|
1681
1959
|
/**
|
|
1682
1960
|
* Fill a data object against the schema: every schema key present, incoming
|
|
@@ -2470,7 +2748,9 @@ class BlokEditorComponent {
|
|
|
2470
2748
|
return;
|
|
2471
2749
|
}
|
|
2472
2750
|
this.lastRenderedData = data;
|
|
2473
|
-
|
|
2751
|
+
// `data` may be null (a controlled "clear to empty"); render() throws on
|
|
2752
|
+
// null, so normalize null → { blocks: [] } at the boundary.
|
|
2753
|
+
this.renderChain = this.renderChain.catch(() => undefined).then(() => editor.render(toRenderableData(data)));
|
|
2474
2754
|
});
|
|
2475
2755
|
// Emit `ready` once per editor, after `instance()` is populated so a consumer
|
|
2476
2756
|
// reading the component/ref instance inside the handler sees it.
|