@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
- * @param spec - a single root node or an array of root nodes.
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
- // Pre-order DFS: push self FIRST, then recurse each child, so the flat array
459
- // is DFS-contiguous. `content` is filled as each child is visited and returns
460
- // its id; it is omitted for leaves.
461
- const visit = (node, parent) => {
462
- const id = node.id ?? generateId();
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
- visit(root, rootParent);
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
- /** Last per-child decorator passed to ctx.mountChildren; re-applied on each remount. */
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
- stampedChildren = new Map();
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
- applyChildAttributes(this.stampedChildren, children, this.childAttributes);
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);