@jsenv/dom 0.17.46 → 0.17.48

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.
Files changed (2) hide show
  1. package/dist/jsenv_dom.js +462 -61
  2. package/package.json +6 -2
package/dist/jsenv_dom.js CHANGED
@@ -8557,7 +8557,7 @@ const isolateInteractions = (elements) => {
8557
8557
 
8558
8558
  installImportMetaCssBuild(import.meta);
8559
8559
 
8560
- const css$5 = /* css */ `.navi_drag_gesture_backdrop {
8560
+ const css$6 = /* css */ `.navi_drag_gesture_backdrop {
8561
8561
  touch-action: none;
8562
8562
  user-select: none;
8563
8563
  position: fixed;
@@ -8568,7 +8568,7 @@ const css$5 = /* css */ `.navi_drag_gesture_backdrop {
8568
8568
  outline: none;
8569
8569
  }
8570
8570
  `;
8571
- import.meta.css = [css$5, "@jsenv/dom/src/interaction/drag/drag_gesture.js"];
8571
+ import.meta.css = [css$6, "@jsenv/dom/src/interaction/drag/drag_gesture.js"];
8572
8572
 
8573
8573
  /*
8574
8574
  * Who asked for the capture of a pointer, last. This module is the only place
@@ -9632,7 +9632,7 @@ installImportMetaCssBuild(import.meta);
9632
9632
  lets the page scroll and still makes the refusal effective — provided the
9633
9633
  listener that will refuse is already known too, which is markDragSource's
9634
9634
  half of the same rule. */
9635
- const css$4 = /* css */ `[data-drag-handle], [data-drag-source] {
9635
+ const css$5 = /* css */ `[data-drag-handle], [data-drag-source] {
9636
9636
  -webkit-touch-callout: none;
9637
9637
  }
9638
9638
 
@@ -9662,7 +9662,7 @@ const css$4 = /* css */ `[data-drag-handle], [data-drag-source] {
9662
9662
  touch-action: auto;
9663
9663
  }
9664
9664
  `;
9665
- import.meta.css = [css$4, "@jsenv/dom/src/interaction/drag/drag_after_intent.js"];
9665
+ import.meta.css = [css$5, "@jsenv/dom/src/interaction/drag/drag_after_intent.js"];
9666
9666
 
9667
9667
  /*
9668
9668
  * A press that may become a drag has to be refusable before anyone knows it is
@@ -10407,7 +10407,7 @@ const getDragCoordinates = (
10407
10407
 
10408
10408
  installImportMetaCssBuild(import.meta);
10409
10409
 
10410
- const css$3 = /* css */ `.navi_constraint_feedback_line {
10410
+ const css$4 = /* css */ `.navi_constraint_feedback_line {
10411
10411
  z-index: 9998;
10412
10412
  visibility: hidden;
10413
10413
  transform-origin: 0;
@@ -10423,7 +10423,7 @@ const css$3 = /* css */ `.navi_constraint_feedback_line {
10423
10423
  `;
10424
10424
 
10425
10425
  const setupConstraintFeedbackLine = () => {
10426
- import.meta.css = [css$3, "@jsenv/dom/src/interaction/drag/constraint_feedback_line.js"];
10426
+ import.meta.css = [css$4, "@jsenv/dom/src/interaction/drag/constraint_feedback_line.js"];
10427
10427
  const constraintFeedbackLine = createConstraintFeedbackLine();
10428
10428
 
10429
10429
  // Track last known mouse position for constraint feedback line during scroll
@@ -10510,7 +10510,7 @@ let currentConstraintMarkers = [];
10510
10510
  let currentReferenceElementMarker = null;
10511
10511
  let currentElementMarker = null;
10512
10512
 
10513
- const css$2 = /* css */ `.navi_debug_markers_container {
10513
+ const css$3 = /* css */ `.navi_debug_markers_container {
10514
10514
  z-index: 999998;
10515
10515
  pointer-events: none;
10516
10516
  --marker-size: ${MARKER_SIZE}px;
@@ -10679,7 +10679,7 @@ const css$2 = /* css */ `.navi_debug_markers_container {
10679
10679
  `;
10680
10680
 
10681
10681
  const setupDragDebugMarkers = (dragGesture, { referenceElement }) => {
10682
- import.meta.css = [css$2, "@jsenv/dom/src/interaction/drag/drag_debug_markers.js"];
10682
+ import.meta.css = [css$3, "@jsenv/dom/src/interaction/drag/drag_debug_markers.js"];
10683
10683
 
10684
10684
  // Clean up any existing persistent markers from previous drag gestures
10685
10685
  {
@@ -12013,7 +12013,7 @@ const TOSS_DURATION_MS = 320;
12013
12013
  // Far enough to be off any screen, in the direction the hand was going.
12014
12014
  const TOSS_DISTANCE = 900;
12015
12015
 
12016
- const css$1 = /* css */ `.navi_drop_hint {
12016
+ const css$2 = /* css */ `.navi_drop_hint {
12017
12017
  inset: auto;
12018
12018
  top: var(--drop-hint-y);
12019
12019
  left: calc(var(--drop-target-left) + var(--drop-hint-margin-x, 0px));
@@ -12162,7 +12162,7 @@ const css$1 = /* css */ `.navi_drop_hint {
12162
12162
  `;
12163
12163
  // At module scope, not inside startDragTo: the cursor rules above say who
12164
12164
  // can start a drag, and they have to be true BEFORE anyone drags anything.
12165
- import.meta.css = [css$1, "@jsenv/dom/src/interaction/drag/drag_to.js"];
12165
+ import.meta.css = [css$2, "@jsenv/dom/src/interaction/drag/drag_to.js"];
12166
12166
 
12167
12167
  // What a press must not be read from at all. `data-drag-ignore` is said by
12168
12168
  // something whose press is its own business — a text one wants to select, a
@@ -12578,16 +12578,33 @@ const createDragToMoveGestureController = ({
12578
12578
  * Starts a drag, for one or more of the outcomes listed.
12579
12579
  *
12580
12580
  * @param {PointerEvent} event The `pointerdown` that may become a drag.
12581
- * @param {("move"|"reorder"|"toss"|"land")[]} effects
12581
+ * @param {("move"|"reorder"|"toss"|"land"|"leave")[]} effects
12582
12582
  * What letting go of this element can mean. `reorder`, `toss` and `land` carry a
12583
- * copy; `move` carries the element itself. Asking for `move` and `reorder`
12584
- * together is asking one release to mean two things, and so is asking for
12585
- * `reorder` and `land`.
12583
+ * copy; `move` carries the element itself, and `leave` goes with either. Asking
12584
+ * for `move` and `reorder` together is asking one release to mean two things,
12585
+ * and so is asking for `reorder` and `land`.
12586
12586
  * @param {object} [options]
12587
12587
  * @param {Element} [options.draggedElement=event.currentTarget]
12588
12588
  * @param {(detail: {gestureInfo: object, x: number, y: number}) => Promise|void} [options.onMove]
12589
- * It was put somewhere. The position is already committed when this runs — the
12590
- * hand let go of it there — and travels back if the promise rejects.
12589
+ * It was put somewhere. It is left where the hand let go of it while this
12590
+ * runs, and travels back if the promise rejects. Once it resolves the position
12591
+ * has one owner: the caller's layout, when the caller drew the element
12592
+ * somewhere while answering (a new `left`/`top` from state, a new `transform`,
12593
+ * a node rebuilt), and the element's own translate otherwise, baked in — see
12594
+ * settleMovedElement.
12595
+ * @param {(detail: {gestureInfo: object, x: number, y: number}) => Promise|void} [options.onLeave]
12596
+ * It was let go of away from every place: with places (`itemSelector`), away
12597
+ * from all of them; without, out of `outsideOf`. Beside `move` the element
12598
+ * itself is left where the hand put it while this runs, and the answer says
12599
+ * what becomes of that position — let go of on a resolve (the caller has
12600
+ * removed the thing, or drawn it where it goes back to), travelled home on a
12601
+ * reject. Beside a copy, the copy fades where it was let go of and comes back
12602
+ * if the promise rejects. Picked up and put straight back down is a cancel:
12603
+ * it has to have gone somewhere to be away from anything.
12604
+ * @param {Element} [options.outsideOf] The box a `leave` is outside of, when
12605
+ * nothing is a place. Left out, what can be seen of the scroll container.
12606
+ * Judged on the carried box no longer overlapping it — not on the pointer,
12607
+ * which is still well inside the frame when a small thing has just left it.
12591
12608
  * @param {Element} [options.containerElement=draggedElement.parentElement]
12592
12609
  * Where the places are looked for with `itemSelector`, and where the drop hint
12593
12610
  * is drawn. The parent covers a list and a board, whose items are siblings of
@@ -12616,16 +12633,8 @@ const createDragToMoveGestureController = ({
12616
12633
  * @param {number} [options.tossDistance=110] How far a throw goes, in px.
12617
12634
  * @param {number} [options.tossSpeed=0.45] And how fast, in px/ms. BOTH are asked
12618
12635
  * for: one without the other is moving the thing while hesitating, and nothing is
12619
- * thrown away on a hesitation.
12620
- * @param {("throw"|"release-outside")[]} [options.tossBy=["throw"]] How a toss
12621
- * is made here. A THROW is far and fast — the flick that gets rid of something,
12622
- * a list's gesture, judged before any landing. A RELEASE OUTSIDE has no place
12623
- * under it — dragging something off the surface it belongs to and letting go,
12624
- * deliberate and never a flick, a surface's gesture. Each is asked for on its
12625
- * own: a surface wants only the second (a fast drag across it that ends ON it
12626
- * has not asked for the thing to go), a list only the first (a row let go of
12627
- * beside its list is a row put back). With no place to be outside of (no
12628
- * `itemSelector`), every release is outside.
12636
+ * thrown away on a hesitation. A throw is judged before any landing; a release
12637
+ * with no place under it is `leave`'s, not a toss — there is no speed to it.
12629
12638
  *
12630
12639
  * Everything else is forwarded to `createDragToMoveGestureController`
12631
12640
  * (`areaConstraint`, `autoScrollAreaPadding`, `direction`…) and to `dragAfterIntent`
@@ -12655,32 +12664,48 @@ const startDragTo = (
12655
12664
  if (!isPrimaryButtonEvent(event)) {
12656
12665
  return undefined;
12657
12666
  }
12667
+ const canMove = effects.includes("move");
12658
12668
  const canReorder = effects.includes("reorder");
12659
12669
  const canToss = effects.includes("toss");
12660
12670
  const canLand = effects.includes("land");
12661
- if (canReorder || canToss || canLand) {
12671
+ const canLeave = effects.includes("leave");
12672
+ // A `leave` on its own carries a copy too: nothing keeps the element where it
12673
+ // was put, so the original stays until the answer says it is gone.
12674
+ if (canReorder || canToss || canLand || (canLeave && !canMove)) {
12662
12675
  return startDragToCarryCopy(event, {
12663
12676
  draggedElement,
12664
12677
  canReorder,
12665
12678
  canToss,
12666
12679
  canLand,
12680
+ canLeave,
12667
12681
  ...options,
12668
12682
  });
12669
12683
  }
12670
- return startDragToMoveElement(event, { draggedElement, ...options });
12684
+ return startDragToMoveElement(event, {
12685
+ draggedElement,
12686
+ canLeave,
12687
+ ...options,
12688
+ });
12671
12689
  };
12672
12690
 
12673
12691
  /**
12674
- * The element ITSELF is carried, and keeps the place the hand gave it.
12692
+ * The element ITSELF is carried, and keeps the place the hand gave it — or is let
12693
+ * go of away from it, when it can `leave`.
12675
12694
  *
12676
- * No copy, unlike the two others: what is being moved is the thing and not a
12695
+ * No copy, unlike the others: what is being moved is the thing and not a
12677
12696
  * stand-in for it, so there is nothing to put back and nothing to reveal.
12678
12697
  */
12679
12698
  const startDragToMoveElement = (
12680
12699
  event,
12681
12700
  {
12682
12701
  draggedElement,
12702
+ canLeave,
12683
12703
  onMove,
12704
+ onLeave,
12705
+ outsideOf,
12706
+ // A thing that can be let go of outside has to be able to get there. Same
12707
+ // reason and same shape as the default of the copy path below.
12708
+ areaConstraint = canLeave ? "none" : undefined,
12684
12709
  threshold,
12685
12710
  longPress,
12686
12711
  longPressDelay,
@@ -12697,6 +12722,7 @@ const startDragToMoveElement = (
12697
12722
  () => {
12698
12723
  const gestureController = createDragToMoveGestureController({
12699
12724
  releasePositionEffect: "manual",
12725
+ areaConstraint,
12700
12726
  ...options,
12701
12727
  });
12702
12728
  const dragGesture = gestureController.grabViaPointer(event, {
@@ -12712,15 +12738,35 @@ const startDragToMoveElement = (
12712
12738
  gestureInfo.cancelPosition();
12713
12739
  return;
12714
12740
  }
12715
- // Committed before the answer rather than after: the hand let go of it
12716
- // there, and a thing that snaps home while a request is in flight says the
12717
- // gesture was not understood.
12718
- gestureInfo.commitPosition();
12741
+ if (
12742
+ canLeave &&
12743
+ !gestureInfo.cancelled &&
12744
+ isOutsideOf(gestureInfo, outsideOf)
12745
+ ) {
12746
+ // Left where the hand let go of it while the answer is asked — a thing
12747
+ // that snaps home with the request in flight says the gesture was not
12748
+ // understood. The answer then says what becomes of that position: let
12749
+ // go of on a resolve, since the caller has removed the thing or drawn
12750
+ // it where it goes back to; travelled home on a reject.
12751
+ try {
12752
+ await onLeave?.({ gestureInfo, x: xDelta, y: yDelta });
12753
+ gestureInfo.cancelPosition();
12754
+ } catch {
12755
+ gestureInfo.cancelPositionAnimated();
12756
+ }
12757
+ return;
12758
+ }
12759
+ // Kept where the hand let go of it while the answer is asked — a thing
12760
+ // that snaps home with the request in flight says the gesture was not
12761
+ // understood — and settled once the answer is in.
12762
+ const layoutBeforeAnswer = readOwnLayout(draggedElement);
12719
12763
  try {
12720
12764
  await onMove?.({ gestureInfo, x: xDelta, y: yDelta });
12721
12765
  } catch {
12722
12766
  gestureInfo.cancelPositionAnimated();
12767
+ return;
12723
12768
  }
12769
+ settleMovedElement(draggedElement, gestureInfo, layoutBeforeAnswer);
12724
12770
  });
12725
12771
  return dragGesture;
12726
12772
  },
@@ -12736,22 +12782,64 @@ const startDragToMoveElement = (
12736
12782
  );
12737
12783
  };
12738
12784
 
12785
+ /**
12786
+ * Who holds the position of a moved element once the answer is in. Two owners
12787
+ * are possible and the element can only have one: the drag's own translate, for
12788
+ * a thing whose place is the hand's alone (a token on a free canvas), or the
12789
+ * caller's layout, for a thing drawn from state (a court at so many metres from
12790
+ * the place point, its `left`/`top` computed from that). Both kept, the thing
12791
+ * lands twice as far as the hand went.
12792
+ *
12793
+ * The answer does not say which, so the element is asked: drawn somewhere else
12794
+ * by the caller while the answer was given — a new `left`, a new inline
12795
+ * `transform`, a node thrown away and rebuilt — its layout holds the position
12796
+ * and the translate goes. Drawn nowhere, the translate is all there is, and it
12797
+ * is baked in. Read from the layout and never from the screen, so a page that
12798
+ * scrolled while the answer was awaited reads as nothing; and read from the
12799
+ * inline style rather than the computed one, which the translate itself is
12800
+ * part of.
12801
+ *
12802
+ * A caller that draws later than it answers (a store rendering on its own
12803
+ * schedule) has to make the answer wait for the draw: what is read here is what
12804
+ * has happened by then.
12805
+ */
12806
+ const settleMovedElement = (element, gestureInfo, layoutBeforeAnswer) => {
12807
+ const layoutAfterAnswer = readOwnLayout(element);
12808
+ const drawnElsewhere =
12809
+ !layoutAfterAnswer.connected ||
12810
+ layoutAfterAnswer.offsetLeft !== layoutBeforeAnswer.offsetLeft ||
12811
+ layoutAfterAnswer.offsetTop !== layoutBeforeAnswer.offsetTop ||
12812
+ layoutAfterAnswer.transform !== layoutBeforeAnswer.transform ||
12813
+ layoutAfterAnswer.translate !== layoutBeforeAnswer.translate;
12814
+ if (drawnElsewhere) {
12815
+ gestureInfo.cancelPosition();
12816
+ } else {
12817
+ gestureInfo.commitPosition();
12818
+ }
12819
+ };
12820
+ const readOwnLayout = (element) => ({
12821
+ connected: element.isConnected,
12822
+ offsetLeft: element.offsetLeft,
12823
+ offsetTop: element.offsetTop,
12824
+ transform: element.style.transform,
12825
+ translate: element.style.translate,
12826
+ });
12827
+
12739
12828
  // Far and fast, both at once: one without the other is moving the thing while
12740
12829
  // hesitating, and nothing is thrown away on a hesitation — it comes back.
12741
12830
  const TOSS_DISTANCE_TO_COMMIT = 110;
12742
12831
  const TOSS_SPEED_TO_COMMIT = 0.45;
12743
- // The flick is how a list gets rid of a row; a surface says otherwise.
12744
- const TOSS_BY_DEFAULT = ["throw"];
12745
12832
 
12746
12833
  const resolveDropMeaning = ({
12747
12834
  gestureInfo,
12748
12835
  hasDropTarget,
12836
+ releasedOutside,
12749
12837
  canReorder,
12750
12838
  canToss,
12751
12839
  canLand,
12840
+ canLeave,
12752
12841
  tossDistance = TOSS_DISTANCE_TO_COMMIT,
12753
12842
  tossSpeed = TOSS_SPEED_TO_COMMIT,
12754
- tossBy = TOSS_BY_DEFAULT,
12755
12843
  }) => {
12756
12844
  if (gestureInfo.cancelled) {
12757
12845
  // Nobody let go of anything: the gesture was taken away mid-air (the
@@ -12759,7 +12847,7 @@ const resolveDropMeaning = ({
12759
12847
  // to be at that moment is not a place it was put.
12760
12848
  return "cancel";
12761
12849
  }
12762
- if (canToss && tossBy.includes("throw")) {
12850
+ if (canToss) {
12763
12851
  const { xDelta, yDelta } = gestureInfo.layout;
12764
12852
  const distance = Math.hypot(xDelta, yDelta);
12765
12853
  if (distance > tossDistance && gestureInfo.velocity > tossSpeed) {
@@ -12774,19 +12862,14 @@ const resolveDropMeaning = ({
12774
12862
  return "reorder";
12775
12863
  }
12776
12864
  }
12777
- if (canToss && tossBy.includes("release-outside")) {
12778
- // Let go of away from every place: the other way of meaning "get rid of
12779
- // this", and the one a surface asks for — off the plan and down is
12780
- // deliberate, never a flick.
12781
- //
12782
- // "Away from every place" is the last frame's answer, not the one above: a
12783
- // row let go of where it already was has no place to INSERT it at and still
12784
- // has a place under it. And it has to have gone somewhere to be away from
12785
- // anything — picked up and put straight back down is a hand that changed its
12786
- // mind, not a thing dropped over nothing.
12865
+ if (canLeave && releasedOutside) {
12866
+ // Let go of away from every place — off the plan and down, deliberate and
12867
+ // never a flick. It has to have gone somewhere to be away from anything:
12868
+ // picked up and put straight back down is a hand that changed its mind, not
12869
+ // a thing dropped over nothing.
12787
12870
  const { xDelta, yDelta } = gestureInfo.layout;
12788
- if ((xDelta || yDelta) && !gestureInfo.dropTargetInfo) {
12789
- return "toss";
12871
+ if (xDelta || yDelta) {
12872
+ return "leave";
12790
12873
  }
12791
12874
  }
12792
12875
  return "cancel";
@@ -12807,28 +12890,34 @@ const startDragToCarryCopy = (
12807
12890
  canReorder,
12808
12891
  canToss,
12809
12892
  canLand,
12810
- // Something that can be thrown away has to be able to LEAVE. The default of
12811
- // the layer below keeps what is dragged inside its scroll area, which is right
12812
- // for a reorder (a row belongs to its list) and makes a throw impossible — the
12813
- // copy hits the edge of the list and no distance is ever covered, so no throw
12814
- // ever happens and no sideways movement is even visible.
12893
+ canLeave,
12894
+ // Something that can be thrown away, or let go of outside, has to be able to
12895
+ // LEAVE. The default of the layer below keeps what is dragged inside its
12896
+ // scroll area, which is right for a reorder (a row belongs to its list) and
12897
+ // makes a throw impossible — the copy hits the edge of the list and no
12898
+ // distance is ever covered, so no throw ever happens and no sideways movement
12899
+ // is even visible.
12815
12900
  // Destructured with the default here rather than written at the call below: a
12816
12901
  // caller passing `areaConstraint: undefined` (which is what saying nothing
12817
12902
  // through an options object looks like) would otherwise put the layer below
12818
12903
  // back on its own default and undo this.
12819
- areaConstraint = canToss ? "none" : undefined,
12904
+ areaConstraint = canToss || canLeave ? "none" : undefined,
12820
12905
  containerElement = draggedElement.parentElement,
12821
12906
  itemSelector,
12822
12907
  getItemId,
12823
12908
  onReorder,
12824
12909
  onLand,
12825
12910
  onToss,
12911
+ onLeave,
12912
+ outsideOf,
12826
12913
  tossDistance,
12827
12914
  tossSpeed,
12828
- tossBy,
12829
12915
  // A list runs one way and reordering walks it; a board has places all around,
12830
- // so something landing on one of them goes wherever the hand takes it.
12831
- direction = canLand ? { x: true, y: true } : { x: false, y: true },
12916
+ // so something landing on one of them goes wherever the hand takes it — and
12917
+ // so does something that can leave, whichever edge it leaves by.
12918
+ direction = canLand || canLeave
12919
+ ? { x: true, y: true }
12920
+ : { x: false, y: true },
12832
12921
  threshold,
12833
12922
  longPress,
12834
12923
  longPressDelay,
@@ -13059,15 +13148,25 @@ const startDragToCarryCopy = (
13059
13148
  const hasDropTarget = canLand
13060
13149
  ? currentReleaseElement !== undefined
13061
13150
  : currentBeforeElement !== undefined;
13151
+ // "Away from every place" is the last frame's answer, not
13152
+ // `hasDropTarget`: a row let go of where it already was has no place
13153
+ // to INSERT it at and still has a place under it. With nothing that is
13154
+ // a place, it is out of the box the thing belongs to.
13155
+ const releasedOutside =
13156
+ canLeave &&
13157
+ (dropHintEl
13158
+ ? !gestureInfo.dropTargetInfo
13159
+ : isOutsideOf(gestureInfo, outsideOf));
13062
13160
  const dropMeans = resolveDropMeaning({
13063
13161
  gestureInfo,
13064
13162
  hasDropTarget,
13163
+ releasedOutside,
13065
13164
  canReorder,
13066
13165
  canToss,
13067
13166
  canLand,
13167
+ canLeave,
13068
13168
  tossDistance,
13069
13169
  tossSpeed,
13070
- tossBy,
13071
13170
  });
13072
13171
 
13073
13172
  // Let go of and still on the screen: from here until it is taken away
@@ -13115,6 +13214,13 @@ const startDragToCarryCopy = (
13115
13214
  // place.
13116
13215
  await settleCloneBack(cloneWrapper, draggedElement);
13117
13216
  }
13217
+ } else if (dropMeans === "leave") {
13218
+ setCloneViewportRect(cloneWrapper, cloneWrapper);
13219
+ gestureInfo.cancelPosition();
13220
+ const gone = await letCloneGo(cloneWrapper, gestureInfo, onLeave);
13221
+ if (!gone) {
13222
+ await settleCloneBack(cloneWrapper, draggedElement);
13223
+ }
13118
13224
  } else if (dropMeans === "land") {
13119
13225
  // Read before the copy is put down: landing bakes its position and
13120
13226
  // drops the transform, and where it came down is where the hand let
@@ -13279,6 +13385,54 @@ const tossCloneAway = async (cloneWrapper, gestureInfo, onToss) => {
13279
13385
  }
13280
13386
  };
13281
13387
 
13388
+ /**
13389
+ * The copy fades where the hand let go of it, and the caller says what being let
13390
+ * go of there meant. Resolves true when it is really gone.
13391
+ *
13392
+ * No flight, unlike a throw: nothing was thrown. The thing was put down over
13393
+ * nothing, and it goes from there — the same attribute as a throw, so that a
13394
+ * refusal brings it back the same way.
13395
+ */
13396
+ const letCloneGo = async (cloneWrapper, gestureInfo, onLeave) => {
13397
+ cloneWrapper.dataset.tossed = "away";
13398
+ try {
13399
+ const { xDelta, yDelta } = gestureInfo.layout;
13400
+ await onLeave?.({ gestureInfo, x: xDelta, y: yDelta });
13401
+ return true;
13402
+ } catch {
13403
+ return false;
13404
+ }
13405
+ };
13406
+
13407
+ // Whether what is carried has left the box entirely. The box rather than the
13408
+ // pointer: a small thing dragged past the edge of its frame has left it while
13409
+ // the hand is still well inside.
13410
+ const isOutsideOf = (gestureInfo, outsideOf) => {
13411
+ const carried = gestureInfo.elementImpacted || gestureInfo.element;
13412
+ const carriedRect = carried.getBoundingClientRect();
13413
+ const frameRect = outsideOf
13414
+ ? outsideOf.getBoundingClientRect()
13415
+ : getVisibleRect(gestureInfo.scrollContainer);
13416
+ return !rectangleAreIntersecting(carriedRect, frameRect);
13417
+ };
13418
+
13419
+ // What can be seen of a scroll container, in viewport coordinates.
13420
+ const getVisibleRect = (scrollContainer) => {
13421
+ if (scrollContainer === document.documentElement) {
13422
+ const { clientWidth, clientHeight } = scrollContainer;
13423
+ return { left: 0, top: 0, right: clientWidth, bottom: clientHeight };
13424
+ }
13425
+ const rect = scrollContainer.getBoundingClientRect();
13426
+ const left = rect.left + scrollContainer.clientLeft;
13427
+ const top = rect.top + scrollContainer.clientTop;
13428
+ return {
13429
+ left,
13430
+ top,
13431
+ right: left + scrollContainer.clientWidth,
13432
+ bottom: top + scrollContainer.clientHeight,
13433
+ };
13434
+ };
13435
+
13282
13436
  /**
13283
13437
  * It comes back where it came from, and only then is taken away — which is what
13284
13438
  * makes the original reappear in place rather than blink back into it.
@@ -14690,6 +14844,253 @@ const isPressDisputedByDrag = (element) => {
14690
14844
  return !isPressExcluded(element, dragged);
14691
14845
  };
14692
14846
 
14847
+ installImportMetaCssBuild(import.meta);
14848
+
14849
+ const SURFACE_ATTRIBUTE = "data-pan-zoom-surface";
14850
+
14851
+ const css$1 = /* css */ `[data-pan-zoom-surface] {
14852
+ touch-action: none;
14853
+ user-select: none;
14854
+ }
14855
+ `;
14856
+ import.meta.css = [css$1, "@jsenv/dom/src/interaction/drag/pan_zoom.js"];
14857
+
14858
+ // How far a wheel travels to double the zoom, or halve it: about three notches
14859
+ // of a mouse. A trackpad pinch arrives as a wheel too (ctrl held, small deltas,
14860
+ // many events) and reads the same way.
14861
+ const WHEEL_DISTANCE_PER_DOUBLING = 300;
14862
+ const WHEEL_LINE_HEIGHT = 16;
14863
+ const WHEEL_PAGE_HEIGHT = 400;
14864
+
14865
+ // What a press on the surface is NOT for it: what answers the pointer on its own
14866
+ // (a field, a handle, a popover…), what is carried across the surface (a drag
14867
+ // source, a thing that said the press is its own), and a surface inside this
14868
+ // one. The nearest word wins: the surface is in the list too, so a press on it
14869
+ // or on plain content in it finds the surface first.
14870
+ const YIELDED_SELECTOR = `${DRAG_EXCLUDED_SELECTOR},[data-drag-source],[data-drag-ignore],[${SURFACE_ATTRIBUTE}]`;
14871
+
14872
+ /**
14873
+ * Makes an element a surface that pans under the hand and zooms between two
14874
+ * fingers or under a wheel.
14875
+ *
14876
+ * @param {Element} element
14877
+ * @param {object} options
14878
+ * @param {(detail: {event: PointerEvent, x: number, y: number}) => void} [options.onPan]
14879
+ * The hand moved: `x`/`y` are how far since the last report, in px.
14880
+ * @param {(detail: {event: PointerEvent|WheelEvent, factor: number, x: number, y: number}) => void} [options.onZoom]
14881
+ * The zoom changed by `factor` (above 1 is in) around the point `x`/`y` of the
14882
+ * surface, measured inside its border. Left out, a wheel over the surface is
14883
+ * left to the page, and two fingers only pan.
14884
+ * @param {number} [options.threshold=5] How far a pointer travels before it pans.
14885
+ * @returns {() => void} Takes it all back.
14886
+ */
14887
+ const installPanZoom = (
14888
+ element,
14889
+ { onPan, onZoom, threshold = 5 } = {},
14890
+ ) => {
14891
+ element.setAttribute(SURFACE_ATTRIBUTE, "");
14892
+ // A travelling box above must not take the press this reads (see
14893
+ // drag_to_travel.js): the surface says so itself, being the one that knows.
14894
+ element.setAttribute("data-no-drag-travel", "");
14895
+
14896
+ // Every pointer down on the surface, where it is and where it landed.
14897
+ const pointers = new Map();
14898
+ let active = false;
14899
+ // Where the hand was at the last report: the point between the pointers, and
14900
+ // the distance between the first two.
14901
+ let anchor = null;
14902
+ let disarmClickSuppression = null;
14903
+
14904
+ const pointOnSurface = (clientX, clientY) => {
14905
+ const rect = element.getBoundingClientRect();
14906
+ return {
14907
+ x: clientX - rect.left - element.clientLeft,
14908
+ y: clientY - rect.top - element.clientTop,
14909
+ };
14910
+ };
14911
+
14912
+ const readHand = (where = "now") => {
14913
+ let sumX = 0;
14914
+ let sumY = 0;
14915
+ for (const pointer of pointers.values()) {
14916
+ sumX += where === "now" ? pointer.x : pointer.startX;
14917
+ sumY += where === "now" ? pointer.y : pointer.startY;
14918
+ }
14919
+ const count = pointers.size;
14920
+ const hand = { x: sumX / count, y: sumY / count, distance: 0 };
14921
+ if (count >= 2) {
14922
+ const [first, second] = pointers.values();
14923
+ hand.distance =
14924
+ where === "now"
14925
+ ? Math.hypot(second.x - first.x, second.y - first.y)
14926
+ : Math.hypot(
14927
+ second.startX - first.startX,
14928
+ second.startY - first.startY,
14929
+ );
14930
+ }
14931
+ return hand;
14932
+ };
14933
+
14934
+ const activate = (anchorWhere) => {
14935
+ active = true;
14936
+ for (const pointerId of pointers.keys()) {
14937
+ element.setPointerCapture(pointerId);
14938
+ }
14939
+ anchor = readHand(anchorWhere);
14940
+ // The click the release leaves behind is not for what is under the hand.
14941
+ disarmClickSuppression = suppressClickAfterGesture();
14942
+ };
14943
+
14944
+ const report = (event) => {
14945
+ const hand = readHand();
14946
+ if (onZoom && anchor.distance && hand.distance) {
14947
+ const factor = hand.distance / anchor.distance;
14948
+ if (factor !== 1) {
14949
+ onZoom({ event, factor, ...pointOnSurface(anchor.x, anchor.y) });
14950
+ }
14951
+ }
14952
+ const x = hand.x - anchor.x;
14953
+ const y = hand.y - anchor.y;
14954
+ if (onPan && (x || y)) {
14955
+ onPan({ event, x, y });
14956
+ }
14957
+ anchor = hand;
14958
+ };
14959
+
14960
+ const end = () => {
14961
+ window.removeEventListener("pointermove", onPointerMove, true);
14962
+ window.removeEventListener("pointerup", onPointerEnd, true);
14963
+ window.removeEventListener("pointercancel", onPointerEnd, true);
14964
+ if (active) {
14965
+ active = false;
14966
+ anchor = null;
14967
+ disarmClickSuppression();
14968
+ disarmClickSuppression = null;
14969
+ }
14970
+ };
14971
+
14972
+ const onPointerDown = (event) => {
14973
+ // A secondary button (right click and friends) is a context menu.
14974
+ if (!isPrimaryButtonEvent(event)) {
14975
+ return;
14976
+ }
14977
+ const yieldedTo = event.target.closest(YIELDED_SELECTOR);
14978
+ if (yieldedTo && yieldedTo !== element && element.contains(yieldedTo)) {
14979
+ return;
14980
+ }
14981
+ if (pointers.size === 0) {
14982
+ // On the window rather than on the surface, filtered by id: nothing is
14983
+ // captured until the travel proves the intent, and a pointer that leaves
14984
+ // the surface meanwhile must still be heard.
14985
+ window.addEventListener("pointermove", onPointerMove, true);
14986
+ window.addEventListener("pointerup", onPointerEnd, true);
14987
+ window.addEventListener("pointercancel", onPointerEnd, true);
14988
+ }
14989
+ pointers.set(event.pointerId, {
14990
+ x: event.clientX,
14991
+ y: event.clientY,
14992
+ startX: event.clientX,
14993
+ startY: event.clientY,
14994
+ });
14995
+ if (active) {
14996
+ element.setPointerCapture(event.pointerId);
14997
+ anchor = readHand();
14998
+ return;
14999
+ }
15000
+ if (pointers.size >= 2) {
15001
+ activate("now");
15002
+ }
15003
+ };
15004
+
15005
+ const onPointerMove = (event) => {
15006
+ const pointer = pointers.get(event.pointerId);
15007
+ if (!pointer) {
15008
+ return;
15009
+ }
15010
+ pointer.x = event.clientX;
15011
+ pointer.y = event.clientY;
15012
+ if (!active) {
15013
+ const travelled = Math.hypot(
15014
+ pointer.x - pointer.startX,
15015
+ pointer.y - pointer.startY,
15016
+ );
15017
+ if (travelled < threshold) {
15018
+ return;
15019
+ }
15020
+ // Anchored where the hand LANDED: the pixels that proved the intent are
15021
+ // replayed by the first report, so the surface catches up with the finger
15022
+ // rather than starting from under it.
15023
+ activate("start");
15024
+ }
15025
+ report(event);
15026
+ };
15027
+
15028
+ const onPointerEnd = (event) => {
15029
+ if (!pointers.delete(event.pointerId)) {
15030
+ return;
15031
+ }
15032
+ if (pointers.size === 0) {
15033
+ end();
15034
+ return;
15035
+ }
15036
+ if (active) {
15037
+ anchor = readHand();
15038
+ }
15039
+ };
15040
+
15041
+ // A capture that goes while the pointer is still down is the browser dropping
15042
+ // it (or another gesture taking it): that pointer is over for this one. After
15043
+ // a pointerup it has already been let go of, and this says nothing.
15044
+ const onLostPointerCapture = (event) => {
15045
+ if (active) {
15046
+ onPointerEnd(event);
15047
+ }
15048
+ };
15049
+
15050
+ const onWheel = (event) => {
15051
+ // A burst somebody above is already answering (a row of slides travelling
15052
+ // under the wheel) is theirs; one that began here is held for as long as it
15053
+ // lasts, so drifting over the edge does not hand its tail to the page.
15054
+ if (wheelGestureIsTakenFrom(element)) {
15055
+ return;
15056
+ }
15057
+ claimWheelGesture(element);
15058
+ // Taken whole, whichever way it leans: the browser would scroll the page
15059
+ // with it, or on a laptop read a sideways swipe as "go back".
15060
+ event.preventDefault();
15061
+ const deltaY =
15062
+ event.deltaMode === 1
15063
+ ? event.deltaY * WHEEL_LINE_HEIGHT
15064
+ : event.deltaMode === 2
15065
+ ? event.deltaY * WHEEL_PAGE_HEIGHT
15066
+ : event.deltaY;
15067
+ if (!deltaY) {
15068
+ return;
15069
+ }
15070
+ onZoom({
15071
+ event,
15072
+ factor: 2 ** (-deltaY / WHEEL_DISTANCE_PER_DOUBLING),
15073
+ ...pointOnSurface(event.clientX, event.clientY),
15074
+ });
15075
+ };
15076
+
15077
+ element.addEventListener("pointerdown", onPointerDown);
15078
+ element.addEventListener("lostpointercapture", onLostPointerCapture);
15079
+ if (onZoom) {
15080
+ element.addEventListener("wheel", onWheel, { passive: false });
15081
+ }
15082
+
15083
+ return () => {
15084
+ end();
15085
+ pointers.clear();
15086
+ element.removeEventListener("pointerdown", onPointerDown);
15087
+ element.removeEventListener("lostpointercapture", onLostPointerCapture);
15088
+ element.removeEventListener("wheel", onWheel);
15089
+ element.removeAttribute(SURFACE_ATTRIBUTE);
15090
+ element.removeAttribute("data-no-drag-travel");
15091
+ };
15092
+ };
15093
+
14693
15094
  // Shared by navi's own use_displayed_layout_effect.js (rich "navi_displayed"
14694
15095
  // CustomEvent, open transitions only) and visible_rect.js (needs both
14695
15096
  // directions: hide when a container closes, recheck when it reopens) — the
@@ -20644,4 +21045,4 @@ const useResizeStatus = (elementRef, { as = "number" } = {}) => {
20644
21045
  };
20645
21046
  };
20646
21047
 
20647
- export { EASING, ELEMENT_SIZE_CHANGE, activeElementSignal, addActiveElementEffect, addAttributeEffect, allowWheelThrough, appendStyles, applyNewPosition, canScroll, captureScrollState, chainEvent, claimWheelGesture, clickIsSuppressed, closestOpenableAncestor, contrastColor, createBackgroundColorTransition, createBackgroundTransition, createBorderRadiusTransition, createBorderTransition, createDragGestureController, createDragToMoveGestureController, createEventGroupLogger, createGroupTransitionController, createHeightTransition, createIterableWeakSet, createOpacityTransition, createPubSub, createStyleController, createTimelineTransition, createTransition, createTranslateXTransition, createValueEffect, createWidthTransition, cubicBezier, dispatchCustomEvent, dispatchInternalCustomEvent, dispatchPublicCustomEvent, dragAfterIntent, elementIsFocusable, elementIsVisibleForFocus, elementIsVisuallyVisible, findAfter, findAncestor, findBefore, findDescendant, findEvent, findFocusDelegateTarget, findFocusable, findSelfOrAncestorFixedPosition, formatEventSideEffect, getAncestorOpenType, getAvailableHeight, getAvailableWidth, getBackground, getBackgroundColor, getBorder, getBorderRadius, getBorderSizes, getContrastRatio, getDefaultStyles, getDragCoordinates, getDropTargetInfo, getElementSignature, getFirstVisuallyVisibleAncestor, getFocusVisibilityInfo, getHeight, getHeightWithoutTransition, getInnerHeight, getInnerWidth, getKeyboardEventDefaultAction, getLuminance, getMarginSizes, getMaxHeight, getMaxWidth, getMinHeight, getMinWidth, getOpacity, getOpacityWithoutTransition, getPaddingSizes, getPositionedParent, getPositioningScrollOffset, getPreferedColorScheme, getScrollBox, getScrollContainer, getScrollContainerSet, getScrollIntoViewScopedOffsets, getScrollRelativeRect, getSelfAndAncestorScrolls, getStyle, getTranslateX, getTranslateXWithoutTransition, getTranslateY, getVirtualKeyboardOverlayHeight, getVisuallyVisibleInfo, getWidth, getWidthWithoutTransition, hasCSSSizeUnit, initFlexDetailsSet, initFocusGroup, initPositionSticky, isAncestorOpen, isDisplayedDespiteClosedAncestor, isPressDisputedByDrag, isPrimaryButtonEvent, isSameColor, isScrollable, isTouchDrivenEvent, keepTouchRefusable, markDragSource, measureLongestVisualLineWidth, measureScrollbar, measureWidestChildRow, mergeOneStyle, mergeTwoStyles, normalizeKeyboardKey, normalizeStyle, normalizeStyles, observeAncestorOpenState, onAncestorReopen, parsePositionArea, parseStyle, performTabNavigation, pickPositionRelativeTo, prefersDarkColors, prefersLightColors, preventFocusNav, preventFocusNavViaKeyboard, preventIntermediateScrollbar, releaseWheelGesture, resolveCSSColor, resolveCSSSize, resolveColorLuminance, resolveOklchLightness, scrollIntoViewScoped, scrollIntoViewThroughScrollables, scrollIntoViewWithStickyAwareness, scrollRoomTowards, setAttribute, setAttributes, setPlacementViewportInsets, setStyles, setVirtualKeyboardOverlaysContent, snapToPixel, startDragTo, startDragToResizeGesture, startDragToTravel, stickyAsRelativeCoords, stringifyStyle, subscribeVirtualKeyboardGeometryChange, subscribeVisualViewportResizeSettled, subscribeWindowResizeSettled, suppressClickAfterGesture, trapFocusInside, trapScrollInside, useActiveElement, useAvailableHeight, useAvailableWidth, useMaxHeight, useMaxWidth, useResizeStatus, visibleRectEffect, waitForPressHeld, watchWheelTravel, wheelGestureIsTakenFrom };
21048
+ export { EASING, ELEMENT_SIZE_CHANGE, activeElementSignal, addActiveElementEffect, addAttributeEffect, allowWheelThrough, appendStyles, applyNewPosition, canScroll, captureScrollState, chainEvent, claimWheelGesture, clickIsSuppressed, closestOpenableAncestor, contrastColor, createBackgroundColorTransition, createBackgroundTransition, createBorderRadiusTransition, createBorderTransition, createDragGestureController, createDragToMoveGestureController, createEventGroupLogger, createGroupTransitionController, createHeightTransition, createIterableWeakSet, createOpacityTransition, createPubSub, createStyleController, createTimelineTransition, createTransition, createTranslateXTransition, createValueEffect, createWidthTransition, cubicBezier, dispatchCustomEvent, dispatchInternalCustomEvent, dispatchPublicCustomEvent, dragAfterIntent, elementIsFocusable, elementIsVisibleForFocus, elementIsVisuallyVisible, findAfter, findAncestor, findBefore, findDescendant, findEvent, findFocusDelegateTarget, findFocusable, findSelfOrAncestorFixedPosition, formatEventSideEffect, getAncestorOpenType, getAvailableHeight, getAvailableWidth, getBackground, getBackgroundColor, getBorder, getBorderRadius, getBorderSizes, getContrastRatio, getDefaultStyles, getDragCoordinates, getDropTargetInfo, getElementSignature, getFirstVisuallyVisibleAncestor, getFocusVisibilityInfo, getHeight, getHeightWithoutTransition, getInnerHeight, getInnerWidth, getKeyboardEventDefaultAction, getLuminance, getMarginSizes, getMaxHeight, getMaxWidth, getMinHeight, getMinWidth, getOpacity, getOpacityWithoutTransition, getPaddingSizes, getPositionedParent, getPositioningScrollOffset, getPreferedColorScheme, getScrollBox, getScrollContainer, getScrollContainerSet, getScrollIntoViewScopedOffsets, getScrollRelativeRect, getSelfAndAncestorScrolls, getStyle, getTranslateX, getTranslateXWithoutTransition, getTranslateY, getVirtualKeyboardOverlayHeight, getVisuallyVisibleInfo, getWidth, getWidthWithoutTransition, hasCSSSizeUnit, initFlexDetailsSet, initFocusGroup, initPositionSticky, installPanZoom, isAncestorOpen, isDisplayedDespiteClosedAncestor, isPressDisputedByDrag, isPrimaryButtonEvent, isSameColor, isScrollable, isTouchDrivenEvent, keepTouchRefusable, markDragSource, measureLongestVisualLineWidth, measureScrollbar, measureWidestChildRow, mergeOneStyle, mergeTwoStyles, normalizeKeyboardKey, normalizeStyle, normalizeStyles, observeAncestorOpenState, onAncestorReopen, parsePositionArea, parseStyle, performTabNavigation, pickPositionRelativeTo, prefersDarkColors, prefersLightColors, preventFocusNav, preventFocusNavViaKeyboard, preventIntermediateScrollbar, releaseWheelGesture, resolveCSSColor, resolveCSSSize, resolveColorLuminance, resolveOklchLightness, scrollIntoViewScoped, scrollIntoViewThroughScrollables, scrollIntoViewWithStickyAwareness, scrollRoomTowards, setAttribute, setAttributes, setPlacementViewportInsets, setStyles, setVirtualKeyboardOverlaysContent, snapToPixel, startDragTo, startDragToResizeGesture, startDragToTravel, stickyAsRelativeCoords, stringifyStyle, subscribeVirtualKeyboardGeometryChange, subscribeVisualViewportResizeSettled, subscribeWindowResizeSettled, suppressClickAfterGesture, trapFocusInside, trapScrollInside, useActiveElement, useAvailableHeight, useAvailableWidth, useMaxHeight, useMaxWidth, useResizeStatus, visibleRectEffect, waitForPressHeld, watchWheelTravel, wheelGestureIsTakenFrom };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/dom",
3
- "version": "0.17.46",
3
+ "version": "0.17.48",
4
4
  "type": "module",
5
5
  "description": "DOM utilities for writing frontend code",
6
6
  "repository": {
@@ -35,12 +35,16 @@
35
35
  "build": "node ./scripts/build.mjs",
36
36
  "prepublishOnly": "npm run build"
37
37
  },
38
+ "peerDependencies": {
39
+ "@preact/signals": "^2.11.2",
40
+ "preact": "^11.0.0-beta.2"
41
+ },
38
42
  "dependencies": {},
39
43
  "devDependencies": {
40
44
  "@jsenv/core": "../../../",
41
45
  "@jsenv/navi": "../navi",
42
46
  "@jsenv/snapshot": "../../tooling/snapshot",
43
- "@preact/signals": "2.11.1",
47
+ "@preact/signals": "2.11.2",
44
48
  "preact": "11.0.0-beta.2"
45
49
  },
46
50
  "publishConfig": {