@jsenv/dom 0.17.19 → 0.17.20

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 +114 -26
  2. package/package.json +1 -1
package/dist/jsenv_dom.js CHANGED
@@ -261,11 +261,22 @@ const chainEvent = (customEvent, parentEvent) => {
261
261
  if (!parentEvent) {
262
262
  return customEvent;
263
263
  }
264
- if (!customEvent.detail) {
265
- console.warn(
266
- `Event "${customEvent.type}" has no detail object. Cannot chain to parent event "${parentEvent.type}".`,
267
- );
268
- return customEvent;
264
+ if (!customEvent.detail || typeof customEvent.detail !== "object") {
265
+ // A native event has nowhere to hang the chain: `Event` has no detail at
266
+ // all and `UIEvent` (so `InputEvent` too) exposes it as a readonly number.
267
+ // Give it an own detail object, shadowing the prototype getter, so a
268
+ // synthetic event dispatched on behalf of a gesture can still say what
269
+ // caused it.
270
+ if (nativeDetailHasMeaning(customEvent)) {
271
+ console.warn(
272
+ `Chaining "${customEvent.type}" to "${parentEvent.type}" replaces its native detail (${customEvent.detail}), which carries the click count on this event type. Chain a custom event instead, or read the click count before chaining.`,
273
+ );
274
+ }
275
+ Object.defineProperty(customEvent, "detail", {
276
+ value: {},
277
+ configurable: true,
278
+ enumerable: true,
279
+ });
269
280
  }
270
281
  // Always build eventChain from the first wrapping so callers can rely on it
271
282
  // being present whenever `parentEvent` is set.
@@ -309,6 +320,24 @@ const findEvent = (event, predicate) => {
309
320
  return undefined;
310
321
  };
311
322
 
323
+ // `detail` is a click count on the pointer events that define one, and 0
324
+ // everywhere else (`input`, `focus`, `wheel`…). Overwriting it there loses the
325
+ // only way to tell a real click from a keyboard/programmatic one (detail === 0),
326
+ // so those events must not be chained.
327
+ const EVENT_TYPES_WITH_MEANINGFUL_DETAIL = new Set([
328
+ "click",
329
+ "auxclick",
330
+ "dblclick",
331
+ "mousedown",
332
+ "mouseup",
333
+ ]);
334
+ const nativeDetailHasMeaning = (event) => {
335
+ if (EVENT_TYPES_WITH_MEANINGFUL_DETAIL.has(event.type)) {
336
+ return true;
337
+ }
338
+ return typeof event.detail === "number" && event.detail !== 0;
339
+ };
340
+
312
341
  const resolveEventPredicate = (predicate) => {
313
342
  if (typeof predicate === "string") {
314
343
  return (e) => e.type === predicate;
@@ -9117,12 +9146,16 @@ const css$4 = /* css */`
9117
9146
  /* A source taken by long press must let the scroll through until the grab —
9118
9147
  which is exactly what the long press is there to tell apart. Zoom has
9119
9148
  nothing to do with the gesture and nobody should lose it by resting a
9120
- finger on a word. */
9149
+ finger on a word.
9150
+
9151
+ Vertical, because that is the way the page and the lists in it go: a
9152
+ source dragged along one axis is surrounded by something scrolling along
9153
+ that same axis (a row of a list runs the way the list scrolls), and a
9154
+ source dragged both ways sits on the usual vertical page. */
9121
9155
  touch-action: pan-y pinch-zoom;
9122
9156
  }
9123
9157
  [data-drag-source="x"] {
9124
- /* The axis is the one thing the caller has to say, being the only one who
9125
- knows which way what surrounds the source scrolls. */
9158
+ /* …and the sideways one, for the same reason read the other way. */
9126
9159
  touch-action: pan-x pinch-zoom;
9127
9160
  }
9128
9161
  [data-drag-on-contact] [data-drag-source],
@@ -9158,23 +9191,36 @@ import.meta.css = [css$4, "@jsenv/dom/src/interaction/drag/drag_after_intent.js"
9158
9191
  *
9159
9192
  * On the element and not on the window, so the rest of the page keeps its
9160
9193
  * touches on the compositor's fast path.
9194
+ *
9195
+ * Exported because a drag does not always begin on a drag source: a copy caught
9196
+ * on its way home is pressed through the pictures of a view transition, and the
9197
+ * touch lands on the document root (see letCopyBeCaught in drag_to.js). Same
9198
+ * rule, other element — and it has to be the same function, or the listener put
9199
+ * down is not the one taken back off.
9161
9200
  */
9162
9201
  const keepTouchRefusable = () => {
9163
9202
  // Being registered IS the whole of it — see above.
9164
9203
  };
9165
9204
 
9166
9205
  /**
9167
- * Says an element is something a drag can start from.
9206
+ * Says an element is something a drag can start from, and which way that drag
9207
+ * goes.
9208
+ *
9209
+ * The axes are written in the DOM rather than kept here because they are what
9210
+ * someone ELSE reads: a box above this one that travels under the same finger
9211
+ * (a row of slides, a sheet pushed down to close it) has to know which axes are
9212
+ * already spoken for before it answers the press — the same thing a travel says
9213
+ * about itself with `data-travel-by-drag`. It is also what leaves the browser
9214
+ * the pan it may still do until the grab (see the stylesheet above).
9168
9215
  *
9169
9216
  * @param {Element} element
9170
- * @param {string} [axes]
9171
- * Which way the SURROUNDINGS scroll, so the other axis is left to them until
9172
- * the grab: `"x"` for a source inside something travelling sideways, anything
9173
- * else for the usual vertical page.
9217
+ * @param {"x"|"y"|"xy"} [axes="xy"]
9218
+ * Which way the drag walks. A list reordered along its own line says `"y"`;
9219
+ * something carried across a board, or thrown, goes both ways.
9174
9220
  * @returns {function} Takes the mark back off.
9175
9221
  */
9176
- const markDragSource = (element, axes) => {
9177
- element.setAttribute("data-drag-source", axes === "x" ? "x" : "");
9222
+ const markDragSource = (element, axes = "xy") => {
9223
+ element.setAttribute("data-drag-source", axes);
9178
9224
  element.addEventListener("touchmove", keepTouchRefusable, {
9179
9225
  passive: false
9180
9226
  });
@@ -11569,6 +11615,18 @@ const css$1 = /* css */`
11569
11615
  pointer-events: auto;
11570
11616
  }
11571
11617
 
11618
+ /* …and a FINGER reaching for it does not land on it: the pictures of the
11619
+ transition cover the page, so as far as the browser is concerned the touch
11620
+ began on the document root. What a touch may do is decided there and at that
11621
+ moment, so the root says it for as long as the copy can be caught — the pan
11622
+ is ours (nothing should scroll while something is landing), zoom stays the
11623
+ reader's. Half of a pair: without the non-passive listener put down at the
11624
+ same moment (see letCopyBeCaught) every touchmove arrives already
11625
+ non-cancelable and refusing it does nothing. */
11626
+ [data-drag-catchable] {
11627
+ touch-action: pinch-zoom;
11628
+ }
11629
+
11572
11630
  /* Ce qui a été lancé: il continue dans la direction du geste jusqu'à sortir de
11573
11631
  l'écran, et revient par le même chemin si la réponse refuse. */
11574
11632
  [navi-drag-clone-wrapper][data-tossed] {
@@ -12514,7 +12572,15 @@ const startDragToCarryCopy = (event, {
12514
12572
  return dragGesture;
12515
12573
  }, {
12516
12574
  threshold,
12517
- longPress,
12575
+ // A copy caught on its way home is not an ambiguous press: the hand
12576
+ // reached for something moving, and the press was already matched
12577
+ // against the copy's own box before it got here. The wait a finger is
12578
+ // asked for elsewhere tells a scroll from a drag, and there is no scroll
12579
+ // to tell it from — the copy covers that spot from the top layer. Asked
12580
+ // for anyway it cannot even be answered: the wait is about as long as the
12581
+ // journey, so the thing is home before the proof is done, while a mouse
12582
+ // takes it in five pixels.
12583
+ longPress: cloneWrapperCaught ? false : longPress,
12518
12584
  longPressDelay,
12519
12585
  longPressSlop,
12520
12586
  onPressStart,
@@ -12704,6 +12770,17 @@ const letCopyBeCaught = (cloneWrapper, carryAgain) => {
12704
12770
  // gesture holds what it grabbed rather than whatever was behind.
12705
12771
  cloneWrapper.setAttribute("data-catchable", "");
12706
12772
  document.addEventListener("pointerdown", onPointerDown, true);
12773
+ // The touch half of the same reach, said on the root because that is where a
12774
+ // finger pressing through the pictures lands (see the stylesheet). Both go
12775
+ // down before the copy sets off, since what a touch may do is settled when it
12776
+ // begins: put down later, the press is still read, the carry still starts, and
12777
+ // the browser cancels the pointer one move afterwards — a copy that cannot be
12778
+ // caught with a finger and can with a mouse.
12779
+ const root = document.documentElement;
12780
+ root.setAttribute("data-drag-catchable", "");
12781
+ root.addEventListener("touchmove", keepTouchRefusable, {
12782
+ passive: false
12783
+ });
12707
12784
  return {
12708
12785
  settled: async () => {
12709
12786
  // A hand that lets go and presses again while the copy is still there is
@@ -12715,6 +12792,8 @@ const letCopyBeCaught = (cloneWrapper, carryAgain) => {
12715
12792
  }
12716
12793
  cloneWrapper.removeAttribute("data-catchable");
12717
12794
  document.removeEventListener("pointerdown", onPointerDown, true);
12795
+ root.removeAttribute("data-drag-catchable");
12796
+ root.removeEventListener("touchmove", keepTouchRefusable);
12718
12797
  return caught;
12719
12798
  }
12720
12799
  };
@@ -13006,12 +13085,13 @@ const DRAG_FLICK_DISTANCE = 8;
13006
13085
  // way. Let go and it comes back — a wall one can lean on, never walk through.
13007
13086
  const DRAG_RESISTANCE = 0.3;
13008
13087
 
13009
- // What a drag must not start on: something that reads the pointer itself. A
13010
- // button or a link is not in the list — dragging from one travels, and the
13011
- // click it would have made is swallowed on the way out. A drag source is: it
13012
- // answers the same press, and a travel starting there takes the pointer capture
13013
- // away from a gesture already carrying something.
13014
- const DRAG_EXCLUDED_SELECTOR = ["input", "textarea", "select", '[contenteditable=""]', '[contenteditable="true"]', "[data-drag-source]", "[data-drag-handle]", "[data-no-drag-travel]"].join(",");
13088
+ // What a drag must not start on: something that reads the pointer itself, whole,
13089
+ // with no axis left to share. A button or a link is not in the list — dragging
13090
+ // from one travels, and the click it would have made is swallowed on the way
13091
+ // out. A drag SOURCE is not either: it says which way it goes and only takes
13092
+ // that (see DRAG_SOURCE_AXES_ATTRIBUTE) — but a dedicated handle is, being a
13093
+ // place whose only purpose is to be taken hold of, from the first pixel.
13094
+ const DRAG_EXCLUDED_SELECTOR = ["input", "textarea", "select", '[contenteditable=""]', '[contenteditable="true"]', "[data-drag-handle]", "[data-no-drag-travel]"].join(",");
13015
13095
 
13016
13096
  // Which axes a box travels on, one attribute per gesture, said in the DOM by
13017
13097
  // whoever owns the box: it is what a box ABOVE another reads to know the
@@ -13019,6 +13099,12 @@ const DRAG_EXCLUDED_SELECTOR = ["input", "textarea", "select", '[contenteditable
13019
13099
  // from the outside.
13020
13100
  const DRAG_AXES_ATTRIBUTE = "data-travel-by-drag";
13021
13101
  const WHEEL_AXES_ATTRIBUTE = "data-travel-by-wheel";
13102
+ // The same thing said by something that is PICKED UP rather than travelled: a
13103
+ // row taken out of a list, a card carried across a board (see markDragSource).
13104
+ // It holds the pointer from the press exactly as a nested travel does, so it is
13105
+ // read exactly as one — a list reordered along its own line takes the axis it
13106
+ // runs on and leaves the other to whoever is above.
13107
+ const DRAG_SOURCE_AXES_ATTRIBUTE = "data-drag-source";
13022
13108
 
13023
13109
  // A surface the browser paints in the top layer: it is still a DOM descendant
13024
13110
  // of whatever it was written in, and it is nowhere near it on screen — it
@@ -13234,10 +13320,12 @@ const startDragToTravel = (pointerDownEvent, {
13234
13320
  if (!target.closest || target.closest(DRAG_EXCLUDED_SELECTOR)) {
13235
13321
  return null;
13236
13322
  }
13237
- // A box between the finger and this one that travels the same way: the
13238
- // gesture is its, and this one is left with the axes it does not walk — none
13239
- // at all, most of the time, and then there is no gesture here to read.
13240
- const axesLeft = axesLeftBy(axes, target, element, DRAG_AXES_ATTRIBUTE);
13323
+ // A box between the finger and this one that travels the same way, and then
13324
+ // anything between them that is picked up and carried the same way: the
13325
+ // gesture is theirs, and this one is left with the axes none of them walks
13326
+ // none at all, most of the time, and then there is no gesture here to read.
13327
+ const axesLeftByTravels = axesLeftBy(axes, target, element, DRAG_AXES_ATTRIBUTE);
13328
+ const axesLeft = axesLeftByTravels && axesLeftBy(axesLeftByTravels, target, element, DRAG_SOURCE_AXES_ATTRIBUTE);
13241
13329
  if (!axesLeft) {
13242
13330
  return null;
13243
13331
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/dom",
3
- "version": "0.17.19",
3
+ "version": "0.17.20",
4
4
  "type": "module",
5
5
  "description": "DOM utilities for writing frontend code",
6
6
  "repository": {