@jsenv/dom 0.17.25 → 0.17.27
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.
- package/dist/jsenv_dom.js +210 -7
- package/package.json +1 -1
package/dist/jsenv_dom.js
CHANGED
|
@@ -7674,11 +7674,187 @@ const getPaddingSizes = (element) => {
|
|
|
7674
7674
|
};
|
|
7675
7675
|
};
|
|
7676
7676
|
|
|
7677
|
+
// A gesture on an element the browser paints in the top layer belongs to that
|
|
7678
|
+
// element, wherever it sits in the DOM.
|
|
7679
|
+
const TOP_LAYER_SELECTOR$1 = [
|
|
7680
|
+
":popover-open",
|
|
7681
|
+
"dialog:modal",
|
|
7682
|
+
":fullscreen",
|
|
7683
|
+
].join(",");
|
|
7684
|
+
|
|
7685
|
+
// The keys that scroll the document when nothing focusable holds them.
|
|
7686
|
+
const SCROLL_KEY_SET = new Set([
|
|
7687
|
+
"ArrowUp",
|
|
7688
|
+
"ArrowDown",
|
|
7689
|
+
"ArrowLeft",
|
|
7690
|
+
"ArrowRight",
|
|
7691
|
+
"PageUp",
|
|
7692
|
+
"PageDown",
|
|
7693
|
+
"Home",
|
|
7694
|
+
"End",
|
|
7695
|
+
" ",
|
|
7696
|
+
]);
|
|
7697
|
+
|
|
7698
|
+
/**
|
|
7699
|
+
* Stops the background from scrolling by cancelling the scroll gestures that
|
|
7700
|
+
* would reach it, leaving every style on the page untouched.
|
|
7701
|
+
*
|
|
7702
|
+
* **Why not overflow: hidden?**
|
|
7703
|
+
* A scroll container that cannot scroll has no sticky offsets: browsers only
|
|
7704
|
+
* apply `position: sticky` constraints to a scroller that can actually scroll.
|
|
7705
|
+
* Locking the document with `overflow: hidden` therefore drops every stuck
|
|
7706
|
+
* element (a sticky header, a sticky table column) back to its flow position —
|
|
7707
|
+
* off screen, since the page stays visually scrolled where it was. Cancelling
|
|
7708
|
+
* gestures keeps the scroller scrollable, so sticky, the scrollbar and the
|
|
7709
|
+
* layout width all stay exactly as they are (no gutter to compensate).
|
|
7710
|
+
*
|
|
7711
|
+
* **What it does not stop**
|
|
7712
|
+
* - dragging the native scrollbar: it sits outside the viewport, no backdrop
|
|
7713
|
+
* covers it and no event of ours is involved;
|
|
7714
|
+
* - programmatic scroll (`scrollTo`, `scrollIntoView`, a focus moving into the
|
|
7715
|
+
* background).
|
|
7716
|
+
* Both are deliberate acts on a background that is covered by a backdrop, hence
|
|
7717
|
+
* the requirement: without something covering the background, lock the overflow
|
|
7718
|
+
* instead (see `scroll_trap.js`).
|
|
7719
|
+
*
|
|
7720
|
+
* @param {HTMLElement} element - The overlay being shown. Gestures landing on
|
|
7721
|
+
* it (or on any other top layer element) are left alone.
|
|
7722
|
+
* @param {Object} [options]
|
|
7723
|
+
* @param {HTMLElement} [options.boundaryElement] - Only block gestures landing
|
|
7724
|
+
* inside this element. For an overlay confined to a local container: the
|
|
7725
|
+
* container must stop scrolling, the rest of the page keeps its gestures.
|
|
7726
|
+
* @param {HTMLElement} [options.backdropElement] - The element covering the
|
|
7727
|
+
* background, when it is one of ours (a modal dialog covers it with its own
|
|
7728
|
+
* `::backdrop` and there is nothing to pass). It is in the top layer like the
|
|
7729
|
+
* overlay itself, yet a gesture on it aims at the background behind it.
|
|
7730
|
+
* @returns {() => void} Cleanup function removing the listeners.
|
|
7731
|
+
*/
|
|
7732
|
+
|
|
7733
|
+
const trapScrollGestureInside = (
|
|
7734
|
+
element,
|
|
7735
|
+
{ boundaryElement, backdropElement } = {},
|
|
7736
|
+
) => {
|
|
7737
|
+
const { ownerDocument } = element;
|
|
7738
|
+
const lockedRegion = boundaryElement || ownerDocument.documentElement;
|
|
7739
|
+
|
|
7740
|
+
const isBackgroundGesture = (target, clientX, clientY) => {
|
|
7741
|
+
if (!lockedRegion.contains(target)) {
|
|
7742
|
+
return false;
|
|
7743
|
+
}
|
|
7744
|
+
if (element.contains(target)) {
|
|
7745
|
+
// A modal <dialog> is the event target for the whole backdrop area too
|
|
7746
|
+
// (::backdrop is not hit-testable), so being the target does not mean the
|
|
7747
|
+
// pointer is on it — the rect decides, same as the backdrop-click
|
|
7748
|
+
// detection in dialog.jsx.
|
|
7749
|
+
if (target !== element) {
|
|
7750
|
+
return false;
|
|
7751
|
+
}
|
|
7752
|
+
const { left, right, top, bottom } = element.getBoundingClientRect();
|
|
7753
|
+
return (
|
|
7754
|
+
clientX < left || clientX > right || clientY < top || clientY > bottom
|
|
7755
|
+
);
|
|
7756
|
+
}
|
|
7757
|
+
if (backdropElement && backdropElement.contains(target)) {
|
|
7758
|
+
return true;
|
|
7759
|
+
}
|
|
7760
|
+
let ancestorOrSelf = target;
|
|
7761
|
+
while (ancestorOrSelf && ancestorOrSelf.nodeType === 1) {
|
|
7762
|
+
if (ancestorOrSelf.matches(TOP_LAYER_SELECTOR$1)) {
|
|
7763
|
+
return false;
|
|
7764
|
+
}
|
|
7765
|
+
ancestorOrSelf = ancestorOrSelf.parentNode;
|
|
7766
|
+
}
|
|
7767
|
+
return true;
|
|
7768
|
+
};
|
|
7769
|
+
|
|
7770
|
+
const onWheel = (wheelEvent) => {
|
|
7771
|
+
if (wheelEvent.ctrlKey) {
|
|
7772
|
+
// Browser zoom, not a scroll — cancelling it would take zoom away from
|
|
7773
|
+
// the page while an overlay is open.
|
|
7774
|
+
return;
|
|
7775
|
+
}
|
|
7776
|
+
if (
|
|
7777
|
+
isBackgroundGesture(
|
|
7778
|
+
wheelEvent.target,
|
|
7779
|
+
wheelEvent.clientX,
|
|
7780
|
+
wheelEvent.clientY,
|
|
7781
|
+
)
|
|
7782
|
+
) {
|
|
7783
|
+
wheelEvent.preventDefault();
|
|
7784
|
+
}
|
|
7785
|
+
};
|
|
7786
|
+
const onTouchMove = (touchMoveEvent) => {
|
|
7787
|
+
const { touches } = touchMoveEvent;
|
|
7788
|
+
if (touches.length !== 1) {
|
|
7789
|
+
return; // pinch
|
|
7790
|
+
}
|
|
7791
|
+
const [touch] = touches;
|
|
7792
|
+
if (
|
|
7793
|
+
isBackgroundGesture(touchMoveEvent.target, touch.clientX, touch.clientY)
|
|
7794
|
+
) {
|
|
7795
|
+
touchMoveEvent.preventDefault();
|
|
7796
|
+
}
|
|
7797
|
+
};
|
|
7798
|
+
const onKeyDown = (keydownEvent) => {
|
|
7799
|
+
if (!SCROLL_KEY_SET.has(keydownEvent.key)) {
|
|
7800
|
+
return;
|
|
7801
|
+
}
|
|
7802
|
+
const { target } = keydownEvent;
|
|
7803
|
+
// Anything else focused (a field, a scrollable box, a button) owns the key
|
|
7804
|
+
// press; only a document with nothing focused scrolls on it.
|
|
7805
|
+
if (
|
|
7806
|
+
target !== ownerDocument.body &&
|
|
7807
|
+
target !== ownerDocument.documentElement
|
|
7808
|
+
) {
|
|
7809
|
+
return;
|
|
7810
|
+
}
|
|
7811
|
+
if (isBackgroundGesture(target)) {
|
|
7812
|
+
keydownEvent.preventDefault();
|
|
7813
|
+
}
|
|
7814
|
+
};
|
|
7815
|
+
|
|
7816
|
+
// Capture phase: the gesture must be cancelled before whatever it landed on
|
|
7817
|
+
// gets a chance to act on it. passive: false is what makes preventDefault
|
|
7818
|
+
// effective at all on wheel/touchmove.
|
|
7819
|
+
const listenerOptions = { capture: true, passive: false };
|
|
7820
|
+
ownerDocument.addEventListener("wheel", onWheel, listenerOptions);
|
|
7821
|
+
ownerDocument.addEventListener("touchmove", onTouchMove, listenerOptions);
|
|
7822
|
+
ownerDocument.addEventListener("keydown", onKeyDown, listenerOptions);
|
|
7823
|
+
return () => {
|
|
7824
|
+
ownerDocument.removeEventListener("wheel", onWheel, listenerOptions);
|
|
7825
|
+
ownerDocument.removeEventListener(
|
|
7826
|
+
"touchmove",
|
|
7827
|
+
onTouchMove,
|
|
7828
|
+
listenerOptions,
|
|
7829
|
+
);
|
|
7830
|
+
ownerDocument.removeEventListener("keydown", onKeyDown, listenerOptions);
|
|
7831
|
+
};
|
|
7832
|
+
};
|
|
7833
|
+
|
|
7834
|
+
// "gesture": cancel the scroll gestures aimed at the background, keeping it
|
|
7835
|
+
// scrollable (see scroll_gesture_trap.js) — only possible when a backdrop
|
|
7836
|
+
// covers it.
|
|
7837
|
+
// "overflow": hide the overflow of every scroll container behind the overlay.
|
|
7838
|
+
// Set this to "overflow" to put every scroll lock on that single strategy.
|
|
7839
|
+
const SCROLL_LOCK_STRATEGY = "gesture";
|
|
7840
|
+
|
|
7677
7841
|
/**
|
|
7678
7842
|
* Prevents scrolling on all scrollable containers that are ancestors of (or
|
|
7679
7843
|
* siblings preceding) `element`. Used when an overlay (popover, dialog) is
|
|
7680
7844
|
* open and background scroll should be disabled.
|
|
7681
7845
|
*
|
|
7846
|
+
* **Two strategies, and why the gesture one comes first**
|
|
7847
|
+
* Hiding the overflow makes the scroller unable to scroll, and a scroller that
|
|
7848
|
+
* cannot scroll has no sticky offsets: every `position: sticky` element behind
|
|
7849
|
+
* the overlay drops back to its flow position — off screen, since the page
|
|
7850
|
+
* stays visually scrolled where it was. So whenever a backdrop covers the
|
|
7851
|
+
* background (`backdrop`), the lock is done by cancelling scroll gestures
|
|
7852
|
+
* (`scroll_gesture_trap.js`): nothing in the page moves, sticky included, and
|
|
7853
|
+
* there is no scrollbar to compensate for. The overflow lock below is what
|
|
7854
|
+
* remains when nothing covers the background — a gesture trap would then let
|
|
7855
|
+
* the user scroll by dragging the scrollbar of a container they can still see
|
|
7856
|
+
* and reach. `SCROLL_LOCK_STRATEGY` forces that overflow lock everywhere.
|
|
7857
|
+
*
|
|
7682
7858
|
* **Why padding instead of scrollbar-gutter?**
|
|
7683
7859
|
* `scrollbar-gutter: stable` would be the modern, CSS-native way to reserve
|
|
7684
7860
|
* the scrollbar lane before hiding overflow so the layout doesn't shift.
|
|
@@ -7700,9 +7876,23 @@ const getPaddingSizes = (element) => {
|
|
|
7700
7876
|
* inside this element (itself included). For an overlay confined to a local
|
|
7701
7877
|
* container rather than the viewport: the container's own scroll must stop,
|
|
7702
7878
|
* the rest of the page keeps scrolling as usual.
|
|
7879
|
+
* @param {true|HTMLElement} [options.backdrop] - What covers the background
|
|
7880
|
+
* while the overlay is open: the backdrop element, or `true` when the browser
|
|
7881
|
+
* paints it itself (a modal dialog's `::backdrop`). Enables the gesture
|
|
7882
|
+
* strategy described above.
|
|
7703
7883
|
* @returns {() => void} Cleanup function that restores all modified styles.
|
|
7704
7884
|
*/
|
|
7705
|
-
const trapScrollInside = (
|
|
7885
|
+
const trapScrollInside = (
|
|
7886
|
+
element,
|
|
7887
|
+
{ boundaryElement, backdrop } = {},
|
|
7888
|
+
) => {
|
|
7889
|
+
if (backdrop && SCROLL_LOCK_STRATEGY === "gesture") {
|
|
7890
|
+
return trapScrollGestureInside(element, {
|
|
7891
|
+
boundaryElement,
|
|
7892
|
+
backdropElement: backdrop === true ? null : backdrop,
|
|
7893
|
+
});
|
|
7894
|
+
}
|
|
7895
|
+
|
|
7706
7896
|
const cleanupCallbackSet = new Set();
|
|
7707
7897
|
|
|
7708
7898
|
// Collect every element to lock first (preceding scrollable siblings + all
|
|
@@ -9287,7 +9477,8 @@ const css$4 = /* css */`
|
|
|
9287
9477
|
not to the gesture, and two fingers are never a drag. */
|
|
9288
9478
|
touch-action: pinch-zoom;
|
|
9289
9479
|
}
|
|
9290
|
-
[data-drag-ignore]
|
|
9480
|
+
[data-drag-ignore],
|
|
9481
|
+
[data-own-target] {
|
|
9291
9482
|
-webkit-touch-callout: default;
|
|
9292
9483
|
touch-action: auto;
|
|
9293
9484
|
}
|
|
@@ -11741,7 +11932,8 @@ const css$1 = /* css */`
|
|
|
11741
11932
|
[data-drag-source] {
|
|
11742
11933
|
cursor: default;
|
|
11743
11934
|
}
|
|
11744
|
-
[data-drag-ignore]
|
|
11935
|
+
[data-drag-ignore],
|
|
11936
|
+
[data-own-target] {
|
|
11745
11937
|
cursor: auto;
|
|
11746
11938
|
}
|
|
11747
11939
|
|
|
@@ -11836,6 +12028,14 @@ const css$1 = /* css */`
|
|
|
11836
12028
|
// can start a drag, and they have to be true BEFORE anyone drags anything.
|
|
11837
12029
|
import.meta.css = [css$1, "@jsenv/dom/src/interaction/drag/drag_to.js"];
|
|
11838
12030
|
|
|
12031
|
+
// What a press must not be read from at all. `data-drag-ignore` is said by
|
|
12032
|
+
// something whose press is its own business — a text one wants to select, a
|
|
12033
|
+
// control that reads the pointer itself. `data-own-target` is the same fact said
|
|
12034
|
+
// once for every gesture there is: an element declaring that a press landing on
|
|
12035
|
+
// it is aimed AT it, whatever it happens to sit inside (see also
|
|
12036
|
+
// DRAG_EXCLUDED_SELECTOR in drag_to_travel.js).
|
|
12037
|
+
const DRAG_IGNORED_SELECTOR = "[data-drag-ignore],[data-own-target]";
|
|
12038
|
+
|
|
11839
12039
|
/**
|
|
11840
12040
|
* Starts a drag-to-reorder interaction on a list item.
|
|
11841
12041
|
*
|
|
@@ -12335,7 +12535,7 @@ const startDragTo = (event, effects, {
|
|
|
12335
12535
|
} = {}) => {
|
|
12336
12536
|
// An area that opted out of dragging (a text one wants to select, a control that
|
|
12337
12537
|
// owns the gesture): the press there is none of our business.
|
|
12338
|
-
if (event.target.closest && event.target.closest(
|
|
12538
|
+
if (event.target.closest && event.target.closest(DRAG_IGNORED_SELECTOR)) {
|
|
12339
12539
|
return undefined;
|
|
12340
12540
|
}
|
|
12341
12541
|
// A secondary button (right click and friends) is a context menu, not a grab.
|
|
@@ -12517,7 +12717,7 @@ const startDragToCarryCopy = (event, {
|
|
|
12517
12717
|
}) => {
|
|
12518
12718
|
// An area that opted out of dragging (a text one wants to select, a control
|
|
12519
12719
|
// that owns the gesture): the press there is none of our business.
|
|
12520
|
-
if (event.target.closest && event.target.closest(
|
|
12720
|
+
if (event.target.closest && event.target.closest(DRAG_IGNORED_SELECTOR)) {
|
|
12521
12721
|
return undefined;
|
|
12522
12722
|
}
|
|
12523
12723
|
// A secondary button (right click and friends) is a context menu, not a grab.
|
|
@@ -13262,8 +13462,11 @@ const DRAG_RESISTANCE = 0.3;
|
|
|
13262
13462
|
// from one travels, and the click it would have made is swallowed on the way
|
|
13263
13463
|
// out. A drag SOURCE is not either: it says which way it goes and only takes
|
|
13264
13464
|
// that (see DRAG_SOURCE_AXES_ATTRIBUTE) — but a dedicated handle is, being a
|
|
13265
|
-
// place whose only purpose is to be taken hold of, from the first pixel.
|
|
13266
|
-
|
|
13465
|
+
// place whose only purpose is to be taken hold of, from the first pixel. And so
|
|
13466
|
+
// is an OWN TARGET: an element saying a press landing on it is aimed at IT,
|
|
13467
|
+
// which is the same sentence said to every gesture at once rather than to this
|
|
13468
|
+
// one (see DRAG_IGNORED_SELECTOR in drag_to.js).
|
|
13469
|
+
const DRAG_EXCLUDED_SELECTOR = ["input", "textarea", "select", '[contenteditable=""]', '[contenteditable="true"]', "[data-drag-handle]", "[data-no-drag-travel]", "[data-own-target]"].join(",");
|
|
13267
13470
|
|
|
13268
13471
|
// Which axes a box travels on, one attribute per gesture, said in the DOM by
|
|
13269
13472
|
// whoever owns the box: it is what a box ABOVE another reads to know the
|