@bloklabs/angular 1.9.0 → 1.10.1
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.
|
@@ -765,6 +765,73 @@ const EMPTY_API = {
|
|
|
765
765
|
clear: async () => undefined,
|
|
766
766
|
isSyncingFromYjs: () => false,
|
|
767
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
|
+
};
|
|
768
835
|
/**
|
|
769
836
|
* Build the framework-agnostic, id/parentId-relative block-tree API over a LIVE
|
|
770
837
|
* editor. Mutators route through the editor-level `blocks` API (core's
|
|
@@ -2741,6 +2808,101 @@ const mountChildBlocks = (container, children) => {
|
|
|
2741
2808
|
}
|
|
2742
2809
|
};
|
|
2743
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
|
+
|
|
2744
2906
|
/**
|
|
2745
2907
|
* Fired when a CONTAINER block's child holders have been mounted into its
|
|
2746
2908
|
* child slot — the moment the child DOM has settled and a caret set into a
|
|
@@ -3478,37 +3640,6 @@ const CREATION_ORIGINS = new Set([
|
|
|
3478
3640
|
'api',
|
|
3479
3641
|
'convert',
|
|
3480
3642
|
]);
|
|
3481
|
-
/**
|
|
3482
|
-
* Apply one pass of per-child holder attributes and clear the previous pass's
|
|
3483
|
-
* leftovers — including on a child that has since left the container, which
|
|
3484
|
-
* would otherwise keep a dead index forever.
|
|
3485
|
-
* @param stamped - the container's ledger of what the last pass wrote (mutated)
|
|
3486
|
-
* @param children - the container's model children, in model order
|
|
3487
|
-
* @param decorate - the authored per-child decorator, if any
|
|
3488
|
-
*/
|
|
3489
|
-
const applyChildAttributes = (stamped, children, decorate) => {
|
|
3490
|
-
const next = new Map();
|
|
3491
|
-
children.forEach((child, index) => {
|
|
3492
|
-
const names = [];
|
|
3493
|
-
for (const [name, value] of Object.entries(decorate?.(child, index) ?? {})) {
|
|
3494
|
-
if (value === null || value === undefined) {
|
|
3495
|
-
child.holder.removeAttribute(name);
|
|
3496
|
-
continue;
|
|
3497
|
-
}
|
|
3498
|
-
child.holder.setAttribute(name, String(value));
|
|
3499
|
-
names.push(name);
|
|
3500
|
-
}
|
|
3501
|
-
next.set(child.id, { holder: child.holder, names });
|
|
3502
|
-
});
|
|
3503
|
-
for (const [id, previous] of stamped) {
|
|
3504
|
-
const current = next.get(id);
|
|
3505
|
-
previous.names
|
|
3506
|
-
.filter(name => current === undefined || !current.names.includes(name))
|
|
3507
|
-
.forEach(name => previous.holder.removeAttribute(name));
|
|
3508
|
-
}
|
|
3509
|
-
stamped.clear();
|
|
3510
|
-
next.forEach((entry, id) => stamped.set(id, entry));
|
|
3511
|
-
};
|
|
3512
3643
|
/**
|
|
3513
3644
|
* Announce that a container's child holders have settled in its slot — the
|
|
3514
3645
|
* signal a host waits on before putting the caret into a freshly inserted
|
|
@@ -3573,10 +3704,17 @@ function createAngularBlock(spec) {
|
|
|
3573
3704
|
hostEl = null;
|
|
3574
3705
|
/** Last host passed to ctx.mountChildren, re-mounted on each data change. */
|
|
3575
3706
|
childHost = null;
|
|
3576
|
-
/**
|
|
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
|
+
*/
|
|
3577
3714
|
childAttributes;
|
|
3715
|
+
childContentAttributes;
|
|
3578
3716
|
/** What the last decoration pass wrote, so a dropped key is cleaned up. */
|
|
3579
|
-
|
|
3717
|
+
childLedger = createChildDecorationLedger();
|
|
3580
3718
|
/** Why core built this instance — gates `onCreated`, handed to `onMounted`. */
|
|
3581
3719
|
origin;
|
|
3582
3720
|
/** True once the post-mount hooks fired; a repeated rendered() must not re-fire them. */
|
|
@@ -3608,6 +3746,7 @@ function createAngularBlock(spec) {
|
|
|
3608
3746
|
api: this.api,
|
|
3609
3747
|
readOnly: this.readOnlySig.asReadonly(),
|
|
3610
3748
|
mountChildren: this.mountChildren,
|
|
3749
|
+
setToolbarAnchor: this.setToolbarAnchor,
|
|
3611
3750
|
};
|
|
3612
3751
|
}
|
|
3613
3752
|
render() {
|
|
@@ -3687,12 +3826,30 @@ function createAngularBlock(spec) {
|
|
|
3687
3826
|
* exactly what core's default positioning already assumes.
|
|
3688
3827
|
*/
|
|
3689
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
|
+
}
|
|
3690
3839
|
const host = this.hostEl;
|
|
3691
3840
|
if (host === null || spec.getToolbarAnchorElement === undefined) {
|
|
3692
3841
|
return undefined;
|
|
3693
3842
|
}
|
|
3694
3843
|
return spec.getToolbarAnchorElement(host, this.blockApi) ?? undefined;
|
|
3695
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
|
+
};
|
|
3696
3853
|
moved() {
|
|
3697
3854
|
// No remount: core relocates the host element; the mounted view rides along
|
|
3698
3855
|
// as its DOM children.
|
|
@@ -3713,9 +3870,10 @@ function createAngularBlock(spec) {
|
|
|
3713
3870
|
* Container blocks: remember the host (and any per-child decorator) and
|
|
3714
3871
|
* (re)mount the real child holders.
|
|
3715
3872
|
*/
|
|
3716
|
-
mountChildren = (host, childAttributes) => {
|
|
3873
|
+
mountChildren = (host, childAttributes, childContentAttributes) => {
|
|
3717
3874
|
this.childHost = host;
|
|
3718
3875
|
this.childAttributes = childAttributes;
|
|
3876
|
+
this.childContentAttributes = childContentAttributes;
|
|
3719
3877
|
host.setAttribute(DATA_ATTR.nestedBlocks, '');
|
|
3720
3878
|
this.remountChildren();
|
|
3721
3879
|
};
|
|
@@ -3725,7 +3883,10 @@ function createAngularBlock(spec) {
|
|
|
3725
3883
|
}
|
|
3726
3884
|
const children = this.blockApi.getChildren();
|
|
3727
3885
|
mountChildBlocks(this.childHost, children);
|
|
3728
|
-
|
|
3886
|
+
applyChildDecoration(this.childLedger, children, {
|
|
3887
|
+
childAttributes: this.childAttributes,
|
|
3888
|
+
childContentAttributes: this.childContentAttributes,
|
|
3889
|
+
});
|
|
3729
3890
|
emitChildrenMounted(this.api, this.blockApi.id, children);
|
|
3730
3891
|
}
|
|
3731
3892
|
/**
|
|
@@ -3827,8 +3988,16 @@ const BLOCK_CHANGED_EVENT = 'block changed';
|
|
|
3827
3988
|
* `ed === sub.editor` makes it a no-op, so no double-subscription occurs.
|
|
3828
3989
|
*
|
|
3829
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
|
|
3830
3993
|
*/
|
|
3831
|
-
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;
|
|
3832
4001
|
// Bumped on every `block changed`; the shared API's read methods touch it (via
|
|
3833
4002
|
// the onRead seam) so template/computed reads stay reactive.
|
|
3834
4003
|
const version = signal(0, /* @ts-ignore */
|
|
@@ -3866,7 +4035,11 @@ function injectBlocks(editor) {
|
|
|
3866
4035
|
bound.set(EMPTY_API);
|
|
3867
4036
|
return;
|
|
3868
4037
|
}
|
|
3869
|
-
const handler = () => {
|
|
4038
|
+
const handler = (payload) => {
|
|
4039
|
+
const within = resolveWithin();
|
|
4040
|
+
if (within !== null && !changeTouchesSubtree(ed, payload, within)) {
|
|
4041
|
+
return;
|
|
4042
|
+
}
|
|
3870
4043
|
version.update((v) => v + 1);
|
|
3871
4044
|
};
|
|
3872
4045
|
ed.on(BLOCK_CHANGED_EVENT, handler);
|