@jackuait/blok 0.10.7 → 0.10.9
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/dist/blok.mjs +2 -2
- package/dist/chunks/{blok-oWXfRfnM.mjs → blok-DbRn9adY.mjs} +2681 -2238
- package/dist/chunks/{constants-BQ1-lyZI.mjs → constants-C9lsSOXl.mjs} +4 -3
- package/dist/chunks/{core-C942GvJO.mjs → core-B7mxBIHA.mjs} +1 -1
- package/dist/chunks/{engine-javascript-Dd6ViPCH.mjs → engine-javascript-Bmmg8uL9.mjs} +1 -1
- package/dist/chunks/{i18next-loader-CIXsptng.mjs → i18next-loader-453gJdot.mjs} +1 -1
- package/dist/chunks/{tools-MuBQQyZ-.mjs → tools-D0W3_dlA.mjs} +504 -499
- package/dist/full.mjs +3 -3
- package/dist/react.mjs +3 -3
- package/dist/tools.mjs +2 -2
- package/package.json +3 -6
- package/src/components/block/index.ts +36 -0
- package/src/components/blocks.ts +191 -5
- package/src/components/modules/api/blocks.ts +20 -5
- package/src/components/modules/blockEvents/composers/keyboardNavigation.ts +17 -6
- package/src/components/modules/blockManager/blockManager.ts +364 -23
- package/src/components/modules/blockManager/hierarchy.ts +164 -8
- package/src/components/modules/blockManager/operations.ts +223 -26
- package/src/components/modules/blockManager/types.ts +13 -1
- package/src/components/modules/blockManager/yjs-sync.ts +48 -3
- package/src/components/modules/drag/DragController.ts +209 -8
- package/src/components/modules/drag/operations/DragOperations.ts +153 -20
- package/src/components/modules/paste/handlers/base.ts +48 -20
- package/src/components/modules/paste/handlers/blok-data-handler.ts +184 -44
- package/src/components/modules/paste/index.ts +20 -0
- package/src/components/modules/renderer.ts +9 -1
- package/src/components/modules/saver.ts +75 -5
- package/src/components/modules/toolbar/index.ts +41 -60
- package/src/components/modules/uiControllers/controllers/keyboard.ts +20 -0
- package/src/components/modules/yjs/block-observer.ts +87 -23
- package/src/components/modules/yjs/document-store.ts +37 -11
- package/src/components/modules/yjs/index.ts +83 -7
- package/src/components/modules/yjs/types.ts +35 -2
- package/src/components/modules/yjs/undo-history.ts +116 -5
- package/src/components/utils/data-model-transform.ts +247 -35
- package/src/components/utils/hierarchy-invariant.ts +137 -0
- package/src/markdown/markdown-handler.ts +9 -2
- package/src/styles/main.css +5 -0
- package/src/tools/callout/constants.ts +0 -1
- package/src/tools/callout/dom-builder.ts +1 -11
- package/src/tools/callout/index.ts +0 -6
- package/src/tools/header/index.ts +14 -1
- package/src/tools/table/table-operations.ts +9 -4
- package/src/tools/toggle/constants.ts +2 -1
- package/src/tools/toggle/dom-builder.ts +7 -0
- package/src/tools/toggle/index.ts +14 -1
- package/src/tools/toggle/toggle-lifecycle.ts +24 -0
- /package/dist/chunks/{lightweight-i18n-DTYoSr_o.mjs → lightweight-i18n-DSjG0iTr.mjs} +0 -0
- /package/dist/chunks/{objectWithoutProperties-D0XxKB4n.mjs → objectWithoutProperties-Dci1-l7D.mjs} +0 -0
package/dist/full.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { n as e, t } from "./chunks/blok-
|
|
2
|
-
import { ur as n } from "./chunks/constants-
|
|
1
|
+
import { n as e, t } from "./chunks/blok-DbRn9adY.mjs";
|
|
2
|
+
import { ur as n } from "./chunks/constants-C9lsSOXl.mjs";
|
|
3
3
|
import { t as r } from "./chunks/objectSpread2-CWwMYL_U.mjs";
|
|
4
|
-
import { a as i, b as a, c as o, g as s, i as c, l, n as u, o as d, s as f, t as p, v as m, y as h } from "./chunks/tools-
|
|
4
|
+
import { a as i, b as a, c as o, g as s, i as c, l, n as u, o as d, s as f, t as p, v as m, y as h } from "./chunks/tools-D0W3_dlA.mjs";
|
|
5
5
|
//#region src/full.ts
|
|
6
6
|
var g = {
|
|
7
7
|
paragraph: {
|
package/dist/react.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { t as e } from "./chunks/blok-
|
|
2
|
-
import "./chunks/constants-
|
|
1
|
+
import { t as e } from "./chunks/blok-DbRn9adY.mjs";
|
|
2
|
+
import "./chunks/constants-C9lsSOXl.mjs";
|
|
3
3
|
import { t } from "./chunks/objectSpread2-CWwMYL_U.mjs";
|
|
4
|
-
import { t as n } from "./chunks/objectWithoutProperties-
|
|
4
|
+
import { t as n } from "./chunks/objectWithoutProperties-Dci1-l7D.mjs";
|
|
5
5
|
import { forwardRef as r, useEffect as i, useMemo as a, useRef as o, useState as s } from "react";
|
|
6
6
|
import { jsx as c } from "react/jsx-runtime";
|
|
7
7
|
//#region src/react/holder-map.ts
|
package/dist/tools.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { m as e } from "./chunks/constants-
|
|
2
|
-
import { _ as t, a as n, b as r, c as i, d as a, f as o, g as s, h as c, i as l, l as u, m as d, n as f, o as p, p as m, r as h, s as g, t as _, u as v, v as y, y as b } from "./chunks/tools-
|
|
1
|
+
import { m as e } from "./chunks/constants-C9lsSOXl.mjs";
|
|
2
|
+
import { _ as t, a as n, b as r, c as i, d as a, f as o, g as s, h as c, i as l, l as u, m as d, n as f, o as p, p as m, r as h, s as g, t as _, u as v, v as y, y as b } from "./chunks/tools-D0W3_dlA.mjs";
|
|
3
3
|
export { u as Bold, c as Callout, v as Code, e as Convert, d as Database, m as DatabaseRow, o as Divider, b as Header, h as InlineCode, i as Italic, g as Link, y as List, p as Marker, r as Paragraph, a as Quote, l as Strikethrough, t as Table, s as Toggle, n as Underline, _ as defaultBlockTools, f as defaultInlineTools };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jackuait/blok",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.9",
|
|
4
4
|
"description": "Blok — headless, highly extensible rich text editor built for developers who need to implement a block-based editing experience (similar to Notion) without building it from scratch",
|
|
5
5
|
"module": "dist/blok.mjs",
|
|
6
6
|
"types": "./types/index.d.ts",
|
|
@@ -116,8 +116,8 @@
|
|
|
116
116
|
"perf:compare": "node scripts/analyze-performance.mjs --baseline",
|
|
117
117
|
"perf:dashboard": "node scripts/generate-performance-dashboard.mjs",
|
|
118
118
|
"e2e:validate-categories": "node scripts/validate-test-categories.mjs",
|
|
119
|
-
"
|
|
120
|
-
"
|
|
119
|
+
"validate:spec-coverage": "node scripts/validate-spec-file-coverage.mjs",
|
|
120
|
+
"ci:timing": "node scripts/ci-timing-probe.mjs",
|
|
121
121
|
"release": "node scripts/release.mjs",
|
|
122
122
|
"release:preflight": "yarn lint && yarn test",
|
|
123
123
|
"unused-css": "node scripts/unused-css-finder/cli.mjs"
|
|
@@ -137,8 +137,6 @@
|
|
|
137
137
|
"@commitlint/cli": "20.4.4",
|
|
138
138
|
"@commitlint/config-conventional": "20.4.4",
|
|
139
139
|
"@playwright/test": "1.58.2",
|
|
140
|
-
"@size-limit/esbuild-why": "12.0.1",
|
|
141
|
-
"@size-limit/preset-small-lib": "12.0.1",
|
|
142
140
|
"@storybook/addon-a11y": "10.2.19",
|
|
143
141
|
"@storybook/addon-vitest": "10.2.19",
|
|
144
142
|
"@storybook/html-vite": "10.2.19",
|
|
@@ -177,7 +175,6 @@
|
|
|
177
175
|
"rollup-plugin-license": "3.7.0",
|
|
178
176
|
"semantic-release": "25.0.3",
|
|
179
177
|
"shiki": "^4.0.2",
|
|
180
|
-
"size-limit": "12.0.1",
|
|
181
178
|
"storybook": "10.2.19",
|
|
182
179
|
"tailwindcss": "4.2.1",
|
|
183
180
|
"typescript": "5.9.3",
|
|
@@ -224,6 +224,14 @@ export class Block extends EventsDispatcher<BlockEvents> {
|
|
|
224
224
|
*/
|
|
225
225
|
private draggableCleanup: (() => void) | null = null;
|
|
226
226
|
|
|
227
|
+
/**
|
|
228
|
+
* Callbacks to invoke at the top of destroy(). External subscribers
|
|
229
|
+
* (e.g. an in-flight drag) register here so they can cancel themselves
|
|
230
|
+
* when the Block instance they hold becomes invalid — preventing stale
|
|
231
|
+
* Block references from reaching drop handlers mid-drag.
|
|
232
|
+
*/
|
|
233
|
+
private destroyCallbacks: Set<() => void> = new Set();
|
|
234
|
+
|
|
227
235
|
/**
|
|
228
236
|
* Manages tool element composition and rendering
|
|
229
237
|
*/
|
|
@@ -529,10 +537,38 @@ export class Block extends EventsDispatcher<BlockEvents> {
|
|
|
529
537
|
return this.dataPersistenceManager.setData(newData);
|
|
530
538
|
}
|
|
531
539
|
|
|
540
|
+
/**
|
|
541
|
+
* Register a callback to be invoked when this Block is destroyed.
|
|
542
|
+
* Returns an unregister function.
|
|
543
|
+
*
|
|
544
|
+
* Used by DragController to cancel an in-flight drag when its source
|
|
545
|
+
* block is replaced (Yjs remote update, blockManager.update, tool
|
|
546
|
+
* conversion, etc). Without this hook, the state machine would hold
|
|
547
|
+
* a stale Block reference and drop the wrong block on mouseup.
|
|
548
|
+
* @param callback - Function to call during destroy()
|
|
549
|
+
* @returns Unregister function
|
|
550
|
+
*/
|
|
551
|
+
public addDestroyCallback(callback: () => void): () => void {
|
|
552
|
+
this.destroyCallbacks.add(callback);
|
|
553
|
+
|
|
554
|
+
return (): void => {
|
|
555
|
+
this.destroyCallbacks.delete(callback);
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
|
|
532
559
|
/**
|
|
533
560
|
* Call Tool instance destroy method
|
|
534
561
|
*/
|
|
535
562
|
public destroy(): void {
|
|
563
|
+
// Notify external subscribers FIRST — before any state is torn down —
|
|
564
|
+
// so listeners (e.g. DragController) can cancel cleanly while the
|
|
565
|
+
// Block is still consistent. Copy to a local list so a callback that
|
|
566
|
+
// unregisters itself doesn't mutate the set during iteration.
|
|
567
|
+
const callbacks = Array.from(this.destroyCallbacks);
|
|
568
|
+
|
|
569
|
+
this.destroyCallbacks.clear();
|
|
570
|
+
callbacks.forEach((cb) => cb());
|
|
571
|
+
|
|
536
572
|
this.mutationHandler.destroy();
|
|
537
573
|
this.inputManager.destroy();
|
|
538
574
|
|
package/src/components/blocks.ts
CHANGED
|
@@ -117,6 +117,25 @@ export class Blocks {
|
|
|
117
117
|
* @param {boolean} skipDOM - if true, do not manipulate DOM (useful when SortableJS already did it)
|
|
118
118
|
*/
|
|
119
119
|
public move(toIndex: number, fromIndex: number, skipDOM = false, skipMovedHook = false): void {
|
|
120
|
+
/**
|
|
121
|
+
* Invalid-index guard (regression: wrong-block-dropped).
|
|
122
|
+
*
|
|
123
|
+
* `Array.splice(-1, 1)` removes the LAST element, and `splice(N, 1)` where
|
|
124
|
+
* N is past the end does nothing — both hide stale-reference bugs as silent
|
|
125
|
+
* data corruption. Every caller in every surface (drag, yjs-sync, API,
|
|
126
|
+
* tool conversion, undo) flows through here, so this is the lowest-level
|
|
127
|
+
* point to reject nonsense indices and keep the "wrong block dropped"
|
|
128
|
+
* class of bug from ever reappearing.
|
|
129
|
+
*/
|
|
130
|
+
if (
|
|
131
|
+
fromIndex < 0 ||
|
|
132
|
+
fromIndex >= this.blocks.length ||
|
|
133
|
+
toIndex < 0 ||
|
|
134
|
+
toIndex >= this.blocks.length
|
|
135
|
+
) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
120
139
|
/**
|
|
121
140
|
* cut out the block, move the DOM element and insert at the desired index
|
|
122
141
|
* again (the shifting within the blocks array will happen automatically).
|
|
@@ -156,8 +175,33 @@ export class Blocks {
|
|
|
156
175
|
* @param {number} index — index to insert Block
|
|
157
176
|
* @param {Block} block — Block to insert
|
|
158
177
|
* @param {boolean} replace — it true, replace block on given index
|
|
178
|
+
* @param {boolean} appendToWorkingArea — if true, append to workingArea end (ignores position)
|
|
179
|
+
* @param {boolean} forceTopLevel — if true, place holder at workingArea root level even when
|
|
180
|
+
* the previous block in the flat array is nested inside another container. Used when the
|
|
181
|
+
* caller knows the new block is logically top-level (parentId === null). Prevents the
|
|
182
|
+
* Enter-after-callout regression family where insertAdjacentElement('afterend',
|
|
183
|
+
* previousBlock.holder) would otherwise drop the new holder inside a nested container.
|
|
159
184
|
*/
|
|
160
|
-
public insert(
|
|
185
|
+
public insert(
|
|
186
|
+
index: number,
|
|
187
|
+
block: Block,
|
|
188
|
+
replace = false,
|
|
189
|
+
appendToWorkingArea = false,
|
|
190
|
+
forceTopLevel = false
|
|
191
|
+
): void {
|
|
192
|
+
/**
|
|
193
|
+
* Invalid-index guard (regression: wrong-block-dropped via alt+drag).
|
|
194
|
+
*
|
|
195
|
+
* `Array.splice(-1, 0, block)` inserts BEFORE the last element — a silent
|
|
196
|
+
* divergence between the flat blocks array and the DOM that corrupts the
|
|
197
|
+
* next move() operation and drops an unrelated block. Mirrors the guard
|
|
198
|
+
* in Blocks.move so every caller (drag duplicate, yjs-sync, undo, API) is
|
|
199
|
+
* protected at the lowest level.
|
|
200
|
+
*/
|
|
201
|
+
if (index < 0) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
161
205
|
if (!this.length) {
|
|
162
206
|
this.push(block);
|
|
163
207
|
|
|
@@ -202,12 +246,62 @@ export class Blocks {
|
|
|
202
246
|
|
|
203
247
|
if (insertIndex > 0) {
|
|
204
248
|
const previousBlock = this.blocks[insertIndex - 1];
|
|
249
|
+
const nextBlock = this.blocks[insertIndex + 1] as Block | undefined;
|
|
250
|
+
|
|
251
|
+
if (forceTopLevel) {
|
|
252
|
+
this.insertAtRootLevel(block, insertIndex);
|
|
253
|
+
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Universal defense against the Enter-after-nested bug family, covering
|
|
259
|
+
* every insertion path (paste, toolbox, slash menu, plus button, markdown
|
|
260
|
+
* shortcut, conversion, split, public API, yjs-sync, drag, programmatic).
|
|
261
|
+
*
|
|
262
|
+
* The only configuration where flat-array adjacency disagrees with the
|
|
263
|
+
* caller's intended DOM container is when the predecessor is DOM-nested
|
|
264
|
+
* (inside a callout, toggle, header-toggleable, table cell, or any future
|
|
265
|
+
* nesting tool's `[data-blok-nested-blocks]` container) AND the successor
|
|
266
|
+
* is at workingArea root. In that case, anchoring `afterend previous`
|
|
267
|
+
* would leak the new holder into the nested container even though the
|
|
268
|
+
* caller's logical intent is a top-level sibling. Route to the successor
|
|
269
|
+
* instead, which correctly anchors at workingArea root.
|
|
270
|
+
*
|
|
271
|
+
* Every OTHER configuration stays on the original `afterend previous`
|
|
272
|
+
* rule, which preserves:
|
|
273
|
+
* - Plain top-level insertion: prev at root, any next → afterend prev.
|
|
274
|
+
* - Nested-sibling insertion (paste inside a toggle child, no top-level
|
|
275
|
+
* follower): prev nested, next undefined or same-container → afterend
|
|
276
|
+
* prev keeps the new block in the same nested container.
|
|
277
|
+
* - Insertion between two same-parent nested children: prev and next
|
|
278
|
+
* both in same nested container → afterend prev stays in container.
|
|
279
|
+
*
|
|
280
|
+
* The forceTopLevel flag is still honored above for callers that want to
|
|
281
|
+
* skip past a trailing nested subtree. This rule is the defense for every
|
|
282
|
+
* path that does NOT explicitly set forceTopLevel.
|
|
283
|
+
*/
|
|
284
|
+
const prevIsAtRoot = previousBlock.holder.parentElement === this.workingArea;
|
|
285
|
+
const nextIsAtRoot = nextBlock !== undefined
|
|
286
|
+
&& nextBlock.holder.parentElement === this.workingArea;
|
|
287
|
+
|
|
288
|
+
if (!prevIsAtRoot && nextIsAtRoot && nextBlock !== undefined) {
|
|
289
|
+
this.insertToDOM(block, 'beforebegin', nextBlock);
|
|
290
|
+
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
205
293
|
|
|
206
294
|
this.insertToDOM(block, 'afterend', previousBlock);
|
|
207
295
|
|
|
208
296
|
return;
|
|
209
297
|
}
|
|
210
298
|
|
|
299
|
+
if (forceTopLevel) {
|
|
300
|
+
this.insertAtRootLevel(block, insertIndex);
|
|
301
|
+
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
211
305
|
const nextBlock = this.blocks[insertIndex + 1] as Block | undefined;
|
|
212
306
|
|
|
213
307
|
if (nextBlock) {
|
|
@@ -231,8 +325,12 @@ export class Blocks {
|
|
|
231
325
|
|
|
232
326
|
const prevBlock = this.blocks[index];
|
|
233
327
|
|
|
234
|
-
prevBlock.holder.replaceWith(block.holder);
|
|
235
328
|
prevBlock.call(BlockToolAPI.REMOVED);
|
|
329
|
+
// Destroy releases the drag-handle listener bound to the shared settings
|
|
330
|
+
// toggler; skipping it leaves the orphan Block wired up and dragging it
|
|
331
|
+
// on next mousedown instead of whatever the user intended.
|
|
332
|
+
prevBlock.destroy();
|
|
333
|
+
prevBlock.holder.replaceWith(block.holder);
|
|
236
334
|
|
|
237
335
|
this.blocks[index] = block;
|
|
238
336
|
|
|
@@ -245,6 +343,17 @@ export class Blocks {
|
|
|
245
343
|
* @param index - index to insert blocks at
|
|
246
344
|
*/
|
|
247
345
|
public insertMany(blocks: Block[], index: number ): void {
|
|
346
|
+
/**
|
|
347
|
+
* Invalid-index guard (regression: wrong-block-dropped family).
|
|
348
|
+
* Mirrors Blocks.move and Blocks.insert — `splice(-1, 0, ...blocks)`
|
|
349
|
+
* silently inserts BEFORE the last element, diverging array from DOM.
|
|
350
|
+
* Yjs batch-add paths feed this method with computed indices; a stale
|
|
351
|
+
* input would otherwise cause a later move() to drop an unrelated block.
|
|
352
|
+
*/
|
|
353
|
+
if (index < 0) {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
248
357
|
const fragment = new DocumentFragment();
|
|
249
358
|
|
|
250
359
|
for (const block of blocks) {
|
|
@@ -293,6 +402,21 @@ export class Blocks {
|
|
|
293
402
|
*/
|
|
294
403
|
public remove(index: number): void {
|
|
295
404
|
const removeIndex = isNaN(index) ? this.length - 1 : index;
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Layer 15: invalid-index guard (regression: wrong-block-dropped family).
|
|
408
|
+
*
|
|
409
|
+
* Before this guard, a stale caller passing a negative or out-of-bounds
|
|
410
|
+
* index would crash on `blockToRemove.call(REMOVED)` (undefined.call),
|
|
411
|
+
* aborting the surrounding batch (e.g. a Yjs undo transaction) partway
|
|
412
|
+
* and leaving the flat array inconsistent with the DOM — exactly the
|
|
413
|
+
* soil that grows the wrong-block-dropped bug class. Reject nonsense
|
|
414
|
+
* indices at the lowest level instead of throwing.
|
|
415
|
+
*/
|
|
416
|
+
if (removeIndex < 0 || removeIndex >= this.blocks.length) {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
|
|
296
420
|
const blockToRemove = this.blocks[removeIndex];
|
|
297
421
|
|
|
298
422
|
/**
|
|
@@ -311,10 +435,16 @@ export class Blocks {
|
|
|
311
435
|
* Remove all blocks
|
|
312
436
|
*/
|
|
313
437
|
public removeAll(): void {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
438
|
+
// Destroy each block so draggable listeners bound to shared DOM handles
|
|
439
|
+
// (e.g. the settings toggler) are released. Skipping destroy leaves stale
|
|
440
|
+
// mousedown handlers on the shared toggler, which later fire for whichever
|
|
441
|
+
// block is hovered and drags the wrong block.
|
|
442
|
+
this.blocks.forEach((block) => {
|
|
443
|
+
block.call(BlockToolAPI.REMOVED);
|
|
444
|
+
block.destroy();
|
|
445
|
+
});
|
|
317
446
|
|
|
447
|
+
this.workingArea.innerHTML = '';
|
|
318
448
|
this.blocks.length = 0;
|
|
319
449
|
}
|
|
320
450
|
|
|
@@ -327,6 +457,19 @@ export class Blocks {
|
|
|
327
457
|
public insertAfter(targetBlock: Block, newBlock: Block): void {
|
|
328
458
|
const index = this.blocks.indexOf(targetBlock);
|
|
329
459
|
|
|
460
|
+
/**
|
|
461
|
+
* Layer 14: stale target guard (regression: wrong-block-dropped family).
|
|
462
|
+
*
|
|
463
|
+
* Without this guard, a stale `targetBlock` makes `indexOf` return `-1`,
|
|
464
|
+
* then `insert(0, newBlock)` teleports the new block to the TOP of the
|
|
465
|
+
* document — the same symptom as wrong-block-dropped. insertAfter has
|
|
466
|
+
* no live callers today, but codifying the guard now prevents a future
|
|
467
|
+
* consumer from reintroducing the bug class through this entry point.
|
|
468
|
+
*/
|
|
469
|
+
if (index === -1) {
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
|
|
330
473
|
this.insert(index + 1, newBlock);
|
|
331
474
|
}
|
|
332
475
|
|
|
@@ -357,6 +500,16 @@ export class Blocks {
|
|
|
357
500
|
* blocks exist in the array before any lifecycle hooks run.
|
|
358
501
|
*/
|
|
359
502
|
public addToArray(index: number, block: Block): void {
|
|
503
|
+
/**
|
|
504
|
+
* Invalid-index guard (regression: wrong-block-dropped family).
|
|
505
|
+
* Same splice(-1, 0) vulnerability as Blocks.move/insert/insertMany.
|
|
506
|
+
* yjs-sync batch-add calls this during undo of hierarchical blocks;
|
|
507
|
+
* negative input would silently corrupt the flat array.
|
|
508
|
+
*/
|
|
509
|
+
if (index < 0) {
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
|
|
360
513
|
const insertIndex = index > this.length ? this.length : index;
|
|
361
514
|
|
|
362
515
|
this.blocks.splice(insertIndex, 0, block);
|
|
@@ -420,6 +573,39 @@ export class Blocks {
|
|
|
420
573
|
block.call(BlockToolAPI.RENDERED);
|
|
421
574
|
}
|
|
422
575
|
|
|
576
|
+
/**
|
|
577
|
+
* Place a single block's holder at workingArea root level. Finds the next
|
|
578
|
+
* top-level block at or after `insertIndex` in the flat array and inserts
|
|
579
|
+
* before it; falls back to appending to workingArea when no top-level
|
|
580
|
+
* sibling follows. Keeps DOM parent aligned with `parentId === null`.
|
|
581
|
+
*/
|
|
582
|
+
private insertAtRootLevel(block: Block, insertIndex: number): void {
|
|
583
|
+
const nextTopLevel = this.blocks.slice(insertIndex + 1).find(
|
|
584
|
+
(b) => b.holder.parentElement === this.workingArea
|
|
585
|
+
);
|
|
586
|
+
|
|
587
|
+
if (nextTopLevel !== undefined) {
|
|
588
|
+
nextTopLevel.holder.insertAdjacentElement('beforebegin', block.holder);
|
|
589
|
+
} else {
|
|
590
|
+
this.workingArea.appendChild(block.holder);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Invariant: forceTopLevel insertion must land at workingArea root. If it
|
|
595
|
+
* does not, a future change broke the resolution logic above and the
|
|
596
|
+
* Enter-after-callout bug is one wrong neighbor away from reappearing.
|
|
597
|
+
* Fail loud and early so the regression cannot sneak in silently.
|
|
598
|
+
*/
|
|
599
|
+
if (block.holder.parentElement !== this.workingArea) {
|
|
600
|
+
throw new Error(
|
|
601
|
+
'[Blocks.insertAtRootLevel] invariant violated: block holder did not land at workingArea root. ' +
|
|
602
|
+
'This indicates the Enter-after-callout regression guard is broken.'
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
block.call(BlockToolAPI.RENDERED);
|
|
607
|
+
}
|
|
608
|
+
|
|
423
609
|
/**
|
|
424
610
|
* When the previous block in the flat array is nested (e.g., a table cell
|
|
425
611
|
* paragraph), inserting after its top-level ancestor would place content
|
|
@@ -7,6 +7,7 @@ import { Module } from '../../__module';
|
|
|
7
7
|
import { Block } from '../../block';
|
|
8
8
|
import { BlockAPI } from '../../block/api';
|
|
9
9
|
import { capitalize } from '../../utils';
|
|
10
|
+
import { normalizeTableChildParents } from '../../utils/data-model-transform';
|
|
10
11
|
|
|
11
12
|
import { logLabeled } from './../../utils';
|
|
12
13
|
|
|
@@ -401,11 +402,23 @@ export class BlocksAPI extends Module {
|
|
|
401
402
|
): BlockAPIInterface[] => {
|
|
402
403
|
this.validateIndex(index);
|
|
403
404
|
|
|
404
|
-
|
|
405
|
+
// Backfill `parent` on children referenced by table cells so that
|
|
406
|
+
// alternative load paths (any consumer of the public API) get the
|
|
407
|
+
// same hierarchical correctness as Renderer.render(). Without this,
|
|
408
|
+
// flat-array article shapes lose their cell→child relationship and
|
|
409
|
+
// children render as detached top-level blocks.
|
|
410
|
+
const normalizedBlocks = normalizeTableChildParents(blocks);
|
|
411
|
+
|
|
412
|
+
const blocksToInsert = normalizedBlocks.map(({ id, type, data, tunes, parent, content, lastEditedAt, lastEditedBy }) => {
|
|
405
413
|
return this.Blok.BlockManager.composeBlock({
|
|
406
414
|
id,
|
|
407
415
|
tool: type || (this.config.defaultBlock as string),
|
|
408
416
|
data: data as BlockToolData,
|
|
417
|
+
tunes,
|
|
418
|
+
parentId: parent,
|
|
419
|
+
contentIds: content,
|
|
420
|
+
lastEditedAt,
|
|
421
|
+
lastEditedBy,
|
|
409
422
|
});
|
|
410
423
|
});
|
|
411
424
|
|
|
@@ -429,10 +442,12 @@ export class BlocksAPI extends Module {
|
|
|
429
442
|
|
|
430
443
|
const newBlock = this.Blok.BlockManager.insertInsideParent(parentId, insertIndex);
|
|
431
444
|
|
|
432
|
-
//
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
445
|
+
// NOTE: Do NOT call stopCapturing in a trailing microtask. The operations layer
|
|
446
|
+
// uses extendThroughRAF on its atomic wrapper to keep isSyncingFromYjs true
|
|
447
|
+
// through the next RAF, suppressing any stray mutation-observer-driven Yjs
|
|
448
|
+
// writes from deferred DOM callbacks. A trailing stopCapturing would force
|
|
449
|
+
// those late writes into a SEPARATE undo group, splitting the insertion
|
|
450
|
+
// across two CMD+Z pops.
|
|
436
451
|
|
|
437
452
|
return new BlockAPI(newBlock);
|
|
438
453
|
};
|
|
@@ -230,9 +230,18 @@ export class KeyboardNavigation extends BlockEventComposer {
|
|
|
230
230
|
}
|
|
231
231
|
|
|
232
232
|
private createBlockOnEnter(currentBlock: Block): Block {
|
|
233
|
+
/**
|
|
234
|
+
* When the current block is top-level (parentId === null), pass forceTopLevel
|
|
235
|
+
* so the new block's DOM holder is anchored at workingArea root level even if
|
|
236
|
+
* the previous block in the flat array is nested inside a callout/toggle/table.
|
|
237
|
+
* Without this, insertAdjacentElement('afterend', previousBlock.holder) drops
|
|
238
|
+
* the new holder inside the nested container — the Enter-after-callout bug.
|
|
239
|
+
*/
|
|
240
|
+
const isCurrentTopLevel = currentBlock.parentId === null;
|
|
241
|
+
|
|
233
242
|
// Case 1: Caret at start - insert block above
|
|
234
243
|
if (currentBlock.currentInput !== undefined && isCaretAtStartOfInput(currentBlock.currentInput) && !currentBlock.hasMedia && (currentBlock.parentId === null || !currentBlock.isEmpty)) {
|
|
235
|
-
const newBlock = this.Blok.BlockManager.insertDefaultBlockAtIndex(this.Blok.BlockManager.currentBlockIndex);
|
|
244
|
+
const newBlock = this.Blok.BlockManager.insertDefaultBlockAtIndex(this.Blok.BlockManager.currentBlockIndex, false, false, isCurrentTopLevel);
|
|
236
245
|
|
|
237
246
|
/**
|
|
238
247
|
* When the current block is a child of a toggle, the new block inserted above
|
|
@@ -258,19 +267,21 @@ export class KeyboardNavigation extends BlockEventComposer {
|
|
|
258
267
|
return promotedBlock;
|
|
259
268
|
}
|
|
260
269
|
|
|
261
|
-
const newBlock = this.Blok.BlockManager.insertDefaultBlockAtIndex(this.Blok.BlockManager.currentBlockIndex + 1);
|
|
262
|
-
|
|
263
270
|
/**
|
|
264
271
|
* When the current block is an open toggle (heading or list item),
|
|
265
272
|
* the new block should become a child of the toggle rather than a sibling.
|
|
266
273
|
* Detect via the data-blok-toggle-open DOM attribute set by toggle tools.
|
|
267
274
|
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
275
|
+
* forceTopLevel is safe to pass only when the current block is top-level
|
|
276
|
+
* AND is not an open toggle (which intentionally nests the new block as its child).
|
|
270
277
|
*/
|
|
271
278
|
const toggleWrapper = currentBlock.holder.querySelector('[data-blok-toggle-open]');
|
|
279
|
+
const isToggleOpen = toggleWrapper?.getAttribute('data-blok-toggle-open') === 'true';
|
|
280
|
+
const forceTopLevelCase2 = isCurrentTopLevel && !isToggleOpen;
|
|
281
|
+
|
|
282
|
+
const newBlock = this.Blok.BlockManager.insertDefaultBlockAtIndex(this.Blok.BlockManager.currentBlockIndex + 1, false, false, forceTopLevelCase2);
|
|
272
283
|
|
|
273
|
-
if (
|
|
284
|
+
if (isToggleOpen) {
|
|
274
285
|
this.Blok.BlockManager.setBlockParent(newBlock, currentBlock.id);
|
|
275
286
|
} else if (currentBlock.parentId !== null && newBlock.parentId !== currentBlock.parentId) {
|
|
276
287
|
this.Blok.BlockManager.setBlockParent(newBlock, currentBlock.parentId);
|