@jsenv/dom 0.17.12 → 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 +386 -235
  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
  *
@@ -9094,61 +9141,28 @@ const dragAfterLongPress = (grabEvent, dragGestureInitializer, {
9094
9141
  onPress
9095
9142
  }) => {
9096
9143
  /*
9097
- * WHETHER a touchmove can be refused at all is decided when the touch BEGINS,
9098
- * from the non-passive listeners present at that moment and on this path the
9099
- * gesture, which is what refuses it (see preventTouchScroll in
9100
- * drag_gesture.js), is only born once the wait is over. By then the browser has
9101
- * long made up its mind: every touchmove it hands over is already
9102
- * `cancelable: false`, the refusal does nothing, and the page scrolls away with
9103
- * the object still under the finger — until the touch is taken for a scroll and
9104
- * the pointer stream is cancelled, which is the drag dying mid-gesture.
9105
- *
9106
- * So the listener the decision looks for is put down with the FINGER, before
9107
- * anyone knows whether this press is a drag. It refuses nothing — a press that
9108
- * is still only a press must leave the scroll alone, which is exactly what the
9109
- * wait is there to tell apart. Its only job is to be there, so that the
9110
- * refusal made later is one the browser still listens to.
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.
9111
9150
  */
9112
- const keepTouchRefusable = () => {
9113
- // Being registered IS the whole of it — see above.
9114
- };
9115
- const grabTarget = grabEvent.target;
9116
- window.addEventListener("touchmove", keepTouchRefusable, {
9117
- passive: false,
9118
- capture: true
9119
- });
9120
- grabTarget.addEventListener("touchmove", keepTouchRefusable, {
9121
- passive: false
9122
- });
9123
- const stopKeepingTouchRefusable = () => {
9124
- window.removeEventListener("touchmove", keepTouchRefusable, {
9125
- capture: true
9126
- });
9127
- grabTarget.removeEventListener("touchmove", keepTouchRefusable);
9128
- };
9129
9151
  waitForPressHeld(grabEvent, {
9130
9152
  delay: longPressDelay,
9131
9153
  slop: longPressSlop,
9132
9154
  onPressStart,
9133
- onPressCancel: pointerEvent => {
9134
- stopKeepingTouchRefusable();
9135
- onPressCancel?.(pointerEvent);
9136
- },
9155
+ onPressCancel,
9137
9156
  onPressHeld: (pressEvent, {
9138
9157
  endPress
9139
9158
  }) => {
9140
9159
  onPress?.(pressEvent);
9141
- // Scrolling is taken away by the gesture itself, from the moment it starts
9142
- // (see markAsStarted in drag_gesture.js) — one place refuses the touchmove,
9143
- // for every way a drag can begin.
9144
9160
  const dragGesture = startDragGesture(dragGestureInitializer);
9145
9161
  if (!dragGesture) {
9146
- stopKeepingTouchRefusable();
9147
9162
  endPress();
9148
9163
  return;
9149
9164
  }
9150
9165
  dragGesture.addReleaseCallback(() => {
9151
- stopKeepingTouchRefusable();
9152
9166
  endPress();
9153
9167
  });
9154
9168
  }
@@ -11398,6 +11412,13 @@ const css$1 = /* css */`
11398
11412
  overflow: visible;
11399
11413
  }
11400
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
+
11401
11422
  /* Ce qui a été lancé: il continue dans la direction du geste jusqu'à sortir de
11402
11423
  l'écran, et revient par le même chemin si la réponse refuse. */
11403
11424
  [navi-drag-clone-wrapper][data-tossed] {
@@ -12117,207 +12138,235 @@ const startDragToCarryCopy = (event, {
12117
12138
  if (!isPrimaryButtonEvent(event)) {
12118
12139
  return undefined;
12119
12140
  }
12120
- event.preventDefault();
12121
- return dragAfterIntent(event, () => {
12122
- const cloneWrapper = createDragClone(draggedElement, event);
12123
- draggedElement.setAttribute("navi-drag-clone-source", "");
12124
- const gestureController = createDragToMoveGestureController({
12125
- direction,
12126
- releasePositionEffect: "manual",
12127
- areaConstraint,
12128
- ...options
12129
- });
12130
- const dragGesture = gestureController.grabViaPointer(event, {
12131
- element: draggedElement,
12132
- elementToMove: cloneWrapper
12133
- });
12134
- // getDropTargetInfo uses gestureInfo.elementImpacted to compute the dragged rect.
12135
- // Point it at the clone so drop detection tracks the clone's current position.
12136
- dragGesture.gestureInfo.elementImpacted = cloneWrapper;
12137
-
12138
- // No place to land, no hint: an element that can only be thrown away has
12139
- // nowhere to be put. What the hint LOOKS like follows what a place is here —
12140
- // a line in the gap between two items, or the place itself lit up.
12141
- const dropHintEl = canLand ? createDropSurface() : canReorder ? createDropHint() : null;
12142
- if (dropHintEl) {
12143
- // In the container it draws into, which is where its own vars are set: the
12144
- // shape of a drop hint is a property of the list or board it belongs to,
12145
- // and reading it from there is inheritance rather than a hand-off.
12146
- draggedElement.parentElement.appendChild(dropHintEl);
12147
- }
12148
- // The hint first, the clone second: that order is what stacks them in the
12149
- // top layer.
12150
- dropHintEl?.showPopover();
12151
- cloneWrapper.showPopover();
12152
-
12153
- // currentBeforeElement: element before which the grabbed item will be inserted (null = end)
12154
- // currentReleaseElement: the actual hovered drop target — used to snap the clone on release
12155
- let currentBeforeElement;
12156
- let currentReleaseElement;
12157
- const clearDropHintDOM = () => {
12158
- if (!dropHintEl) {
12159
- return;
12160
- }
12161
- dropHintEl.removeAttribute("data-drop-edge");
12162
- dropHintEl.removeAttribute("data-drop-over");
12163
- dropHintEl.style.removeProperty("--drop-target-top");
12164
- dropHintEl.style.removeProperty("--drop-target-bottom");
12165
- dropHintEl.style.removeProperty("--drop-target-left");
12166
- dropHintEl.style.removeProperty("--drop-target-width");
12167
- dropHintEl.style.removeProperty("--drop-target-height");
12168
- };
12169
- const clearDropHint = () => {
12170
- currentBeforeElement = undefined;
12171
- currentReleaseElement = undefined;
12172
- clearDropHintDOM();
12173
- };
12174
- dragGesture.addDragCallback(gestureInfo => {
12175
- if (!dropHintEl) {
12176
- return;
12177
- }
12178
- const allItems = [];
12179
- const items = [];
12180
- for (const el of containerElement.querySelectorAll(itemSelector)) {
12181
- allItems.push(el);
12182
- if (el !== draggedElement) {
12183
- items.push(el);
12184
- }
12185
- }
12186
- const dropTargetInfo = getDropTargetInfo(gestureInfo, items, {
12187
- // The edges of a LIST: above the first row means the top of it, below
12188
- // the last one means the end of it. A board has no such reading — away
12189
- // from every place is away from every place.
12190
- 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
12191
12162
  });
12192
- gestureInfo.dropTargetInfo = dropTargetInfo || null;
12193
- if (!dropTargetInfo) {
12194
- clearDropHint();
12195
- return;
12196
- }
12197
- if (canLand) {
12198
- // The whole element is the target, so which of its edges the copy came
12199
- // in by says nothing: there is no gap to be on one side of.
12200
- const dropElement = dropTargetInfo.element;
12201
- 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) {
12202
12197
  return;
12203
12198
  }
12204
- currentReleaseElement = dropElement;
12205
- const dropRect = dropElement.getBoundingClientRect();
12206
- dropHintEl.setAttribute("data-drop-over", "");
12207
- dropHintEl.style.setProperty("--drop-target-top", `${dropRect.top}px`);
12208
- dropHintEl.style.setProperty("--drop-target-left", `${dropRect.left}px`);
12209
- dropHintEl.style.setProperty("--drop-target-width", `${dropRect.width}px`);
12210
- dropHintEl.style.setProperty("--drop-target-height", `${dropRect.height}px`);
12211
- return;
12212
- }
12213
- // Convert {element, edge} to a beforeElement using the items array
12214
- // (not nextElementSibling, which breaks if non-item elements exist between items).
12215
- // edge "start" → insert before the hovered element
12216
- // edge "end" → insert before the next item (null = append at end)
12217
- const edge = dropTargetInfo.elementSide.y;
12218
- const hoveredIndex = items.indexOf(dropTargetInfo.element);
12219
- const beforeElement = edge === "start" ? dropTargetInfo.element : items[hoveredIndex + 1] ?? null;
12220
- // Detect no-op: result would leave the grabbed element in the same position.
12221
- const elementIndex = allItems.indexOf(draggedElement);
12222
- const elementNextItem = allItems[elementIndex + 1] ?? null;
12223
- const isNoop = beforeElement === elementNextItem;
12224
- if (isNoop) {
12225
- clearDropHint();
12226
- return;
12227
- }
12228
- // Early return if nothing changed.
12229
- const releaseElement = dropTargetInfo.element;
12230
- if (beforeElement === currentBeforeElement && releaseElement === currentReleaseElement) {
12231
- return;
12232
- }
12233
- currentBeforeElement = beforeElement;
12234
- currentReleaseElement = releaseElement;
12235
- // Update drop hint CSS vars.
12236
- // beforeElement = null → insert at end (hint after last item)
12237
- // beforeElement = X → insert before X (hint at top edge of X)
12238
- const anchorEl = beforeElement || items[items.length - 1];
12239
- const anchorEdge = beforeElement !== null ? "top" : "bottom";
12240
- // Viewport coordinates, straight from the anchor row: the hint is fixed
12241
- // in the page (see its CSS), so there is no container box to be relative
12242
- // to and no scroll offset to add back.
12243
- const anchorRect = anchorEl.getBoundingClientRect();
12244
- dropHintEl.setAttribute("data-drop-edge", anchorEdge);
12245
- dropHintEl.style.setProperty("--drop-target-top", `${anchorRect.top}px`);
12246
- dropHintEl.style.setProperty("--drop-target-bottom", `${anchorRect.bottom}px`);
12247
- dropHintEl.style.setProperty("--drop-target-left", `${anchorRect.left}px`);
12248
- dropHintEl.style.setProperty("--drop-target-width", `${anchorRect.width}px`);
12249
- });
12250
- dragGesture.addReleaseCallback(async gestureInfo => {
12251
- clearDropHintDOM();
12252
- dropHintEl?.remove();
12253
-
12254
- // What THIS release means, from what the element said it can answer. A
12255
- // throw is asked about first: it is the more insistent of the two, and a
12256
- // hand that sent the thing across the screen has not asked for it to swap
12257
- // places with whatever it happened to fly over.
12258
- const hasDropTarget = canLand ? currentReleaseElement !== undefined : currentBeforeElement !== undefined;
12259
- const dropMeans = resolveDropMeaning({
12260
- gestureInfo,
12261
- hasDropTarget,
12262
- canReorder,
12263
- canToss,
12264
- canLand,
12265
- tossDistance,
12266
- 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`);
12267
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
+ });
12268
12306
 
12269
- // The copy stops where the hand left it, and the answer is given a way to
12270
- // take it the rest of the way synchronously, inside a view transition, so
12271
- // it is captured where it lands rather than where it was let go of.
12272
- const landCopyOn = async (targetElement, answer) => {
12273
- const clone = cloneWrapper.firstElementChild;
12274
- // Bake the current visual position (transform included) into the CSS vars
12275
- // so the copy stays where the user released it when the transform goes.
12276
- setCloneViewportRect(cloneWrapper, cloneWrapper);
12277
- gestureInfo.cancelPosition();
12278
- // Where the copy comes down is not always the thing it came down ON: a
12279
- // place of a board can be larger than what stands on it, and the copy
12280
- // has to keep its own size and land where the item will be. Said with
12281
- // an element, because the caller has one — the piece already standing
12282
- // there, the empty slot waiting.
12283
- const syncCloneWithDropTarget = (landingElement = targetElement) => {
12284
- setCloneViewportRect(cloneWrapper, landingElement);
12285
- // Removing this attr drops the CSS scale, so the browser captures the
12286
- // copy at scale 1 as the "new" state.
12287
- 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);
12288
12332
  };
12289
- await answer(syncCloneWithDropTarget);
12290
- };
12291
- if (dropMeans === "toss") {
12292
- // Bake the position the hand left it at, so the flight starts from
12293
- // there rather than from where the clone was declared.
12294
- setCloneViewportRect(cloneWrapper, cloneWrapper);
12295
- gestureInfo.cancelPosition();
12296
- const gone = await tossCloneAway(cloneWrapper, gestureInfo, onToss);
12297
- if (!gone) {
12298
- // It still exists, so the screen has to say so: the copy comes back
12299
- // over the original, and taking it away then reveals the row in
12300
- // place.
12301
- 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));
12302
12349
  }
12303
- } else if (dropMeans === "land") {
12304
- await landCopyOn(currentReleaseElement, syncCloneWithDropTarget => onLand(getItemId(draggedElement), getItemId(currentReleaseElement), syncCloneWithDropTarget));
12305
- } else if (dropMeans === "reorder") {
12306
- await landCopyOn(currentReleaseElement, syncCloneWithDropTarget => onReorder(getItemId(draggedElement), currentBeforeElement ? getItemId(currentBeforeElement) : null, syncCloneWithDropTarget));
12307
- }
12308
- draggedElement.removeAttribute("navi-drag-clone-source");
12309
- 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
12310
12367
  });
12311
- return dragGesture;
12312
- }, {
12313
- threshold,
12314
- longPress,
12315
- longPressDelay,
12316
- longPressSlop,
12317
- onPressStart,
12318
- onPressCancel,
12319
- onPress
12320
- });
12368
+ };
12369
+ return startCarry(event);
12321
12370
  };
12322
12371
 
12323
12372
  // Viewport coordinates, as getBoundingClientRect gives them: the clone is a
@@ -12427,10 +12476,112 @@ const settleCloneBack = (cloneWrapper, sourceElement) => {
12427
12476
  setTimeout(resolve, TOSS_DURATION_MS);
12428
12477
  });
12429
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
+ };
12430
12576
  const createDragClone = (element, pointerEvent) => {
12431
12577
  const rect = element.getBoundingClientRect();
12432
12578
  const wrapper = document.createElement("div");
12433
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);
12434
12585
  // Manual: it is opened and closed with the drag, and must survive an Escape
12435
12586
  // or a click elsewhere (light dismiss would take it away mid-gesture).
12436
12587
  wrapper.setAttribute("popover", "manual");
@@ -18909,4 +19060,4 @@ const useResizeStatus = (elementRef, { as = "number" } = {}) => {
18909
19060
  };
18910
19061
  };
18911
19062
 
18912
- 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.12",
3
+ "version": "0.17.13",
4
4
  "type": "module",
5
5
  "description": "DOM utilities for writing frontend code",
6
6
  "repository": {