@bloklabs/angular 1.8.0 → 1.10.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
|
}
|
|
@@ -674,6 +765,73 @@ const EMPTY_API = {
|
|
|
674
765
|
clear: async () => undefined,
|
|
675
766
|
isSyncingFromYjs: () => false,
|
|
676
767
|
};
|
|
768
|
+
/**
|
|
769
|
+
* Read the changed block's id out of a `block changed` payload, tolerating any
|
|
770
|
+
* shape that is not core's `{ event: { detail: { target } } }`.
|
|
771
|
+
* @param payload - whatever the dispatcher handed the listener
|
|
772
|
+
* @returns the target block id, or null when the payload does not carry one
|
|
773
|
+
*/
|
|
774
|
+
const changedBlockId = (payload) => {
|
|
775
|
+
const detail = payload
|
|
776
|
+
?.event?.detail?.target?.id;
|
|
777
|
+
return typeof detail === 'string' ? detail : null;
|
|
778
|
+
};
|
|
779
|
+
/**
|
|
780
|
+
* Does a `block changed` event touch the subtree rooted at `withinId`?
|
|
781
|
+
*
|
|
782
|
+
* The reactivity filter behind `useBlocks(editor, { within })` in all three
|
|
783
|
+
* adapters. Without it every consumer of `useBlocks` re-renders on every change
|
|
784
|
+
* anywhere in the document — so a container block that only renders its own
|
|
785
|
+
* children still re-rendered on each keystroke in an unrelated block, and a page
|
|
786
|
+
* of N such containers turned one keystroke into N re-renders.
|
|
787
|
+
*
|
|
788
|
+
* INDETERMINATE READS AS "TOUCHED". The walk resolves ancestry through the LIVE
|
|
789
|
+
* tree, and a removal commonly emits after the block is already gone, so its
|
|
790
|
+
* parent chain cannot be read. Answering `false` there would leave a container
|
|
791
|
+
* rendering a child that no longer exists; answering `true` costs one extra
|
|
792
|
+
* render pass. Same for a payload with no readable target.
|
|
793
|
+
*
|
|
794
|
+
* Ancestry is walked UPWARD one `getById` at a time rather than snapshotted with
|
|
795
|
+
* `parentMap`: this runs on every emission — i.e. on every keystroke — so it must
|
|
796
|
+
* cost O(depth), not O(document).
|
|
797
|
+
* @param editor - the live Blok instance
|
|
798
|
+
* @param payload - the `block changed` payload
|
|
799
|
+
* @param withinId - id of the block whose subtree is the scope
|
|
800
|
+
* @returns true when the change is inside the scope (or cannot be placed)
|
|
801
|
+
*/
|
|
802
|
+
const changeTouchesSubtree = (editor, payload, withinId) => {
|
|
803
|
+
const targetId = changedBlockId(payload);
|
|
804
|
+
if (targetId === null) {
|
|
805
|
+
return true;
|
|
806
|
+
}
|
|
807
|
+
/** Ids already visited on this walk — one Set for the whole climb. */
|
|
808
|
+
const seen = new Set();
|
|
809
|
+
/**
|
|
810
|
+
* Walk one link up the parentId chain.
|
|
811
|
+
* @param id - the block to test, or null once the root is passed
|
|
812
|
+
* @returns true when `withinId` is `id` or one of its ancestors
|
|
813
|
+
*/
|
|
814
|
+
const climbs = (id) => {
|
|
815
|
+
if (id === null) {
|
|
816
|
+
return false;
|
|
817
|
+
}
|
|
818
|
+
if (id === withinId) {
|
|
819
|
+
return true;
|
|
820
|
+
}
|
|
821
|
+
// A parentId cycle is a corrupt tree, not a scope hit — bail rather than spin.
|
|
822
|
+
if (seen.has(id)) {
|
|
823
|
+
return false;
|
|
824
|
+
}
|
|
825
|
+
seen.add(id);
|
|
826
|
+
const node = editor.blocks.getById(id);
|
|
827
|
+
// The target (or one of its ancestors) is no longer in the tree.
|
|
828
|
+
if (node === null || node === undefined) {
|
|
829
|
+
return true;
|
|
830
|
+
}
|
|
831
|
+
return climbs(node.parentId);
|
|
832
|
+
};
|
|
833
|
+
return climbs(targetId);
|
|
834
|
+
};
|
|
677
835
|
/**
|
|
678
836
|
* Build the framework-agnostic, id/parentId-relative block-tree API over a LIVE
|
|
679
837
|
* editor. Mutators route through the editor-level `blocks` API (core's
|
|
@@ -2327,6 +2485,23 @@ const DATA_ATTR = {
|
|
|
2327
2485
|
/** Element excluded from mutation tracking */
|
|
2328
2486
|
mutationFree: 'data-blok-mutation-free',
|
|
2329
2487
|
// ============================================
|
|
2488
|
+
// Keyboard Ownership
|
|
2489
|
+
// ============================================
|
|
2490
|
+
/** Marks a subtree whose keyboard belongs to the Tool that rendered it, not to
|
|
2491
|
+
* the editor. Blok's block-level keydown/keyup handling stands down entirely
|
|
2492
|
+
* for events originating inside it — Escape, Tab, the arrows, "/" and the
|
|
2493
|
+
* Enter/Backspace/Delete structural keys all reach the element untouched.
|
|
2494
|
+
*
|
|
2495
|
+
* Blok already exempts native `<input>`/`<textarea>` from the STRUCTURAL keys
|
|
2496
|
+
* (Enter/Backspace/Delete and "/") because those are contenteditable-shaped
|
|
2497
|
+
* and would splice the document around a form field. Escape/Tab/arrows are
|
|
2498
|
+
* deliberately NOT exempt — they are how a user leaves a field. That default
|
|
2499
|
+
* is right for a one-line title input and wrong for a field with its own
|
|
2500
|
+
* keyboard semantics (Tab between sub-fields, Escape to cancel an edit, arrows
|
|
2501
|
+
* to walk a suggestion list), which is exactly what this attribute is for.
|
|
2502
|
+
* Public authoring hook. */
|
|
2503
|
+
keyboardOwner: 'data-blok-keyboard-owner',
|
|
2504
|
+
// ============================================
|
|
2330
2505
|
// Navigation
|
|
2331
2506
|
// ============================================
|
|
2332
2507
|
/** Block has navigation focus */
|
|
@@ -2633,6 +2808,101 @@ const mountChildBlocks = (container, children) => {
|
|
|
2633
2808
|
}
|
|
2634
2809
|
};
|
|
2635
2810
|
|
|
2811
|
+
/**
|
|
2812
|
+
* A fresh pair of ledgers for one container's child slot.
|
|
2813
|
+
* @returns empty holder/content ledgers
|
|
2814
|
+
*/
|
|
2815
|
+
const createChildDecorationLedger = () => ({
|
|
2816
|
+
holders: new Map(),
|
|
2817
|
+
contents: new Map(),
|
|
2818
|
+
});
|
|
2819
|
+
/**
|
|
2820
|
+
* The child's OWN content wrapper, or null when its DOM has not committed yet.
|
|
2821
|
+
*
|
|
2822
|
+
* The first `[data-blok-element-content]` in document order under a holder is
|
|
2823
|
+
* always that block's own: a descendant block's wrapper necessarily sits INSIDE
|
|
2824
|
+
* it. Not `firstElementChild` — block tunes wrap the content node, so it is not
|
|
2825
|
+
* reliably a direct child (this is the same lookup core's own
|
|
2826
|
+
* `refreshToolRootElement` uses).
|
|
2827
|
+
* @param holder - the child block's holder
|
|
2828
|
+
* @returns the content wrapper, or null
|
|
2829
|
+
*/
|
|
2830
|
+
const contentWrapperOf = (holder) => holder.querySelector(`[${DATA_ATTR.elementContent}]`);
|
|
2831
|
+
/**
|
|
2832
|
+
* Apply one decoration pass and clear the previous pass's leftovers — including
|
|
2833
|
+
* on a child that has since left the container, which would otherwise keep a
|
|
2834
|
+
* dead index forever.
|
|
2835
|
+
* @param ledger - what the last pass wrote (mutated)
|
|
2836
|
+
* @param children - the container's model children, in model order
|
|
2837
|
+
* @param decorate - the per-child decorator for the child HOLDERS, if any
|
|
2838
|
+
* @param resolveTarget - which element of a child this pass writes to
|
|
2839
|
+
*/
|
|
2840
|
+
const applyPass = (ledger, children, decorate, resolveTarget) => {
|
|
2841
|
+
const next = new Map();
|
|
2842
|
+
children.forEach((child, index) => {
|
|
2843
|
+
const target = resolveTarget(child.holder);
|
|
2844
|
+
/**
|
|
2845
|
+
* A portal-rendered child commits its DOM a frame after core inserts it, so
|
|
2846
|
+
* its content wrapper can legitimately be missing on this pass. Skipping
|
|
2847
|
+
* (rather than throwing) leaves the next pass to stamp it.
|
|
2848
|
+
*/
|
|
2849
|
+
if (target === null) {
|
|
2850
|
+
return;
|
|
2851
|
+
}
|
|
2852
|
+
const names = [];
|
|
2853
|
+
for (const [name, value] of Object.entries(decorate?.(child, index) ?? {})) {
|
|
2854
|
+
if (value === null || value === undefined) {
|
|
2855
|
+
target.removeAttribute(name);
|
|
2856
|
+
continue;
|
|
2857
|
+
}
|
|
2858
|
+
target.setAttribute(name, String(value));
|
|
2859
|
+
names.push(name);
|
|
2860
|
+
}
|
|
2861
|
+
next.set(child.id, { target, names });
|
|
2862
|
+
});
|
|
2863
|
+
for (const [id, previous] of ledger) {
|
|
2864
|
+
const current = next.get(id);
|
|
2865
|
+
previous.names
|
|
2866
|
+
.filter(name => current === undefined || !current.names.includes(name))
|
|
2867
|
+
.forEach(name => previous.target.removeAttribute(name));
|
|
2868
|
+
}
|
|
2869
|
+
ledger.clear();
|
|
2870
|
+
next.forEach((entry, id) => ledger.set(id, entry));
|
|
2871
|
+
};
|
|
2872
|
+
/**
|
|
2873
|
+
* Decorate a container's children: named attributes on each child's HOLDER and,
|
|
2874
|
+
* separately, on each child's `[data-blok-element-content]` wrapper.
|
|
2875
|
+
*
|
|
2876
|
+
* The single implementation behind the React, Vue and Angular per-child
|
|
2877
|
+
* decoration channels, so the three cannot drift on which element a hook lands
|
|
2878
|
+
* on or on when a dropped hook is cleaned up.
|
|
2879
|
+
*
|
|
2880
|
+
* BOTH levels exist because core's child-holder decoration law blesses both, and
|
|
2881
|
+
* a container needs both: the holder is the child's outer box (rails, indices,
|
|
2882
|
+
* hover states), while the content wrapper is where the child's own text box
|
|
2883
|
+
* begins — which is what a numbered rail or a connector line has to align to.
|
|
2884
|
+
* With only the holder reachable, a container had to encode core's wrapper chain
|
|
2885
|
+
* in its own CSS (`[data-step] > [data-blok-element-content] > …`) and broke
|
|
2886
|
+
* whenever that chain changed. Neither hook may introduce an element: holders
|
|
2887
|
+
* stay DIRECT children of the slot because core's reparenting and caret
|
|
2888
|
+
* sibling checks compare `holder.parentElement` by identity.
|
|
2889
|
+
*
|
|
2890
|
+
* Both are inert with respect to change tracking: core's mutation filter drops a
|
|
2891
|
+
* holder- or content-targeted attribute record for the CHILD block (both are
|
|
2892
|
+
* ancestors of the child's tool element, not descendants) and scores it
|
|
2893
|
+
* mutation-free for the CONTAINER (whose own `data-blok-mutation-free` host is
|
|
2894
|
+
* the nearest such ancestor).
|
|
2895
|
+
* @param ledger - the slot's ledgers from {@link createChildDecorationLedger}
|
|
2896
|
+
* @param children - the container's model children, in model order
|
|
2897
|
+
* @param decorators - the authored per-child decorators
|
|
2898
|
+
* @param decorators.childAttributes - written on each child's holder
|
|
2899
|
+
* @param decorators.childContentAttributes - written on each child's content wrapper
|
|
2900
|
+
*/
|
|
2901
|
+
const applyChildDecoration = (ledger, children, decorators) => {
|
|
2902
|
+
applyPass(ledger.holders, children, decorators.childAttributes, holder => holder);
|
|
2903
|
+
applyPass(ledger.contents, children, decorators.childContentAttributes, contentWrapperOf);
|
|
2904
|
+
};
|
|
2905
|
+
|
|
2636
2906
|
/**
|
|
2637
2907
|
* Fired when a CONTAINER block's child holders have been mounted into its
|
|
2638
2908
|
* child slot — the moment the child DOM has settled and a caret set into a
|
|
@@ -3370,37 +3640,6 @@ const CREATION_ORIGINS = new Set([
|
|
|
3370
3640
|
'api',
|
|
3371
3641
|
'convert',
|
|
3372
3642
|
]);
|
|
3373
|
-
/**
|
|
3374
|
-
* Apply one pass of per-child holder attributes and clear the previous pass's
|
|
3375
|
-
* leftovers — including on a child that has since left the container, which
|
|
3376
|
-
* would otherwise keep a dead index forever.
|
|
3377
|
-
* @param stamped - the container's ledger of what the last pass wrote (mutated)
|
|
3378
|
-
* @param children - the container's model children, in model order
|
|
3379
|
-
* @param decorate - the authored per-child decorator, if any
|
|
3380
|
-
*/
|
|
3381
|
-
const applyChildAttributes = (stamped, children, decorate) => {
|
|
3382
|
-
const next = new Map();
|
|
3383
|
-
children.forEach((child, index) => {
|
|
3384
|
-
const names = [];
|
|
3385
|
-
for (const [name, value] of Object.entries(decorate?.(child, index) ?? {})) {
|
|
3386
|
-
if (value === null || value === undefined) {
|
|
3387
|
-
child.holder.removeAttribute(name);
|
|
3388
|
-
continue;
|
|
3389
|
-
}
|
|
3390
|
-
child.holder.setAttribute(name, String(value));
|
|
3391
|
-
names.push(name);
|
|
3392
|
-
}
|
|
3393
|
-
next.set(child.id, { holder: child.holder, names });
|
|
3394
|
-
});
|
|
3395
|
-
for (const [id, previous] of stamped) {
|
|
3396
|
-
const current = next.get(id);
|
|
3397
|
-
previous.names
|
|
3398
|
-
.filter(name => current === undefined || !current.names.includes(name))
|
|
3399
|
-
.forEach(name => previous.holder.removeAttribute(name));
|
|
3400
|
-
}
|
|
3401
|
-
stamped.clear();
|
|
3402
|
-
next.forEach((entry, id) => stamped.set(id, entry));
|
|
3403
|
-
};
|
|
3404
3643
|
/**
|
|
3405
3644
|
* Announce that a container's child holders have settled in its slot — the
|
|
3406
3645
|
* signal a host waits on before putting the caret into a freshly inserted
|
|
@@ -3465,10 +3704,17 @@ function createAngularBlock(spec) {
|
|
|
3465
3704
|
hostEl = null;
|
|
3466
3705
|
/** Last host passed to ctx.mountChildren, re-mounted on each data change. */
|
|
3467
3706
|
childHost = null;
|
|
3468
|
-
/**
|
|
3707
|
+
/** Element the component handed to ctx.setToolbarAnchor, if any. */
|
|
3708
|
+
anchorEl = null;
|
|
3709
|
+
/**
|
|
3710
|
+
* Last per-child decorators passed to ctx.mountChildren; BOTH re-applied on
|
|
3711
|
+
* every remount (the author calls mountChildren once, the factory re-runs the
|
|
3712
|
+
* mount on each data change).
|
|
3713
|
+
*/
|
|
3469
3714
|
childAttributes;
|
|
3715
|
+
childContentAttributes;
|
|
3470
3716
|
/** What the last decoration pass wrote, so a dropped key is cleaned up. */
|
|
3471
|
-
|
|
3717
|
+
childLedger = createChildDecorationLedger();
|
|
3472
3718
|
/** Why core built this instance — gates `onCreated`, handed to `onMounted`. */
|
|
3473
3719
|
origin;
|
|
3474
3720
|
/** True once the post-mount hooks fired; a repeated rendered() must not re-fire them. */
|
|
@@ -3500,6 +3746,7 @@ function createAngularBlock(spec) {
|
|
|
3500
3746
|
api: this.api,
|
|
3501
3747
|
readOnly: this.readOnlySig.asReadonly(),
|
|
3502
3748
|
mountChildren: this.mountChildren,
|
|
3749
|
+
setToolbarAnchor: this.setToolbarAnchor,
|
|
3503
3750
|
};
|
|
3504
3751
|
}
|
|
3505
3752
|
render() {
|
|
@@ -3579,12 +3826,30 @@ function createAngularBlock(spec) {
|
|
|
3579
3826
|
* exactly what core's default positioning already assumes.
|
|
3580
3827
|
*/
|
|
3581
3828
|
getToolbarAnchorElement() {
|
|
3829
|
+
/**
|
|
3830
|
+
* An element the component handed over wins — it is the exact node, chosen
|
|
3831
|
+
* from inside the template. `isConnected` is the guard that matters: a
|
|
3832
|
+
* component that re-renders its anchor can leave a detached node here, and
|
|
3833
|
+
* positioning the toolbar against one silently parks it at 0,0.
|
|
3834
|
+
*/
|
|
3835
|
+
const declared = this.anchorEl;
|
|
3836
|
+
if (declared !== null && declared.isConnected) {
|
|
3837
|
+
return declared;
|
|
3838
|
+
}
|
|
3582
3839
|
const host = this.hostEl;
|
|
3583
3840
|
if (host === null || spec.getToolbarAnchorElement === undefined) {
|
|
3584
3841
|
return undefined;
|
|
3585
3842
|
}
|
|
3586
3843
|
return spec.getToolbarAnchorElement(host, this.blockApi) ?? undefined;
|
|
3587
3844
|
}
|
|
3845
|
+
/**
|
|
3846
|
+
* `ctx.setToolbarAnchor`. A bound field so the context object handed through
|
|
3847
|
+
* DI keeps a stable identity.
|
|
3848
|
+
* @param element - the anchor element, or null to fall back to the spec hook
|
|
3849
|
+
*/
|
|
3850
|
+
setToolbarAnchor = (element) => {
|
|
3851
|
+
this.anchorEl = element;
|
|
3852
|
+
};
|
|
3588
3853
|
moved() {
|
|
3589
3854
|
// No remount: core relocates the host element; the mounted view rides along
|
|
3590
3855
|
// as its DOM children.
|
|
@@ -3605,9 +3870,10 @@ function createAngularBlock(spec) {
|
|
|
3605
3870
|
* Container blocks: remember the host (and any per-child decorator) and
|
|
3606
3871
|
* (re)mount the real child holders.
|
|
3607
3872
|
*/
|
|
3608
|
-
mountChildren = (host, childAttributes) => {
|
|
3873
|
+
mountChildren = (host, childAttributes, childContentAttributes) => {
|
|
3609
3874
|
this.childHost = host;
|
|
3610
3875
|
this.childAttributes = childAttributes;
|
|
3876
|
+
this.childContentAttributes = childContentAttributes;
|
|
3611
3877
|
host.setAttribute(DATA_ATTR.nestedBlocks, '');
|
|
3612
3878
|
this.remountChildren();
|
|
3613
3879
|
};
|
|
@@ -3617,7 +3883,10 @@ function createAngularBlock(spec) {
|
|
|
3617
3883
|
}
|
|
3618
3884
|
const children = this.blockApi.getChildren();
|
|
3619
3885
|
mountChildBlocks(this.childHost, children);
|
|
3620
|
-
|
|
3886
|
+
applyChildDecoration(this.childLedger, children, {
|
|
3887
|
+
childAttributes: this.childAttributes,
|
|
3888
|
+
childContentAttributes: this.childContentAttributes,
|
|
3889
|
+
});
|
|
3621
3890
|
emitChildrenMounted(this.api, this.blockApi.id, children);
|
|
3622
3891
|
}
|
|
3623
3892
|
/**
|
|
@@ -3719,8 +3988,16 @@ const BLOCK_CHANGED_EVENT = 'block changed';
|
|
|
3719
3988
|
* `ed === sub.editor` makes it a no-op, so no double-subscription occurs.
|
|
3720
3989
|
*
|
|
3721
3990
|
* @param editor - a signal of the Blok instance, or null pre-ready
|
|
3991
|
+
* @param options - reactivity options; see {@link InjectBlocksOptions.within} to
|
|
3992
|
+
* scope invalidation to one block's subtree
|
|
3722
3993
|
*/
|
|
3723
|
-
function injectBlocks(editor) {
|
|
3994
|
+
function injectBlocks(editor, options = {}) {
|
|
3995
|
+
/**
|
|
3996
|
+
* The scope, resolved per emission so a signal `within` takes effect with no
|
|
3997
|
+
* re-subscription. A signal is a zero-arg function, which is how it is told
|
|
3998
|
+
* apart from a plain id.
|
|
3999
|
+
*/
|
|
4000
|
+
const resolveWithin = () => (typeof options.within === 'function' ? options.within() : options.within) ?? null;
|
|
3724
4001
|
// Bumped on every `block changed`; the shared API's read methods touch it (via
|
|
3725
4002
|
// the onRead seam) so template/computed reads stay reactive.
|
|
3726
4003
|
const version = signal(0, /* @ts-ignore */
|
|
@@ -3758,7 +4035,11 @@ function injectBlocks(editor) {
|
|
|
3758
4035
|
bound.set(EMPTY_API);
|
|
3759
4036
|
return;
|
|
3760
4037
|
}
|
|
3761
|
-
const handler = () => {
|
|
4038
|
+
const handler = (payload) => {
|
|
4039
|
+
const within = resolveWithin();
|
|
4040
|
+
if (within !== null && !changeTouchesSubtree(ed, payload, within)) {
|
|
4041
|
+
return;
|
|
4042
|
+
}
|
|
3762
4043
|
version.update((v) => v + 1);
|
|
3763
4044
|
};
|
|
3764
4045
|
ed.on(BLOCK_CHANGED_EVENT, handler);
|