@jsenv/dom 0.17.21 → 0.17.23

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 +167 -31
  2. package/package.json +1 -1
package/dist/jsenv_dom.js CHANGED
@@ -4295,41 +4295,77 @@ const createPreviousNodeIterator = (fromNode, rootNode, skipRoot = null) => {
4295
4295
  * link or a button that click means "follow me", which is not what the hand
4296
4296
  * asked for: the press was already answered, by the gesture.
4297
4297
  *
4298
- * So it is swallowed, once, in capture on the document before any handler an
4299
- * element may have, and without anyone having to know which element that is.
4298
+ * So it is swallowed, once, before any other listener sees it. Being first is
4299
+ * earned twice, because both orderings matter:
4300
+ *
4301
+ * - on `window`, the first target of the capture phase — a listener anywhere
4302
+ * lower (document included) comes after, no matter when it was registered.
4303
+ * - registered at module load — among listeners on the same target and phase,
4304
+ * registration order decides. A listener added when the gesture ends would
4305
+ * lose to any window-capture listener registered at startup (@jsenv/navi's
4306
+ * link interception is one), so the listener is permanent and merely armed
4307
+ * by each gesture.
4308
+ *
4309
+ * A window-capture listener that this module's evaluation cannot be proven to
4310
+ * precede must not bet on that order: it checks `clickIsSuppressed()` and
4311
+ * stands aside on its own.
4300
4312
  */
4301
4313
 
4314
+ let suppressing = false;
4315
+ let disarmAtNextPress = false;
4316
+
4317
+ const suppressClick = (clickEvent) => {
4318
+ if (!suppressing) {
4319
+ return;
4320
+ }
4321
+ suppressing = false;
4322
+ disarmAtNextPress = false;
4323
+ clickEvent.preventDefault();
4324
+ clickEvent.stopImmediatePropagation();
4325
+ };
4326
+ const onPointerDown = () => {
4327
+ if (disarmAtNextPress) {
4328
+ suppressing = false;
4329
+ disarmAtNextPress = false;
4330
+ }
4331
+ };
4332
+ window.addEventListener("click", suppressClick, { capture: true });
4333
+ window.addEventListener("pointerdown", onPointerDown, { capture: true });
4334
+
4302
4335
  /**
4303
4336
  * Swallows the next click, for a gesture that has just answered the press.
4304
4337
  *
4305
- * @returns {() => void} the gesture is over. The suppressor cannot be taken
4306
- * down with it — the click is dispatched AFTER the pointerup that ends the
4338
+ * @returns {() => void} the gesture is over. The suppression cannot be lifted
4339
+ * with it — the click is dispatched AFTER the pointerup that ends the
4307
4340
  * gesture, so it would be gone one event too early, and the drag would end on
4308
- * the link it started from being followed. It goes once it has swallowed a
4341
+ * the link it started from being followed. It lifts once it has swallowed a
4309
4342
  * click, or at the next press if the gesture produced none: a click is always
4310
- * preceded by a press, so a suppressor that outlives one press can never
4343
+ * preceded by a press, so a suppression that outlives one press can never
4311
4344
  * reach the click of another.
4312
4345
  */
4313
4346
  const suppressClickAfterGesture = () => {
4314
- const suppressClick = (clickEvent) => {
4315
- clickEvent.stopPropagation();
4316
- clickEvent.preventDefault();
4317
- stopSuppressing();
4318
- };
4319
- const stopSuppressing = () => {
4320
- document.removeEventListener("click", suppressClick, { capture: true });
4321
- document.removeEventListener("pointerdown", stopSuppressing, {
4322
- capture: true,
4323
- });
4324
- };
4325
- document.addEventListener("click", suppressClick, { capture: true });
4347
+ suppressing = true;
4348
+ disarmAtNextPress = false;
4326
4349
  return () => {
4327
- document.addEventListener("pointerdown", stopSuppressing, {
4328
- capture: true,
4329
- });
4350
+ disarmAtNextPress = true;
4330
4351
  };
4331
4352
  };
4332
4353
 
4354
+ /**
4355
+ * Whether the click being dispatched is one a gesture left behind — armed by
4356
+ * `suppressClickAfterGesture`, waiting to be swallowed by this module.
4357
+ *
4358
+ * A last resort, not a convenience. The suppressor already swallows the click
4359
+ * before anyone else sees it; the one listener that legitimately needs to ask
4360
+ * is a `click` listener in capture on `window` whose registration cannot be
4361
+ * proven to come after this module's evaluation — that one may run before the
4362
+ * suppressor and must stand aside on its own. Everywhere else (an element,
4363
+ * `document`, the bubble phase) the click never arrives and checking this is
4364
+ * dead code. Reach for it only when you are sure that is your situation and
4365
+ * no other ordering is available.
4366
+ */
4367
+ const clickIsSuppressed = () => suppressing;
4368
+
4333
4369
  /**
4334
4370
  * A press that says something by NOT moving.
4335
4371
  *
@@ -8159,6 +8195,18 @@ const css$5 = /* css */`
8159
8195
  }
8160
8196
  `;
8161
8197
  import.meta.css = [css$5, "@jsenv/dom/src/interaction/drag/drag_gesture.js"];
8198
+
8199
+ /*
8200
+ * Who asked for the capture of a pointer, last. This module is the only place
8201
+ * that ever takes one, so the answer says whether a capture that goes was HANDED
8202
+ * OVER — somebody here took it — or simply LET GO OF by the browser, which does
8203
+ * that on its own more often than the specification suggests, in the middle of a
8204
+ * gesture whose hand is still down and still moving.
8205
+ *
8206
+ * The two must not be answered the same way, and nothing in the event tells them
8207
+ * apart: `lostpointercapture` says the same thing either way.
8208
+ */
8209
+ const captureHolderByPointerId = new Map();
8162
8210
  const createDragGestureController = (options = {}) => {
8163
8211
  const {
8164
8212
  name,
@@ -8786,6 +8834,11 @@ const createDragGestureController = (options = {}) => {
8786
8834
  let captured = false;
8787
8835
  dragGesture.capturePointer = () => {
8788
8836
  captured = true;
8837
+ // Written down before it is taken: this is the only place a capture
8838
+ // is ever taken from, so what this map says is who asked for it
8839
+ // last — which is what tells a hand-over from a capture the browser
8840
+ // dropped on its own (see onCaptureLost).
8841
+ captureHolderByPointerId.set(grabEvent.pointerId, dragGesture);
8789
8842
  target.setPointerCapture(grabEvent.pointerId);
8790
8843
  };
8791
8844
  if (!options?.pointerCaptureDeferred) {
@@ -8833,16 +8886,46 @@ const createDragGestureController = (options = {}) => {
8833
8886
  // above it), and taken as our own it kills the new gesture one
8834
8887
  // millisecond after it started.
8835
8888
  //
8836
- // And when it IS ours, it is a loss, never an end: the ends a gesture
8837
- // has are the pointer going up and the pointer being cancelled, both
8838
- // listened for below. A capture that goes while the pointer is still
8839
- // down was taken by another gesture, or by the element it was held
8840
- // on leaving the document — and what was being carried must go back
8841
- // rather than land wherever the hand happened to be.
8889
+ // And when it IS ours, it is a loss and never an end: the ends a
8890
+ // gesture has are the pointer going up and the pointer being
8891
+ // cancelled, both listened for below. What a loss MEANS is the
8892
+ // question, and the event does not answer it two very different
8893
+ // things arrive as the same one:
8894
+ //
8895
+ // - it was HANDED OVER: another gesture took the pointer, or the
8896
+ // element it was held on left the document. There is nothing to go
8897
+ // on with, and what was being carried must go back rather than land
8898
+ // wherever the hand happened to be.
8899
+ // - it was simply LET GO OF by the browser, with the hand still down
8900
+ // and still moving. It happens, and not rarely: the capture is a
8901
+ // guarantee that events keep coming to one element, and the browser
8902
+ // drops it for reasons of its own that no code here can see. Killing
8903
+ // the gesture for that is dropping an object mid-air — the copy
8904
+ // vanishes, the place the hint had lit up is thrown away, and the
8905
+ // hand is left having done nothing.
8906
+ //
8907
+ // They are told apart by who asked (see captureHolderByPointerId): a
8908
+ // capture nobody here took, on an element still in the document, was
8909
+ // let go of. The gesture does not need it — every move and the release
8910
+ // are read at the WINDOW, not at the element — so it goes on.
8842
8911
  const onCaptureLost = pointerEvent => {
8843
8912
  if (!captured || pointerEvent.target !== target) {
8844
8913
  return;
8845
8914
  }
8915
+ const handedOver = captureHolderByPointerId.get(grabEvent.pointerId) !== dragGesture;
8916
+ if (!handedOver && target.isConnected) {
8917
+ // Nobody took it and the element it was held on is still there:
8918
+ // the browser let the capture go by itself, which it does — a
8919
+ // node moved by a re-render and put straight back, a decision of
8920
+ // its own we are not told the reason for. The hand has not let go
8921
+ // of anything, so neither does the gesture: it is a guarantee that
8922
+ // was lost, not the gesture. Every move and the release are read
8923
+ // at the window (see below), so it goes on without it rather than
8924
+ // dropping what is still being carried — and the drop the hand was
8925
+ // aiming at, which the hint had already lit up, still happens.
8926
+ captured = false;
8927
+ return;
8928
+ }
8846
8929
  onRelease(pointerEvent, {
8847
8930
  cancelled: true
8848
8931
  });
@@ -8902,6 +8985,9 @@ const createDragGestureController = (options = {}) => {
8902
8985
  // that is up no longer exists — the browser has already dropped the
8903
8986
  // capture with it, and asking again throws ("No active pointer with
8904
8987
  // the given id is found") on the most ordinary release there is.
8988
+ if (captureHolderByPointerId.get(grabEvent.pointerId) === dragGesture) {
8989
+ captureHolderByPointerId.delete(grabEvent.pointerId);
8990
+ }
8905
8991
  if (captured && target.hasPointerCapture(grabEvent.pointerId)) {
8906
8992
  target.releasePointerCapture(grabEvent.pointerId);
8907
8993
  }
@@ -11064,7 +11150,10 @@ const roundForConstraints = (value) => {
11064
11150
 
11065
11151
  /**
11066
11152
  * Detects the drop target based on what element is actually under the mouse cursor.
11067
- * Uses document.elementsFromPoint() to respect visual stacking order naturally.
11153
+ * Uses document.elementsFromPoint() to respect visual stacking order naturally,
11154
+ * and falls back on the rectangles alone when the hit test cannot answer — which
11155
+ * is not only "over nothing": during a view transition the browser hands back the
11156
+ * root for every point of the page (see findTargetByGeometry).
11068
11157
  *
11069
11158
  * @param {Object} gestureInfo - Gesture information
11070
11159
  * @param {Element[]} targetElements - Array of potential drop target elements
@@ -11203,8 +11292,20 @@ const getDropTargetInfo = (
11203
11292
  }
11204
11293
  }
11205
11294
  if (!targetElement) {
11206
- targetElement = intersectingTargets[0];
11207
- intersectingIndex = 0;
11295
+ // Nothing in the stack answered. The point may be over no target at all —
11296
+ // and it may also be over one the hit test cannot see: a view transition
11297
+ // covers the page with its pictures, and from then on every point of the
11298
+ // document reads as the root, whatever is really under it. Taking the first
11299
+ // of the overlapped targets then means taking the first one in DOM ORDER,
11300
+ // which has nothing to do with where the hand is: a piece carried onto the
11301
+ // place next door comes back down on the place it left, and the hint says so
11302
+ // by lighting up the wrong one.
11303
+ //
11304
+ // Geometry is what is left, and it is the reading the eye makes anyway: the
11305
+ // place the middle of the carried thing is IN, or — the middle being over a
11306
+ // gap — the one it covers most of.
11307
+ targetElement = findTargetByGeometry(intersectingTargets, dragElementRect);
11308
+ intersectingIndex = intersectingTargets.indexOf(targetElement);
11208
11309
  }
11209
11310
  targetIndex = targetElements.indexOf(targetElement);
11210
11311
 
@@ -11257,6 +11358,41 @@ const getDropTargetInfo = (
11257
11358
  return result;
11258
11359
  };
11259
11360
 
11361
+ /**
11362
+ * Which of the overlapped targets the carried thing is on, said with rectangles
11363
+ * alone: the one holding its centre, or the one it covers the most of. Used when
11364
+ * the hit test cannot answer (see its caller).
11365
+ */
11366
+ const findTargetByGeometry = (targetElements, dragElementRect) => {
11367
+ const dragCenterX = dragElementRect.left + dragElementRect.width / 2;
11368
+ const dragCenterY = dragElementRect.top + dragElementRect.height / 2;
11369
+ let bestElement = null;
11370
+ let bestOverlapArea = -1;
11371
+ for (const targetElement of targetElements) {
11372
+ const targetRect = targetElement.getBoundingClientRect();
11373
+ if (
11374
+ dragCenterX >= targetRect.left &&
11375
+ dragCenterX <= targetRect.right &&
11376
+ dragCenterY >= targetRect.top &&
11377
+ dragCenterY <= targetRect.bottom
11378
+ ) {
11379
+ return targetElement;
11380
+ }
11381
+ const overlapWidth =
11382
+ Math.min(targetRect.right, dragElementRect.right) -
11383
+ Math.max(targetRect.left, dragElementRect.left);
11384
+ const overlapHeight =
11385
+ Math.min(targetRect.bottom, dragElementRect.bottom) -
11386
+ Math.max(targetRect.top, dragElementRect.top);
11387
+ const overlapArea = overlapWidth * overlapHeight;
11388
+ if (overlapArea > bestOverlapArea) {
11389
+ bestOverlapArea = overlapArea;
11390
+ bestElement = targetElement;
11391
+ }
11392
+ }
11393
+ return bestElement;
11394
+ };
11395
+
11260
11396
  const rectangleAreIntersecting = (r1, r2) => {
11261
11397
  return !(
11262
11398
  r2.left > r1.right ||
@@ -19523,4 +19659,4 @@ const useResizeStatus = (elementRef, { as = "number" } = {}) => {
19523
19659
  };
19524
19660
  };
19525
19661
 
19526
- 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, getVirtualKeyboardOverlayHeight, 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, setVirtualKeyboardOverlaysContent, snapToPixel, startDragTo, startDragToResizeGesture, startDragToTravel, stickyAsRelativeCoords, stringifyStyle, subscribeVirtualKeyboardGeometryChange, subscribeVisualViewportResizeSettled, subscribeWindowResizeSettled, suppressClickAfterGesture, trapFocusInside, trapScrollInside, useActiveElement, useAvailableHeight, useAvailableWidth, useMaxHeight, useMaxWidth, useResizeStatus, visibleRectEffect, waitForPressHeld, watchWheelTravel, wheelGestureIsTakenFrom };
19662
+ export { EASING, ELEMENT_SIZE_CHANGE, activeElementSignal, addActiveElementEffect, addAttributeEffect, allowWheelThrough, appendStyles, applyNewPosition, canScroll, captureScrollState, chainEvent, claimWheelGesture, clickIsSuppressed, closestOpenableAncestor, contrastColor, createBackgroundColorTransition, createBackgroundTransition, createBorderRadiusTransition, createBorderTransition, createDragGestureController, createDragToMoveGestureController, createEventGroupLogger, createGroupTransitionController, createHeightTransition, createIterableWeakSet, createOpacityTransition, createPubSub, createStyleController, createTimelineTransition, createTransition, createTranslateXTransition, createValueEffect, createWidthTransition, cubicBezier, dispatchCustomEvent, dispatchInternalCustomEvent, dispatchPublicCustomEvent, dragAfterIntent, elementIsFocusable, elementIsVisibleForFocus, elementIsVisuallyVisible, findAfter, findAncestor, findBefore, findDescendant, findEvent, findFocusDelegateTarget, findFocusable, findSelfOrAncestorFixedPosition, formatEventSideEffect, getAncestorOpenType, getAvailableHeight, getAvailableWidth, getBackground, getBackgroundColor, getBorder, getBorderRadius, getBorderSizes, getContrastRatio, getDefaultStyles, getDragCoordinates, getDropTargetInfo, getElementSignature, getFirstVisuallyVisibleAncestor, getFocusVisibilityInfo, getHeight, getHeightWithoutTransition, getInnerHeight, getInnerWidth, getKeyboardEventDefaultAction, getLuminance, getMarginSizes, getMaxHeight, getMaxWidth, getMinHeight, getMinWidth, getOpacity, getOpacityWithoutTransition, getPaddingSizes, getPositionedParent, getPositioningScrollOffset, getPreferedColorScheme, getScrollBox, getScrollContainer, getScrollContainerSet, getScrollRelativeRect, getSelfAndAncestorScrolls, getStyle, getTranslateX, getTranslateXWithoutTransition, getTranslateY, getVirtualKeyboardOverlayHeight, 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, setVirtualKeyboardOverlaysContent, snapToPixel, startDragTo, startDragToResizeGesture, startDragToTravel, stickyAsRelativeCoords, stringifyStyle, subscribeVirtualKeyboardGeometryChange, subscribeVisualViewportResizeSettled, subscribeWindowResizeSettled, suppressClickAfterGesture, trapFocusInside, trapScrollInside, useActiveElement, useAvailableHeight, useAvailableWidth, useMaxHeight, useMaxWidth, useResizeStatus, visibleRectEffect, waitForPressHeld, watchWheelTravel, wheelGestureIsTakenFrom };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/dom",
3
- "version": "0.17.21",
3
+ "version": "0.17.23",
4
4
  "type": "module",
5
5
  "description": "DOM utilities for writing frontend code",
6
6
  "repository": {