@bloklabs/angular 1.8.0 → 1.9.0
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.
|
@@ -420,6 +420,39 @@ const generateId = (prefix = '') => {
|
|
|
420
420
|
return `${prefix}${(Math.floor(Math.random() * ID_RANDOM_MULTIPLIER)).toString(HEXADECIMAL_RADIX)}`;
|
|
421
421
|
};
|
|
422
422
|
|
|
423
|
+
/**
|
|
424
|
+
* Whether a spec node is a pre-flat run (`{ blocks: [...] }`) rather than a
|
|
425
|
+
* tree node. Keyed on the `blocks` array, the only field the two shapes cannot
|
|
426
|
+
* share.
|
|
427
|
+
* @param node - spec node to classify
|
|
428
|
+
*/
|
|
429
|
+
const isRunSpec = (node) => {
|
|
430
|
+
return Array.isArray(node.blocks);
|
|
431
|
+
};
|
|
432
|
+
/**
|
|
433
|
+
* Wire-tolerant id read: `null` and `''` mean "absent", as everywhere else a
|
|
434
|
+
* loaded document is normalized.
|
|
435
|
+
* @param id - id as it arrived on a saved block
|
|
436
|
+
*/
|
|
437
|
+
const nonEmptyId = (id) => {
|
|
438
|
+
return typeof id === 'string' && id !== '' ? id : undefined;
|
|
439
|
+
};
|
|
440
|
+
/**
|
|
441
|
+
* Reject an already-flat block passed where a TREE node is expected. A spec
|
|
442
|
+
* node has no `parent`/`content` fields, so such a block would be flattened
|
|
443
|
+
* with its links silently DROPPED — the structure it described is gone and
|
|
444
|
+
* nothing reports it. Pre-flat blocks belong in a run node instead.
|
|
445
|
+
* @param node - tree node about to be flattened
|
|
446
|
+
* @throws if the node carries link information a tree node cannot express
|
|
447
|
+
*/
|
|
448
|
+
const assertNotPreFlat = (node) => {
|
|
449
|
+
const asFlat = node;
|
|
450
|
+
const carriesParent = typeof asFlat.parent === 'string' && asFlat.parent !== '';
|
|
451
|
+
const carriesContent = Array.isArray(asFlat.content) && asFlat.content.length > 0;
|
|
452
|
+
if (carriesParent || carriesContent) {
|
|
453
|
+
throw new Error(`flattenTree: block "${node.id ?? '(no id)'}" carries \`parent\`/\`content\` links, which a tree node cannot express — pass pre-flat blocks as a run node: { blocks: [...] }.`);
|
|
454
|
+
}
|
|
455
|
+
};
|
|
423
456
|
/**
|
|
424
457
|
* Flatten a hierarchical block spec (nodes with `children`) into the flat DFS
|
|
425
458
|
* pre-order `OutputBlockData[]` Blok stores, wiring every node's `parent` and
|
|
@@ -441,29 +474,82 @@ const generateId = (prefix = '') => {
|
|
|
441
474
|
* },
|
|
442
475
|
* });
|
|
443
476
|
*
|
|
444
|
-
*
|
|
477
|
+
* Content that is not tree-shaped to begin with — an already-flat saved
|
|
478
|
+
* document a migration is splicing into a page — goes in as a RUN node,
|
|
479
|
+
* `{ blocks: [...] }`, at the root or as a child. A run is spliced verbatim:
|
|
480
|
+
* ids, `data`, `tunes` and existing `parent`/`content` links are kept, and only
|
|
481
|
+
* the blocks it left un-parented are re-parented onto the enclosing node.
|
|
482
|
+
*
|
|
483
|
+
* @example
|
|
484
|
+
* flattenTree({ type: 'column', children: [{ blocks: saved.blocks }] });
|
|
485
|
+
*
|
|
486
|
+
* @param spec - a single root node or an array of root nodes; each is either a
|
|
487
|
+
* tree node or a pre-flat run.
|
|
445
488
|
* @param options - `parentId` sets the `parent` of the root node(s);
|
|
446
489
|
* `generateId` overrides id generation for nodes that omit an `id` (default:
|
|
447
490
|
* Blok's nanoid scheme).
|
|
448
491
|
* @returns DFS pre-order blocks with `parent`/`content` wired. Leaves omit the
|
|
449
492
|
* empty `content` array, matching the documented `OutputBlockData` shape.
|
|
450
493
|
* @throws if an explicit `id` is reused within the spec — a duplicate id would
|
|
451
|
-
* corrupt every id-keyed lookup, so it is surfaced loudly rather than encoded
|
|
494
|
+
* corrupt every id-keyed lookup, so it is surfaced loudly rather than encoded
|
|
495
|
+
* — or if a TREE node carries `parent`/`content` links, which it cannot
|
|
496
|
+
* express (that is a pre-flat block; wrap it in a run node).
|
|
452
497
|
*/
|
|
453
498
|
function flattenTree(spec, options = {}) {
|
|
454
499
|
const generateId = options.generateId ?? generateBlockId;
|
|
455
500
|
const rootParent = options.parentId ?? undefined;
|
|
456
501
|
const flat = [];
|
|
457
502
|
const usedIds = new Set();
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
503
|
+
/**
|
|
504
|
+
* Resolve a node's id and reserve it. A duplicate would corrupt every
|
|
505
|
+
* id-keyed lookup, so it is surfaced loudly rather than encoded.
|
|
506
|
+
* @param explicitId - id the spec carried, if any
|
|
507
|
+
*/
|
|
508
|
+
const claimId = (explicitId) => {
|
|
509
|
+
const id = explicitId ?? generateId();
|
|
463
510
|
if (usedIds.has(id)) {
|
|
464
511
|
throw new Error(`flattenTree: duplicate block id "${id}" — every block id must be unique.`);
|
|
465
512
|
}
|
|
466
513
|
usedIds.add(id);
|
|
514
|
+
return id;
|
|
515
|
+
};
|
|
516
|
+
/**
|
|
517
|
+
* Splice a pre-flat run in verbatim: every block keeps its id, `data`,
|
|
518
|
+
* `tunes` and its existing `parent`/`content` links, and only the blocks the
|
|
519
|
+
* run left un-parented are re-parented onto the enclosing node. Mirrors
|
|
520
|
+
* `useBlocks.insertMarkdown`, which nests a converted run the same way.
|
|
521
|
+
* @param run - the run node
|
|
522
|
+
* @param parent - id of the node the run is spliced into (undefined at root)
|
|
523
|
+
* @returns ids of the run's top-level blocks, in run order
|
|
524
|
+
*/
|
|
525
|
+
const visitRun = (run, parent) => {
|
|
526
|
+
const rootIds = [];
|
|
527
|
+
for (const block of run.blocks) {
|
|
528
|
+
const { parent: incomingParent, id: incomingId, content: incomingContent, data, ...rest } = block;
|
|
529
|
+
const id = claimId(nonEmptyId(incomingId));
|
|
530
|
+
const incomingParentId = nonEmptyId(incomingParent);
|
|
531
|
+
const isRunRoot = incomingParentId === undefined;
|
|
532
|
+
const resolvedParent = isRunRoot ? parent : incomingParentId;
|
|
533
|
+
flat.push({
|
|
534
|
+
...rest,
|
|
535
|
+
id,
|
|
536
|
+
data: data ?? {},
|
|
537
|
+
// `null`/`[]` content means "no children" — omitted, like a tree leaf.
|
|
538
|
+
...(Array.isArray(incomingContent) && incomingContent.length > 0 ? { content: incomingContent } : {}),
|
|
539
|
+
...(resolvedParent !== undefined ? { parent: resolvedParent } : {}),
|
|
540
|
+
});
|
|
541
|
+
if (isRunRoot) {
|
|
542
|
+
rootIds.push(id);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
return rootIds;
|
|
546
|
+
};
|
|
547
|
+
// Pre-order DFS: push self FIRST, then recurse each child, so the flat array
|
|
548
|
+
// is DFS-contiguous. `content` is filled as each child is visited and returns
|
|
549
|
+
// its id; it is omitted for leaves.
|
|
550
|
+
const visit = (node, parent) => {
|
|
551
|
+
assertNotPreFlat(node);
|
|
552
|
+
const id = claimId(node.id);
|
|
467
553
|
const childIds = [];
|
|
468
554
|
const flatNode = {
|
|
469
555
|
id,
|
|
@@ -476,7 +562,7 @@ function flattenTree(spec, options = {}) {
|
|
|
476
562
|
};
|
|
477
563
|
flat.push(flatNode);
|
|
478
564
|
for (const child of node.children ?? []) {
|
|
479
|
-
childIds.push(visit(child, id));
|
|
565
|
+
childIds.push(...(isRunSpec(child) ? visitRun(child, id) : [visit(child, id)]));
|
|
480
566
|
}
|
|
481
567
|
// Only wire `content` when there are children — leaves stay clean.
|
|
482
568
|
if (childIds.length > 0) {
|
|
@@ -486,7 +572,12 @@ function flattenTree(spec, options = {}) {
|
|
|
486
572
|
};
|
|
487
573
|
const roots = Array.isArray(spec) ? spec : [spec];
|
|
488
574
|
for (const root of roots) {
|
|
489
|
-
|
|
575
|
+
if (isRunSpec(root)) {
|
|
576
|
+
visitRun(root, rootParent);
|
|
577
|
+
}
|
|
578
|
+
else {
|
|
579
|
+
visit(root, rootParent);
|
|
580
|
+
}
|
|
490
581
|
}
|
|
491
582
|
return flat;
|
|
492
583
|
}
|
|
@@ -2327,6 +2418,23 @@ const DATA_ATTR = {
|
|
|
2327
2418
|
/** Element excluded from mutation tracking */
|
|
2328
2419
|
mutationFree: 'data-blok-mutation-free',
|
|
2329
2420
|
// ============================================
|
|
2421
|
+
// Keyboard Ownership
|
|
2422
|
+
// ============================================
|
|
2423
|
+
/** Marks a subtree whose keyboard belongs to the Tool that rendered it, not to
|
|
2424
|
+
* the editor. Blok's block-level keydown/keyup handling stands down entirely
|
|
2425
|
+
* for events originating inside it — Escape, Tab, the arrows, "/" and the
|
|
2426
|
+
* Enter/Backspace/Delete structural keys all reach the element untouched.
|
|
2427
|
+
*
|
|
2428
|
+
* Blok already exempts native `<input>`/`<textarea>` from the STRUCTURAL keys
|
|
2429
|
+
* (Enter/Backspace/Delete and "/") because those are contenteditable-shaped
|
|
2430
|
+
* and would splice the document around a form field. Escape/Tab/arrows are
|
|
2431
|
+
* deliberately NOT exempt — they are how a user leaves a field. That default
|
|
2432
|
+
* is right for a one-line title input and wrong for a field with its own
|
|
2433
|
+
* keyboard semantics (Tab between sub-fields, Escape to cancel an edit, arrows
|
|
2434
|
+
* to walk a suggestion list), which is exactly what this attribute is for.
|
|
2435
|
+
* Public authoring hook. */
|
|
2436
|
+
keyboardOwner: 'data-blok-keyboard-owner',
|
|
2437
|
+
// ============================================
|
|
2330
2438
|
// Navigation
|
|
2331
2439
|
// ============================================
|
|
2332
2440
|
/** Block has navigation focus */
|