@jsenv/dom 0.17.81 → 0.17.82

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 +209 -19
  2. package/package.json +1 -1
package/dist/jsenv_dom.js CHANGED
@@ -10183,6 +10183,10 @@ const dragSourceThatStoodDown = (pressEvent) => {
10183
10183
  * drag_gesture.js). `"manual"` leaves the press to the browser until the
10184
10184
  * caller's own gesture says otherwise. A long press waits without refusing
10185
10185
  * anything either way (see the stylesheet above).
10186
+ * @param {boolean} [options.handedOver=false]
10187
+ * The press was held on something else, which handed it to this drag (see
10188
+ * `handedOver` in drag_to.js): the hold that did so established the intent,
10189
+ * so the gesture starts at once, as it does from a dedicated handle.
10186
10190
  */
10187
10191
  const dragAfterIntent = (
10188
10192
  grabEvent,
@@ -10196,6 +10200,7 @@ const dragAfterIntent = (
10196
10200
  onPressCancel,
10197
10201
  onPress,
10198
10202
  selection = "auto",
10203
+ handedOver = false,
10199
10204
  } = {},
10200
10205
  ) => {
10201
10206
  if (!isPrimaryButtonEvent(grabEvent)) {
@@ -10204,7 +10209,7 @@ const dragAfterIntent = (
10204
10209
  const target = grabEvent.target;
10205
10210
  const isDedicatedHandle =
10206
10211
  target.closest && target.closest("[data-drag-handle]");
10207
- if (isDedicatedHandle) {
10212
+ if (isDedicatedHandle || handedOver) {
10208
10213
  startDragGesture(dragGestureInitializer);
10209
10214
  return;
10210
10215
  }
@@ -10836,6 +10841,82 @@ const getDragCoordinates = (
10836
10841
  return [leftRelativeToScrollContainer, topRelativeToScrollContainer];
10837
10842
  };
10838
10843
 
10844
+ /**
10845
+ * Which presses are still down, and where each one is now.
10846
+ *
10847
+ * A press is read by whoever it landed on, from its `pointerdown` to its
10848
+ * `pointerup`. Some are handed over halfway: a hold opens a popup, and
10849
+ * something inside it takes the press the finger is still making (see
10850
+ * `handedOver` in drag_to.js). Whoever receives it was not listening when it
10851
+ * began, so it cannot know whether the pointer has let go since, nor where it
10852
+ * is now — the `pointerdown` it is handed says where the finger WAS. Listened
10853
+ * for from the start instead, on the window in capture so that nothing lower
10854
+ * can hide a press from it.
10855
+ *
10856
+ * Keyed by the `pointerdown` itself and not by the pointer: the same finger
10857
+ * lifted and put down again is another press, and a press handed over late
10858
+ * must not be answered by the next one.
10859
+ */
10860
+
10861
+ const pressDownByPointerId = new Map();
10862
+
10863
+ /**
10864
+ * Where the pointer of `pressEvent` is now, while that press is still down.
10865
+ *
10866
+ * @param {PointerEvent} pressEvent A `pointerdown`.
10867
+ * @returns {{ clientX: number, clientY: number } | null} `null` once that press
10868
+ * has ended — let go of, or cancelled by the browser.
10869
+ */
10870
+ const readPressDown = (pressEvent) => {
10871
+ const pressDown = pressDownByPointerId.get(pressEvent.pointerId);
10872
+ if (!pressDown || pressDown.pressEvent !== pressEvent) {
10873
+ return null;
10874
+ }
10875
+ return { clientX: pressDown.clientX, clientY: pressDown.clientY };
10876
+ };
10877
+
10878
+ const onPointerMove = (pointerMoveEvent) => {
10879
+ const pressDown = pressDownByPointerId.get(pointerMoveEvent.pointerId);
10880
+ if (!pressDown) {
10881
+ return;
10882
+ }
10883
+ pressDown.clientX = pointerMoveEvent.clientX;
10884
+ pressDown.clientY = pointerMoveEvent.clientY;
10885
+ };
10886
+ const onPointerEnd = (pointerEndEvent) => {
10887
+ pressDownByPointerId.delete(pointerEndEvent.pointerId);
10888
+ if (pressDownByPointerId.size === 0) {
10889
+ // A mouse moves all the time without being pressed: nothing is read from
10890
+ // it between presses.
10891
+ window.removeEventListener("pointermove", onPointerMove, true);
10892
+ }
10893
+ };
10894
+ window.addEventListener(
10895
+ "pointerdown",
10896
+ (pointerDownEvent) => {
10897
+ if (pressDownByPointerId.size === 0) {
10898
+ window.addEventListener("pointermove", onPointerMove, {
10899
+ capture: true,
10900
+ passive: true,
10901
+ });
10902
+ }
10903
+ pressDownByPointerId.set(pointerDownEvent.pointerId, {
10904
+ pressEvent: pointerDownEvent,
10905
+ clientX: pointerDownEvent.clientX,
10906
+ clientY: pointerDownEvent.clientY,
10907
+ });
10908
+ },
10909
+ { capture: true, passive: true },
10910
+ );
10911
+ window.addEventListener("pointerup", onPointerEnd, {
10912
+ capture: true,
10913
+ passive: true,
10914
+ });
10915
+ window.addEventListener("pointercancel", onPointerEnd, {
10916
+ capture: true,
10917
+ passive: true,
10918
+ });
10919
+
10839
10920
  installImportMetaCssBuild(import.meta);
10840
10921
 
10841
10922
  const css$4 = /* css */ `.navi_constraint_feedback_line {
@@ -14324,6 +14405,16 @@ const css$1 = /* css */ `.navi_drop_hint {
14324
14405
  opacity: 0;
14325
14406
  }
14326
14407
 
14408
+ [navi-drag-clone-wrapper][data-reaching] {
14409
+ transition: translate .2s ease-out;
14410
+ }
14411
+
14412
+ @starting-style {
14413
+ [navi-drag-clone-wrapper][data-reaching] {
14414
+ translate: var(--clone-reach-from);
14415
+ }
14416
+ }
14417
+
14327
14418
  [navi-drag-clone] {
14328
14419
  box-shadow: var(--drag-clone-shadow, 0 12px 28px #00000038);
14329
14420
  transform: scale(var(--drag-clone-scale, 1.03));
@@ -14740,6 +14831,11 @@ const createDragToMoveGestureController = ({
14740
14831
  element,
14741
14832
  referenceElement,
14742
14833
  elementToMove,
14834
+ // How far from `element` what moves starts, when it does not start over it
14835
+ // (a copy brought to a hand that pressed elsewhere, see handedOver). The
14836
+ // layout is then the box the hand holds, so constraints and auto-scroll
14837
+ // read what is seen; the translate is unaffected, it counts from the grab.
14838
+ grabOffset,
14743
14839
  event,
14744
14840
  ...rest
14745
14841
  } = {}) => {
@@ -14752,8 +14848,12 @@ const createDragToMoveGestureController = ({
14752
14848
  const dragGesture = grab({
14753
14849
  element,
14754
14850
  scrollContainer,
14755
- layoutScrollableLeft: elementScrollableLeft,
14756
- layoutScrollableTop: elementScrollableTop,
14851
+ layoutScrollableLeft: grabOffset
14852
+ ? elementScrollableLeft + grabOffset.x
14853
+ : elementScrollableLeft,
14854
+ layoutScrollableTop: grabOffset
14855
+ ? elementScrollableTop + grabOffset.y
14856
+ : elementScrollableTop,
14757
14857
  event,
14758
14858
  ...rest,
14759
14859
  });
@@ -14847,6 +14947,23 @@ const createDragToMoveGestureController = ({
14847
14947
  * for: one without the other is moving the thing while hesitating, and nothing is
14848
14948
  * thrown away on a hesitation. A throw is judged before any landing; a release
14849
14949
  * with no place under it is `leave`'s, not a toss — there is no speed to it.
14950
+ * @param {boolean} [options.handedOver=false]
14951
+ * `event` is a press held on something ELSE, handed to this element: a hold
14952
+ * opened a popup, and what the popup holds is to be carried by the finger
14953
+ * still down (see HANDED OVER below). Nothing happens when that press has
14954
+ * ended in the meantime.
14955
+ *
14956
+ * HANDED OVER. The press did not land on the element, and the element may not
14957
+ * have existed when it did, so the three things a press on it would have said
14958
+ * are said otherwise. Where it landed is none of the element's business (no
14959
+ * opted-out area is looked for). Whether the hand means it was proved by the
14960
+ * hold that handed it over, so the carry starts at once. And where the hand is
14961
+ * has nothing to do with where the element stands, so the copy is brought to
14962
+ * the hand — centred on the finger, flying there from the element's box —
14963
+ * rather than following it from a distance. The pointer is taken on the
14964
+ * element, away from what the finger first touched. Only a copy can be brought
14965
+ * like that: `move` carries the element itself, which would leave its place
14966
+ * just by being handed a press, and is not started.
14850
14967
  *
14851
14968
  * Everything else is forwarded to `createDragToMoveGestureController`
14852
14969
  * (`areaConstraint`, `autoScrollAreaPadding`, `direction`…) and to `dragAfterIntent`
@@ -14868,8 +14985,9 @@ const startDragTo = (
14868
14985
  { draggedElement = event.currentTarget, ...options } = {},
14869
14986
  ) => {
14870
14987
  // An area that opted out of dragging (a text one wants to select, a control that
14871
- // owns the gesture, a callout): the press there is none of our business.
14872
- if (isPressIgnored(event.target, draggedElement)) {
14988
+ // owns the gesture, a callout): the press there is none of our business. A
14989
+ // press handed over landed outside the element by construction.
14990
+ if (!options.handedOver && isPressIgnored(event.target, draggedElement)) {
14873
14991
  return undefined;
14874
14992
  }
14875
14993
  // A secondary button (right click and friends) is a context menu, not a grab.
@@ -14896,6 +15014,9 @@ const startDragTo = (
14896
15014
  ...options,
14897
15015
  });
14898
15016
  }
15017
+ if (options.handedOver) {
15018
+ return undefined;
15019
+ }
14899
15020
  return startDragToMoveElement(event, {
14900
15021
  draggedElement,
14901
15022
  canMove,
@@ -15285,23 +15406,35 @@ const startDragToCarryCopy = (
15285
15406
  onPressStart,
15286
15407
  onPressCancel,
15287
15408
  onPress,
15409
+ handedOver,
15288
15410
  ...options
15289
15411
  },
15290
15412
  ) => {
15291
15413
  // An area that opted out of dragging (a text one wants to select, a control
15292
15414
  // that owns the gesture, a callout): the press there is none of our business.
15293
- if (isPressIgnored(event.target, draggedElement)) {
15415
+ if (!handedOver && isPressIgnored(event.target, draggedElement)) {
15294
15416
  return undefined;
15295
15417
  }
15296
15418
  // A secondary button (right click and friends) is a context menu, not a grab.
15297
15419
  if (!isPrimaryButtonEvent(event)) {
15298
15420
  return undefined;
15299
15421
  }
15422
+ // Where the hand is, for a press handed over: its `pointerdown` says where it
15423
+ // WAS, before the hold and whatever the hold opened. Let go of meanwhile, it
15424
+ // is not picked up after the fact.
15425
+ const handedPointer = handedOver ? readPressDown(event) : null;
15426
+ if (handedOver && !handedPointer) {
15427
+ return undefined;
15428
+ }
15300
15429
  // One press, one carry — and the same carry over again when the hand comes back
15301
15430
  // for the copy while it is still flying home (see settleCloneBack). Nothing is
15302
15431
  // made twice in that case: it is the same copy, taken in hand again where it
15303
- // had got to.
15304
- const startCarry = (pointerEvent, cloneWrapperCaught, onCarryStart) => {
15432
+ // had got to — by a press ON it, so never a press handed over.
15433
+ const startCarry = (
15434
+ pointerEvent,
15435
+ { cloneWrapperCaught, onCarryStart } = {},
15436
+ ) => {
15437
+ const handedTo = cloneWrapperCaught ? null : handedPointer;
15305
15438
  pointerEvent.preventDefault();
15306
15439
  return dragAfterIntent(
15307
15440
  pointerEvent,
@@ -15310,8 +15443,14 @@ const startDragToCarryCopy = (
15310
15443
  // one moment both ways in (a finger held still, a mouse travelled)
15311
15444
  // agree on.
15312
15445
  onCarryStart?.();
15446
+ // From the element's centre to the finger: the copy is brought that far,
15447
+ // since the hand that pressed elsewhere has nothing to follow it from.
15448
+ const reach = handedTo
15449
+ ? measureReach(draggedElement, handedTo)
15450
+ : undefined;
15313
15451
  const cloneWrapper =
15314
- cloneWrapperCaught || createDragClone(draggedElement, pointerEvent);
15452
+ cloneWrapperCaught ||
15453
+ createDragClone(draggedElement, handedTo || pointerEvent, reach);
15315
15454
  if (cloneWrapperCaught) {
15316
15455
  liftDragClone(cloneWrapperCaught, pointerEvent);
15317
15456
  }
@@ -15323,10 +15462,26 @@ const startDragToCarryCopy = (
15323
15462
  areaConstraint,
15324
15463
  ...options,
15325
15464
  });
15326
- const dragGesture = gestureController.grabViaPointer(pointerEvent, {
15327
- element: draggedElement,
15328
- elementToMove: cloneWrapper,
15329
- });
15465
+ const dragGesture = gestureController.grabViaPointer(
15466
+ pointerEvent,
15467
+ handedTo
15468
+ ? {
15469
+ element: draggedElement,
15470
+ elementToMove: cloneWrapper,
15471
+ // Counted from where the finger is now, so a release that
15472
+ // never moved it is still a hand that changed its mind.
15473
+ grabX: handedTo.clientX,
15474
+ grabY: handedTo.clientY,
15475
+ grabOffset: reach,
15476
+ // Off what the finger first touched, which the popup now
15477
+ // covers: the pointer is the carry's from here.
15478
+ pointerCaptureElement: draggedElement,
15479
+ }
15480
+ : {
15481
+ element: draggedElement,
15482
+ elementToMove: cloneWrapper,
15483
+ },
15484
+ );
15330
15485
  // getDropTargetInfo uses gestureInfo.elementImpacted to compute the dragged rect.
15331
15486
  // Point it at the clone so drop detection tracks the clone's current position.
15332
15487
  dragGesture.gestureInfo.elementImpacted = cloneWrapper;
@@ -15500,6 +15655,10 @@ const startDragToCarryCopy = (
15500
15655
  dragGesture.addReleaseCallback(async (gestureInfo) => {
15501
15656
  clearDropHintDOM();
15502
15657
  dropHintEl?.remove();
15658
+ // Let go of on its way to the hand: it is where the hand is, and from
15659
+ // there it goes wherever the answer sends it — on a translate of its
15660
+ // own, which the flight to the hand must not be holding.
15661
+ cloneWrapper.removeAttribute("data-reaching");
15503
15662
 
15504
15663
  // What THIS release means, from what the element said it can answer. A
15505
15664
  // throw is asked about first: it is the more insistent of the two, and a
@@ -15544,7 +15703,10 @@ const startDragToCarryCopy = (
15544
15703
  const copyLetGoOf = letCopyBeCaught(
15545
15704
  cloneWrapper,
15546
15705
  (pointerDownEvent, whenCarried) =>
15547
- startCarry(pointerDownEvent, cloneWrapper, whenCarried),
15706
+ startCarry(pointerDownEvent, {
15707
+ cloneWrapperCaught: cloneWrapper,
15708
+ onCarryStart: whenCarried,
15709
+ }),
15548
15710
  );
15549
15711
 
15550
15712
  // The copy stops where the hand left it, and the answer is given a way to
@@ -15648,6 +15810,7 @@ const startDragToCarryCopy = (
15648
15810
  onPressStart,
15649
15811
  onPressCancel,
15650
15812
  onPress,
15813
+ handedOver: Boolean(handedTo),
15651
15814
  },
15652
15815
  );
15653
15816
  };
@@ -15951,9 +16114,23 @@ const liftDragClone = (cloneWrapper, pointerEvent) => {
15951
16114
  cloneWrapper.firstElementChild.setAttribute("navi-drag-clone", "");
15952
16115
  };
15953
16116
 
16117
+ // From the centre of the element to the pointer, for a copy brought to a hand
16118
+ // that pressed elsewhere (see handedOver): centred rather than held at the point
16119
+ // the finger had on what it pressed, since that was another box.
16120
+ const measureReach = (element, pointer) => {
16121
+ const rect = element.getBoundingClientRect();
16122
+ return {
16123
+ x: pointer.clientX - (rect.left + rect.width / 2),
16124
+ y: pointer.clientY - (rect.top + rect.height / 2),
16125
+ };
16126
+ };
16127
+
15954
16128
  let dragCloneCount = 0;
15955
- const createDragClone = (element, pointerEvent) => {
16129
+ const createDragClone = (element, grabPoint, reach) => {
15956
16130
  const rect = element.getBoundingClientRect();
16131
+ // Over the element, or shifted to where the hand is (see measureReach).
16132
+ const left = reach ? rect.left + reach.x : rect.left;
16133
+ const top = reach ? rect.top + reach.y : rect.top;
15957
16134
 
15958
16135
  const wrapper = document.createElement("div");
15959
16136
  wrapper.setAttribute("navi-drag-clone-wrapper", "");
@@ -15965,13 +16142,26 @@ const createDragClone = (element, pointerEvent) => {
15965
16142
  // Manual: it is opened and closed with the drag, and must survive an Escape
15966
16143
  // or a click elsewhere (light dismiss would take it away mid-gesture).
15967
16144
  wrapper.setAttribute("popover", "manual");
15968
- setCloneViewportRect(wrapper, element);
15969
- // Grab point within the element — used as transform-origin so the
16145
+ wrapper.style.setProperty("--clone-top", `${top}px`);
16146
+ wrapper.style.setProperty("--clone-left", `${left}px`);
16147
+ wrapper.style.setProperty("--clone-width", `${rect.width}px`);
16148
+ wrapper.style.setProperty("--clone-height", `${rect.height}px`);
16149
+ if (reach) {
16150
+ // It flies there from the element's box, so the eye follows what was
16151
+ // picked up rather than seeing it appear under the finger (see the
16152
+ // stylesheet).
16153
+ wrapper.style.setProperty(
16154
+ "--clone-reach-from",
16155
+ `${-reach.x}px ${-reach.y}px`,
16156
+ );
16157
+ wrapper.setAttribute("data-reaching", "");
16158
+ }
16159
+ // Grab point within the copy — used as transform-origin so the
15970
16160
  // scale expands from where the user clicked, not the element center.
15971
- // These offsets are element-relative so viewport coords are correct here.
16161
+ // These offsets are copy-relative so viewport coords are correct here.
15972
16162
  wrapper.style.setProperty(
15973
16163
  "--drag-origin",
15974
- `${pointerEvent.clientX - rect.left}px ${pointerEvent.clientY - rect.top}px`,
16164
+ `${grabPoint.clientX - left}px ${grabPoint.clientY - top}px`,
15975
16165
  );
15976
16166
  const elementClone = element.cloneNode(true);
15977
16167
  // A deep copy copies the ids too, and two elements answering to one id is a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/dom",
3
- "version": "0.17.81",
3
+ "version": "0.17.82",
4
4
  "type": "module",
5
5
  "description": "DOM utilities for writing frontend code",
6
6
  "repository": {