@jsenv/dom 0.17.11 → 0.17.13

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 +390 -199
  2. package/package.json +1 -1
package/dist/jsenv_dom.js CHANGED
@@ -8969,7 +8969,9 @@ installImportMetaCssBuild(import.meta);/**
8969
8969
  every preventDefault from then on is a "Unable to preventDefault inside
8970
8970
  passive event listener" intervention — on Android, a scroll that runs away
8971
8971
  with the object. Any explicit value other than `auto` is enough: `pan-y` still
8972
- lets the page scroll and still makes the refusal effective. */
8972
+ lets the page scroll and still makes the refusal effective — provided the
8973
+ listener that will refuse is already known too, which is markDragSource's
8974
+ half of the same rule. */
8973
8975
  const css$4 = /* css */`
8974
8976
  [data-drag-handle],
8975
8977
  [data-drag-source] {
@@ -8998,6 +9000,51 @@ const css$4 = /* css */`
8998
9000
  `;
8999
9001
  import.meta.css = [css$4, "@jsenv/dom/src/interaction/drag/drag_after_intent.js"];
9000
9002
 
9003
+ /*
9004
+ * A press that may become a drag has to be refusable before anyone knows it is
9005
+ * one. WHETHER a touchmove can be refused at all is decided when the touch
9006
+ * BEGINS, from the non-passive listeners the browser knows about at that
9007
+ * moment — and on the long press path the gesture, which is what refuses it
9008
+ * (see preventTouchScroll in drag_gesture.js), is only born once the wait is
9009
+ * over. Put down from the pointerdown it is already too late: every touchmove
9010
+ * handed over is `cancelable: false`, the refusal does nothing, and the page
9011
+ * scrolls away with the object still under the finger — until the touch is
9012
+ * taken for a scroll and the pointer stream is cancelled, which is the drag
9013
+ * dying mid-gesture, released where it stood.
9014
+ *
9015
+ * So it goes down with the element, next to the attribute the stylesheet above
9016
+ * reads: same rule, same moment. It refuses nothing itself — a press that is
9017
+ * still only a press must leave the scroll alone, which is exactly what the wait
9018
+ * is there to tell apart. Being there is the whole of it.
9019
+ *
9020
+ * On the element and not on the window, so the rest of the page keeps its
9021
+ * touches on the compositor's fast path.
9022
+ */
9023
+ const keepTouchRefusable = () => {
9024
+ // Being registered IS the whole of it — see above.
9025
+ };
9026
+
9027
+ /**
9028
+ * Says an element is something a drag can start from.
9029
+ *
9030
+ * @param {Element} element
9031
+ * @param {string} [axes]
9032
+ * Which way the SURROUNDINGS scroll, so the other axis is left to them until
9033
+ * the grab: `"x"` for a source inside something travelling sideways, anything
9034
+ * else for the usual vertical page.
9035
+ * @returns {function} Takes the mark back off.
9036
+ */
9037
+ const markDragSource = (element, axes) => {
9038
+ element.setAttribute("data-drag-source", axes === "x" ? "x" : "");
9039
+ element.addEventListener("touchmove", keepTouchRefusable, {
9040
+ passive: false
9041
+ });
9042
+ return () => {
9043
+ element.removeAttribute("data-drag-source");
9044
+ element.removeEventListener("touchmove", keepTouchRefusable);
9045
+ };
9046
+ };
9047
+
9001
9048
  /**
9002
9049
  * Waits for the user to mean it, then starts a drag gesture.
9003
9050
  *
@@ -9093,6 +9140,14 @@ const dragAfterLongPress = (grabEvent, dragGestureInitializer, {
9093
9140
  onPressCancel,
9094
9141
  onPress
9095
9142
  }) => {
9143
+ /*
9144
+ * Nothing is done here to keep the touch refusable: whether it can be refused
9145
+ * at all was settled when the finger landed, from what the element already
9146
+ * carried (see markDragSource). Scrolling is then taken away by the gesture
9147
+ * itself, from the moment it starts (see preventTouchScroll in
9148
+ * drag_gesture.js) — one place refuses the touchmove, for every way a drag can
9149
+ * begin.
9150
+ */
9096
9151
  waitForPressHeld(grabEvent, {
9097
9152
  delay: longPressDelay,
9098
9153
  slop: longPressSlop,
@@ -9102,15 +9157,14 @@ const dragAfterLongPress = (grabEvent, dragGestureInitializer, {
9102
9157
  endPress
9103
9158
  }) => {
9104
9159
  onPress?.(pressEvent);
9105
- // Scrolling is taken away by the gesture itself, from the moment it starts
9106
- // (see markAsStarted in drag_gesture.js) — one place refuses the touchmove,
9107
- // for every way a drag can begin.
9108
9160
  const dragGesture = startDragGesture(dragGestureInitializer);
9109
9161
  if (!dragGesture) {
9110
9162
  endPress();
9111
9163
  return;
9112
9164
  }
9113
- dragGesture.addReleaseCallback(endPress);
9165
+ dragGesture.addReleaseCallback(() => {
9166
+ endPress();
9167
+ });
9114
9168
  }
9115
9169
  });
9116
9170
  };
@@ -11358,6 +11412,13 @@ const css$1 = /* css */`
11358
11412
  overflow: visible;
11359
11413
  }
11360
11414
 
11415
+ /* On its way home and still the object: the hand can reach for it there, so
11416
+ there it takes the pointer — which it must not do at any other moment of the
11417
+ gesture, or it would hide what it is being dropped on. */
11418
+ [navi-drag-clone-wrapper][data-catchable] {
11419
+ pointer-events: auto;
11420
+ }
11421
+
11361
11422
  /* Ce qui a été lancé: il continue dans la direction du geste jusqu'à sortir de
11362
11423
  l'écran, et revient par le même chemin si la réponse refuse. */
11363
11424
  [navi-drag-clone-wrapper][data-tossed] {
@@ -12077,207 +12138,235 @@ const startDragToCarryCopy = (event, {
12077
12138
  if (!isPrimaryButtonEvent(event)) {
12078
12139
  return undefined;
12079
12140
  }
12080
- event.preventDefault();
12081
- return dragAfterIntent(event, () => {
12082
- const cloneWrapper = createDragClone(draggedElement, event);
12083
- draggedElement.setAttribute("navi-drag-clone-source", "");
12084
- const gestureController = createDragToMoveGestureController({
12085
- direction,
12086
- releasePositionEffect: "manual",
12087
- areaConstraint,
12088
- ...options
12089
- });
12090
- const dragGesture = gestureController.grabViaPointer(event, {
12091
- element: draggedElement,
12092
- elementToMove: cloneWrapper
12093
- });
12094
- // getDropTargetInfo uses gestureInfo.elementImpacted to compute the dragged rect.
12095
- // Point it at the clone so drop detection tracks the clone's current position.
12096
- dragGesture.gestureInfo.elementImpacted = cloneWrapper;
12097
-
12098
- // No place to land, no hint: an element that can only be thrown away has
12099
- // nowhere to be put. What the hint LOOKS like follows what a place is here —
12100
- // a line in the gap between two items, or the place itself lit up.
12101
- const dropHintEl = canLand ? createDropSurface() : canReorder ? createDropHint() : null;
12102
- if (dropHintEl) {
12103
- // In the container it draws into, which is where its own vars are set: the
12104
- // shape of a drop hint is a property of the list or board it belongs to,
12105
- // and reading it from there is inheritance rather than a hand-off.
12106
- draggedElement.parentElement.appendChild(dropHintEl);
12107
- }
12108
- // The hint first, the clone second: that order is what stacks them in the
12109
- // top layer.
12110
- dropHintEl?.showPopover();
12111
- cloneWrapper.showPopover();
12112
-
12113
- // currentBeforeElement: element before which the grabbed item will be inserted (null = end)
12114
- // currentReleaseElement: the actual hovered drop target — used to snap the clone on release
12115
- let currentBeforeElement;
12116
- let currentReleaseElement;
12117
- const clearDropHintDOM = () => {
12118
- if (!dropHintEl) {
12119
- return;
12120
- }
12121
- dropHintEl.removeAttribute("data-drop-edge");
12122
- dropHintEl.removeAttribute("data-drop-over");
12123
- dropHintEl.style.removeProperty("--drop-target-top");
12124
- dropHintEl.style.removeProperty("--drop-target-bottom");
12125
- dropHintEl.style.removeProperty("--drop-target-left");
12126
- dropHintEl.style.removeProperty("--drop-target-width");
12127
- dropHintEl.style.removeProperty("--drop-target-height");
12128
- };
12129
- const clearDropHint = () => {
12130
- currentBeforeElement = undefined;
12131
- currentReleaseElement = undefined;
12132
- clearDropHintDOM();
12133
- };
12134
- dragGesture.addDragCallback(gestureInfo => {
12135
- if (!dropHintEl) {
12136
- return;
12137
- }
12138
- const allItems = [];
12139
- const items = [];
12140
- for (const el of containerElement.querySelectorAll(itemSelector)) {
12141
- allItems.push(el);
12142
- if (el !== draggedElement) {
12143
- items.push(el);
12144
- }
12145
- }
12146
- const dropTargetInfo = getDropTargetInfo(gestureInfo, items, {
12147
- // The edges of a LIST: above the first row means the top of it, below
12148
- // the last one means the end of it. A board has no such reading — away
12149
- // from every place is away from every place.
12150
- fallbackToEdge: !canLand
12141
+ // One press, one carry — and the same carry over again when the hand comes back
12142
+ // for the copy while it is still flying home (see settleCloneBack). Nothing is
12143
+ // made twice in that case: it is the same copy, taken in hand again where it
12144
+ // had got to.
12145
+ const startCarry = (pointerEvent, cloneWrapperCaught, onCarryStart) => {
12146
+ pointerEvent.preventDefault();
12147
+ return dragAfterIntent(pointerEvent, () => {
12148
+ // Here and nowhere else is where a press has turned into a carry — the
12149
+ // one moment both ways in (a finger held still, a mouse travelled)
12150
+ // agree on.
12151
+ onCarryStart?.();
12152
+ const cloneWrapper = cloneWrapperCaught || createDragClone(draggedElement, pointerEvent);
12153
+ if (cloneWrapperCaught) {
12154
+ liftDragClone(cloneWrapperCaught, pointerEvent);
12155
+ }
12156
+ draggedElement.setAttribute("navi-drag-clone-source", "");
12157
+ const gestureController = createDragToMoveGestureController({
12158
+ direction,
12159
+ releasePositionEffect: "manual",
12160
+ areaConstraint,
12161
+ ...options
12151
12162
  });
12152
- gestureInfo.dropTargetInfo = dropTargetInfo || null;
12153
- if (!dropTargetInfo) {
12154
- clearDropHint();
12155
- return;
12156
- }
12157
- if (canLand) {
12158
- // The whole element is the target, so which of its edges the copy came
12159
- // in by says nothing: there is no gap to be on one side of.
12160
- const dropElement = dropTargetInfo.element;
12161
- if (dropElement === currentReleaseElement) {
12163
+ const dragGesture = gestureController.grabViaPointer(pointerEvent, {
12164
+ element: draggedElement,
12165
+ elementToMove: cloneWrapper
12166
+ });
12167
+ // getDropTargetInfo uses gestureInfo.elementImpacted to compute the dragged rect.
12168
+ // Point it at the clone so drop detection tracks the clone's current position.
12169
+ dragGesture.gestureInfo.elementImpacted = cloneWrapper;
12170
+
12171
+ // No place to land, no hint: an element that can only be thrown away has
12172
+ // nowhere to be put. What the hint LOOKS like follows what a place is here —
12173
+ // a line in the gap between two items, or the place itself lit up.
12174
+ const dropHintEl = canLand ? createDropSurface() : canReorder ? createDropHint() : null;
12175
+ if (dropHintEl) {
12176
+ // In the container it draws into, which is where its own vars are set: the
12177
+ // shape of a drop hint is a property of the list or board it belongs to,
12178
+ // and reading it from there is inheritance rather than a hand-off.
12179
+ draggedElement.parentElement.appendChild(dropHintEl);
12180
+ }
12181
+ // The hint first, the clone second: that order is what stacks them in the
12182
+ // top layer. A copy taken back in hand is already up there, and the hint
12183
+ // just went above it — shown again it returns to the top, where what the
12184
+ // hand carries belongs.
12185
+ dropHintEl?.showPopover();
12186
+ if (cloneWrapperCaught) {
12187
+ cloneWrapper.hidePopover();
12188
+ }
12189
+ cloneWrapper.showPopover();
12190
+
12191
+ // currentBeforeElement: element before which the grabbed item will be inserted (null = end)
12192
+ // currentReleaseElement: the actual hovered drop target — used to snap the clone on release
12193
+ let currentBeforeElement;
12194
+ let currentReleaseElement;
12195
+ const clearDropHintDOM = () => {
12196
+ if (!dropHintEl) {
12162
12197
  return;
12163
12198
  }
12164
- currentReleaseElement = dropElement;
12165
- const dropRect = dropElement.getBoundingClientRect();
12166
- dropHintEl.setAttribute("data-drop-over", "");
12167
- dropHintEl.style.setProperty("--drop-target-top", `${dropRect.top}px`);
12168
- dropHintEl.style.setProperty("--drop-target-left", `${dropRect.left}px`);
12169
- dropHintEl.style.setProperty("--drop-target-width", `${dropRect.width}px`);
12170
- dropHintEl.style.setProperty("--drop-target-height", `${dropRect.height}px`);
12171
- return;
12172
- }
12173
- // Convert {element, edge} to a beforeElement using the items array
12174
- // (not nextElementSibling, which breaks if non-item elements exist between items).
12175
- // edge "start" → insert before the hovered element
12176
- // edge "end" → insert before the next item (null = append at end)
12177
- const edge = dropTargetInfo.elementSide.y;
12178
- const hoveredIndex = items.indexOf(dropTargetInfo.element);
12179
- const beforeElement = edge === "start" ? dropTargetInfo.element : items[hoveredIndex + 1] ?? null;
12180
- // Detect no-op: result would leave the grabbed element in the same position.
12181
- const elementIndex = allItems.indexOf(draggedElement);
12182
- const elementNextItem = allItems[elementIndex + 1] ?? null;
12183
- const isNoop = beforeElement === elementNextItem;
12184
- if (isNoop) {
12185
- clearDropHint();
12186
- return;
12187
- }
12188
- // Early return if nothing changed.
12189
- const releaseElement = dropTargetInfo.element;
12190
- if (beforeElement === currentBeforeElement && releaseElement === currentReleaseElement) {
12191
- return;
12192
- }
12193
- currentBeforeElement = beforeElement;
12194
- currentReleaseElement = releaseElement;
12195
- // Update drop hint CSS vars.
12196
- // beforeElement = null → insert at end (hint after last item)
12197
- // beforeElement = X → insert before X (hint at top edge of X)
12198
- const anchorEl = beforeElement || items[items.length - 1];
12199
- const anchorEdge = beforeElement !== null ? "top" : "bottom";
12200
- // Viewport coordinates, straight from the anchor row: the hint is fixed
12201
- // in the page (see its CSS), so there is no container box to be relative
12202
- // to and no scroll offset to add back.
12203
- const anchorRect = anchorEl.getBoundingClientRect();
12204
- dropHintEl.setAttribute("data-drop-edge", anchorEdge);
12205
- dropHintEl.style.setProperty("--drop-target-top", `${anchorRect.top}px`);
12206
- dropHintEl.style.setProperty("--drop-target-bottom", `${anchorRect.bottom}px`);
12207
- dropHintEl.style.setProperty("--drop-target-left", `${anchorRect.left}px`);
12208
- dropHintEl.style.setProperty("--drop-target-width", `${anchorRect.width}px`);
12209
- });
12210
- dragGesture.addReleaseCallback(async gestureInfo => {
12211
- clearDropHintDOM();
12212
- dropHintEl?.remove();
12213
-
12214
- // What THIS release means, from what the element said it can answer. A
12215
- // throw is asked about first: it is the more insistent of the two, and a
12216
- // hand that sent the thing across the screen has not asked for it to swap
12217
- // places with whatever it happened to fly over.
12218
- const hasDropTarget = canLand ? currentReleaseElement !== undefined : currentBeforeElement !== undefined;
12219
- const dropMeans = resolveDropMeaning({
12220
- gestureInfo,
12221
- hasDropTarget,
12222
- canReorder,
12223
- canToss,
12224
- canLand,
12225
- tossDistance,
12226
- tossSpeed
12199
+ dropHintEl.removeAttribute("data-drop-edge");
12200
+ dropHintEl.removeAttribute("data-drop-over");
12201
+ dropHintEl.style.removeProperty("--drop-target-top");
12202
+ dropHintEl.style.removeProperty("--drop-target-bottom");
12203
+ dropHintEl.style.removeProperty("--drop-target-left");
12204
+ dropHintEl.style.removeProperty("--drop-target-width");
12205
+ dropHintEl.style.removeProperty("--drop-target-height");
12206
+ };
12207
+ const clearDropHint = () => {
12208
+ currentBeforeElement = undefined;
12209
+ currentReleaseElement = undefined;
12210
+ clearDropHintDOM();
12211
+ };
12212
+ dragGesture.addDragCallback(gestureInfo => {
12213
+ if (!dropHintEl) {
12214
+ return;
12215
+ }
12216
+ const allItems = [];
12217
+ const items = [];
12218
+ for (const el of containerElement.querySelectorAll(itemSelector)) {
12219
+ allItems.push(el);
12220
+ if (el !== draggedElement) {
12221
+ items.push(el);
12222
+ }
12223
+ }
12224
+ const dropTargetInfo = getDropTargetInfo(gestureInfo, items, {
12225
+ // The edges of a LIST: above the first row means the top of it, below
12226
+ // the last one means the end of it. A board has no such reading — away
12227
+ // from every place is away from every place.
12228
+ fallbackToEdge: !canLand
12229
+ });
12230
+ gestureInfo.dropTargetInfo = dropTargetInfo || null;
12231
+ if (!dropTargetInfo) {
12232
+ clearDropHint();
12233
+ return;
12234
+ }
12235
+ if (canLand) {
12236
+ // The whole element is the target, so which of its edges the copy came
12237
+ // in by says nothing: there is no gap to be on one side of.
12238
+ const dropElement = dropTargetInfo.element;
12239
+ if (dropElement === currentReleaseElement) {
12240
+ return;
12241
+ }
12242
+ currentReleaseElement = dropElement;
12243
+ const dropRect = dropElement.getBoundingClientRect();
12244
+ dropHintEl.setAttribute("data-drop-over", "");
12245
+ dropHintEl.style.setProperty("--drop-target-top", `${dropRect.top}px`);
12246
+ dropHintEl.style.setProperty("--drop-target-left", `${dropRect.left}px`);
12247
+ dropHintEl.style.setProperty("--drop-target-width", `${dropRect.width}px`);
12248
+ dropHintEl.style.setProperty("--drop-target-height", `${dropRect.height}px`);
12249
+ return;
12250
+ }
12251
+ // Convert {element, edge} to a beforeElement using the items array
12252
+ // (not nextElementSibling, which breaks if non-item elements exist between items).
12253
+ // edge "start" insert before the hovered element
12254
+ // edge "end" → insert before the next item (null = append at end)
12255
+ const edge = dropTargetInfo.elementSide.y;
12256
+ const hoveredIndex = items.indexOf(dropTargetInfo.element);
12257
+ const beforeElement = edge === "start" ? dropTargetInfo.element : items[hoveredIndex + 1] ?? null;
12258
+ // Detect no-op: result would leave the grabbed element in the same position.
12259
+ const elementIndex = allItems.indexOf(draggedElement);
12260
+ const elementNextItem = allItems[elementIndex + 1] ?? null;
12261
+ const isNoop = beforeElement === elementNextItem;
12262
+ if (isNoop) {
12263
+ clearDropHint();
12264
+ return;
12265
+ }
12266
+ // Early return if nothing changed.
12267
+ const releaseElement = dropTargetInfo.element;
12268
+ if (beforeElement === currentBeforeElement && releaseElement === currentReleaseElement) {
12269
+ return;
12270
+ }
12271
+ currentBeforeElement = beforeElement;
12272
+ currentReleaseElement = releaseElement;
12273
+ // Update drop hint CSS vars.
12274
+ // beforeElement = null → insert at end (hint after last item)
12275
+ // beforeElement = X → insert before X (hint at top edge of X)
12276
+ const anchorEl = beforeElement || items[items.length - 1];
12277
+ const anchorEdge = beforeElement !== null ? "top" : "bottom";
12278
+ // Viewport coordinates, straight from the anchor row: the hint is fixed
12279
+ // in the page (see its CSS), so there is no container box to be relative
12280
+ // to and no scroll offset to add back.
12281
+ const anchorRect = anchorEl.getBoundingClientRect();
12282
+ dropHintEl.setAttribute("data-drop-edge", anchorEdge);
12283
+ dropHintEl.style.setProperty("--drop-target-top", `${anchorRect.top}px`);
12284
+ dropHintEl.style.setProperty("--drop-target-bottom", `${anchorRect.bottom}px`);
12285
+ dropHintEl.style.setProperty("--drop-target-left", `${anchorRect.left}px`);
12286
+ dropHintEl.style.setProperty("--drop-target-width", `${anchorRect.width}px`);
12227
12287
  });
12288
+ dragGesture.addReleaseCallback(async gestureInfo => {
12289
+ clearDropHintDOM();
12290
+ dropHintEl?.remove();
12291
+
12292
+ // What THIS release means, from what the element said it can answer. A
12293
+ // throw is asked about first: it is the more insistent of the two, and a
12294
+ // hand that sent the thing across the screen has not asked for it to swap
12295
+ // places with whatever it happened to fly over.
12296
+ const hasDropTarget = canLand ? currentReleaseElement !== undefined : currentBeforeElement !== undefined;
12297
+ const dropMeans = resolveDropMeaning({
12298
+ gestureInfo,
12299
+ hasDropTarget,
12300
+ canReorder,
12301
+ canToss,
12302
+ canLand,
12303
+ tossDistance,
12304
+ tossSpeed
12305
+ });
12228
12306
 
12229
- // The copy stops where the hand left it, and the answer is given a way to
12230
- // take it the rest of the way synchronously, inside a view transition, so
12231
- // it is captured where it lands rather than where it was let go of.
12232
- const landCopyOn = async (targetElement, answer) => {
12233
- const clone = cloneWrapper.firstElementChild;
12234
- // Bake the current visual position (transform included) into the CSS vars
12235
- // so the copy stays where the user released it when the transform goes.
12236
- setCloneViewportRect(cloneWrapper, cloneWrapper);
12237
- gestureInfo.cancelPosition();
12238
- // Where the copy comes down is not always the thing it came down ON: a
12239
- // place of a board can be larger than what stands on it, and the copy
12240
- // has to keep its own size and land where the item will be. Said with
12241
- // an element, because the caller has one — the piece already standing
12242
- // there, the empty slot waiting.
12243
- const syncCloneWithDropTarget = (landingElement = targetElement) => {
12244
- setCloneViewportRect(cloneWrapper, landingElement);
12245
- // Removing this attr drops the CSS scale, so the browser captures the
12246
- // copy at scale 1 as the "new" state.
12247
- clone.removeAttribute("navi-drag-clone");
12307
+ // Let go of and still on the screen: from here until it is taken away
12308
+ // the copy can be taken back in hand (see letCopyBeCaught).
12309
+ const copyLetGoOf = letCopyBeCaught(cloneWrapper, (pointerDownEvent, whenCarried) => startCarry(pointerDownEvent, cloneWrapper, whenCarried));
12310
+
12311
+ // The copy stops where the hand left it, and the answer is given a way to
12312
+ // take it the rest of the way synchronously, inside a view transition, so
12313
+ // it is captured where it lands rather than where it was let go of.
12314
+ const landCopyOn = async (targetElement, answer) => {
12315
+ const clone = cloneWrapper.firstElementChild;
12316
+ // Bake the current visual position (transform included) into the CSS vars
12317
+ // so the copy stays where the user released it when the transform goes.
12318
+ setCloneViewportRect(cloneWrapper, cloneWrapper);
12319
+ gestureInfo.cancelPosition();
12320
+ // Where the copy comes down is not always the thing it came down ON: a
12321
+ // place of a board can be larger than what stands on it, and the copy
12322
+ // has to keep its own size and land where the item will be. Said with
12323
+ // an element, because the caller has one the piece already standing
12324
+ // there, the empty slot waiting.
12325
+ const syncCloneWithDropTarget = (landingElement = targetElement) => {
12326
+ setCloneViewportRect(cloneWrapper, landingElement);
12327
+ // Removing this attr drops the CSS scale, so the browser captures the
12328
+ // copy at scale 1 as the "new" state.
12329
+ clone.removeAttribute("navi-drag-clone");
12330
+ };
12331
+ await answer(syncCloneWithDropTarget);
12248
12332
  };
12249
- await answer(syncCloneWithDropTarget);
12250
- };
12251
- if (dropMeans === "toss") {
12252
- // Bake the position the hand left it at, so the flight starts from
12253
- // there rather than from where the clone was declared.
12254
- setCloneViewportRect(cloneWrapper, cloneWrapper);
12255
- gestureInfo.cancelPosition();
12256
- const gone = await tossCloneAway(cloneWrapper, gestureInfo, onToss);
12257
- if (!gone) {
12258
- // It still exists, so the screen has to say so: the copy comes back
12259
- // over the original, and taking it away then reveals the row in
12260
- // place.
12261
- await settleCloneBack(cloneWrapper, draggedElement);
12333
+ if (dropMeans === "toss") {
12334
+ // Bake the position the hand left it at, so the flight starts from
12335
+ // there rather than from where the clone was declared.
12336
+ setCloneViewportRect(cloneWrapper, cloneWrapper);
12337
+ gestureInfo.cancelPosition();
12338
+ const gone = await tossCloneAway(cloneWrapper, gestureInfo, onToss);
12339
+ if (!gone) {
12340
+ // It still exists, so the screen has to say so: the copy comes back
12341
+ // over the original, and taking it away then reveals the row in
12342
+ // place.
12343
+ await settleCloneBack(cloneWrapper, draggedElement);
12344
+ }
12345
+ } else if (dropMeans === "land") {
12346
+ await landCopyOn(currentReleaseElement, syncCloneWithDropTarget => onLand(getItemId(draggedElement), getItemId(currentReleaseElement), syncCloneWithDropTarget));
12347
+ } else if (dropMeans === "reorder") {
12348
+ await landCopyOn(currentReleaseElement, syncCloneWithDropTarget => onReorder(getItemId(draggedElement), currentBeforeElement ? getItemId(currentBeforeElement) : null, syncCloneWithDropTarget));
12262
12349
  }
12263
- } else if (dropMeans === "land") {
12264
- await landCopyOn(currentReleaseElement, syncCloneWithDropTarget => onLand(getItemId(draggedElement), getItemId(currentReleaseElement), syncCloneWithDropTarget));
12265
- } else if (dropMeans === "reorder") {
12266
- await landCopyOn(currentReleaseElement, syncCloneWithDropTarget => onReorder(getItemId(draggedElement), currentBeforeElement ? getItemId(currentBeforeElement) : null, syncCloneWithDropTarget));
12267
- }
12268
- draggedElement.removeAttribute("navi-drag-clone-source");
12269
- cloneWrapper.remove();
12350
+ if (await copyLetGoOf.settled()) {
12351
+ // In a hand again: the copy, and the place kept for it, are the new
12352
+ // gesture's this one has nothing left to take away.
12353
+ return;
12354
+ }
12355
+ draggedElement.removeAttribute("navi-drag-clone-source");
12356
+ cloneWrapper.remove();
12357
+ });
12358
+ return dragGesture;
12359
+ }, {
12360
+ threshold,
12361
+ longPress,
12362
+ longPressDelay,
12363
+ longPressSlop,
12364
+ onPressStart,
12365
+ onPressCancel,
12366
+ onPress
12270
12367
  });
12271
- return dragGesture;
12272
- }, {
12273
- threshold,
12274
- longPress,
12275
- longPressDelay,
12276
- longPressSlop,
12277
- onPressStart,
12278
- onPressCancel,
12279
- onPress
12280
- });
12368
+ };
12369
+ return startCarry(event);
12281
12370
  };
12282
12371
 
12283
12372
  // Viewport coordinates, as getBoundingClientRect gives them: the clone is a
@@ -12387,10 +12476,112 @@ const settleCloneBack = (cloneWrapper, sourceElement) => {
12387
12476
  setTimeout(resolve, TOSS_DURATION_MS);
12388
12477
  });
12389
12478
  };
12479
+
12480
+ /**
12481
+ * The copy is let go of, and it is still there: flying home, coming down on a
12482
+ * place, waiting on an answer. It is still the object for all that time — a hand
12483
+ * reaching for it there is reaching for the thing, and the answer is to give it
12484
+ * back.
12485
+ *
12486
+ * Left alone the press finds nothing: the copy does not take the pointer (it must
12487
+ * not, while it is carried, or it would hide what it is being dropped on) and the
12488
+ * original is hidden underneath it. The press falls through to the page, which
12489
+ * answers a held finger with the system context menu — on the very gesture that
12490
+ * meant "I am taking it back". So the copy takes the pointer for exactly this
12491
+ * stretch of the gesture, and for no other.
12492
+ *
12493
+ * It is not stopped where it is caught: it finishes what it was doing under the
12494
+ * hand and is picked up from wherever it got to, which is where it visibly is —
12495
+ * a press has to be held a moment before it counts as a carry, about as long as
12496
+ * these journeys last. What catching it does change is that the copy is not taken
12497
+ * away while a hand is on it: `settled()` waits the press out, so a carry has the
12498
+ * time to be born.
12499
+ *
12500
+ * THE PRESS IS READ AT THE DOCUMENT, and the copy's own box is only the shape it
12501
+ * is matched against. A press landing on it does not always reach it: an answer
12502
+ * that runs a view transition (which is the usual answer — it is what makes a
12503
+ * landing continuous) has the browser cover the page with its pictures, and every
12504
+ * press then goes to the document root whatever those pictures are told about
12505
+ * pointer events. Read from the document and matched against the box, it is
12506
+ * caught either way. Same reason, same shape as the box of travelling pages
12507
+ * (see route_travel.jsx in navi).
12508
+ *
12509
+ * @returns {{settled: function}} `settled()` resolves to whether the copy was
12510
+ * taken back in hand — and then it belongs to the new gesture, not to this one.
12511
+ */
12512
+ const letCopyBeCaught = (cloneWrapper, carryAgain) => {
12513
+ let caught = false;
12514
+ let pressIsOver = Promise.resolve();
12515
+ const onPointerDown = pointerDownEvent => {
12516
+ const {
12517
+ left,
12518
+ right,
12519
+ top,
12520
+ bottom
12521
+ } = cloneWrapper.getBoundingClientRect();
12522
+ const {
12523
+ clientX,
12524
+ clientY
12525
+ } = pointerDownEvent;
12526
+ if (clientX < left || clientX > right || clientY < top || clientY > bottom) {
12527
+ // Somewhere else on the page: this press is not about the copy.
12528
+ return;
12529
+ }
12530
+ let pressIsOverResolve;
12531
+ pressIsOver = new Promise(resolve => {
12532
+ pressIsOverResolve = resolve;
12533
+ });
12534
+ const onPointerEnd = () => {
12535
+ window.removeEventListener("pointerup", onPointerEnd, true);
12536
+ window.removeEventListener("pointercancel", onPointerEnd, true);
12537
+ pressIsOverResolve();
12538
+ };
12539
+ window.addEventListener("pointerup", onPointerEnd, true);
12540
+ window.addEventListener("pointercancel", onPointerEnd, true);
12541
+ carryAgain(pointerDownEvent, () => {
12542
+ caught = true;
12543
+ onPointerEnd();
12544
+ });
12545
+ };
12546
+ // The attribute is what gives the copy the pointer (see the stylesheet): read
12547
+ // at the document or not, a press meant for the copy must land ON it, so the
12548
+ // gesture holds what it grabbed rather than whatever was behind.
12549
+ cloneWrapper.setAttribute("data-catchable", "");
12550
+ document.addEventListener("pointerdown", onPointerDown, true);
12551
+ return {
12552
+ settled: async () => {
12553
+ // A hand that lets go and presses again while the copy is still there is
12554
+ // one more press to wait out, not a press that was already over.
12555
+ let awaited;
12556
+ while (awaited !== pressIsOver) {
12557
+ awaited = pressIsOver;
12558
+ await awaited;
12559
+ }
12560
+ cloneWrapper.removeAttribute("data-catchable");
12561
+ document.removeEventListener("pointerdown", onPointerDown, true);
12562
+ return caught;
12563
+ }
12564
+ };
12565
+ };
12566
+
12567
+ // What a copy is given when it is picked up: the point it lifts FROM, and the
12568
+ // lift itself. Both are lost by a copy that has already been let go of — a
12569
+ // landing drops the lift (see syncCloneWithDropTarget) and the hand catching it
12570
+ // again is somewhere else on it — so taking one back hands it both again.
12571
+ const liftDragClone = (cloneWrapper, pointerEvent) => {
12572
+ const rect = cloneWrapper.getBoundingClientRect();
12573
+ cloneWrapper.style.setProperty("--drag-origin", `${pointerEvent.clientX - rect.left}px ${pointerEvent.clientY - rect.top}px`);
12574
+ cloneWrapper.firstElementChild.setAttribute("navi-drag-clone", "");
12575
+ };
12390
12576
  const createDragClone = (element, pointerEvent) => {
12391
12577
  const rect = element.getBoundingClientRect();
12392
12578
  const wrapper = document.createElement("div");
12393
12579
  wrapper.setAttribute("navi-drag-clone-wrapper", "");
12580
+ // A copy can be caught on its way home (see settleCloneBack), which is a press
12581
+ // that may become a drag — so it carries what such a press needs, and it
12582
+ // carries it from the moment it exists: what a touch may do is decided when the
12583
+ // touch begins, and by then this has to have been true for a while.
12584
+ markDragSource(wrapper);
12394
12585
  // Manual: it is opened and closed with the drag, and must survive an Escape
12395
12586
  // or a click elsewhere (light dismiss would take it away mid-gesture).
12396
12587
  wrapper.setAttribute("popover", "manual");
@@ -18869,4 +19060,4 @@ const useResizeStatus = (elementRef, { as = "number" } = {}) => {
18869
19060
  };
18870
19061
  };
18871
19062
 
18872
- export { EASING, ELEMENT_SIZE_CHANGE, activeElementSignal, addActiveElementEffect, addAttributeEffect, allowWheelThrough, appendStyles, applyNewPosition, canScroll, captureScrollState, chainEvent, claimWheelGesture, 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, getScrollRelativeRect, getSelfAndAncestorScrolls, getStyle, getTranslateX, getTranslateXWithoutTransition, getTranslateY, getVisuallyVisibleInfo, getWidth, getWidthWithoutTransition, hasCSSSizeUnit, initFlexDetailsSet, initFocusGroup, initPositionSticky, isAncestorOpen, isPrimaryButtonEvent, isSameColor, isScrollable, 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, scrollIntoViewWithStickyAwareness, scrollRoomTowards, setAttribute, setAttributes, setStyles, snapToPixel, startDragTo, startDragToResizeGesture, startDragToTravel, stickyAsRelativeCoords, stringifyStyle, subscribeVisualViewportResizeSettled, subscribeWindowResizeSettled, suppressClickAfterGesture, trapFocusInside, trapScrollInside, useActiveElement, useAvailableHeight, useAvailableWidth, useMaxHeight, useMaxWidth, useResizeStatus, visibleRectEffect, waitForPressHeld, watchWheelTravel, wheelGestureIsTakenFrom };
19063
+ export { EASING, ELEMENT_SIZE_CHANGE, activeElementSignal, addActiveElementEffect, addAttributeEffect, allowWheelThrough, appendStyles, applyNewPosition, canScroll, captureScrollState, chainEvent, claimWheelGesture, 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, getScrollRelativeRect, getSelfAndAncestorScrolls, getStyle, getTranslateX, getTranslateXWithoutTransition, getTranslateY, getVisuallyVisibleInfo, getWidth, getWidthWithoutTransition, hasCSSSizeUnit, initFlexDetailsSet, initFocusGroup, initPositionSticky, isAncestorOpen, isPrimaryButtonEvent, isSameColor, isScrollable, 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, scrollIntoViewWithStickyAwareness, scrollRoomTowards, setAttribute, setAttributes, setStyles, snapToPixel, startDragTo, startDragToResizeGesture, startDragToTravel, stickyAsRelativeCoords, stringifyStyle, 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.11",
3
+ "version": "0.17.13",
4
4
  "type": "module",
5
5
  "description": "DOM utilities for writing frontend code",
6
6
  "repository": {