@jsenv/dom 0.17.5 → 0.17.6
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 +2450 -1244
- package/package.json +1 -1
package/dist/jsenv_dom.js
CHANGED
|
@@ -7628,1316 +7628,1682 @@ const applyWheelScrollThrough = (element, wheelEvent) => {
|
|
|
7628
7628
|
});
|
|
7629
7629
|
};
|
|
7630
7630
|
|
|
7631
|
-
|
|
7632
|
-
|
|
7633
|
-
* to: its own nearest positioned ancestor (walking up the DOM tree), or
|
|
7634
|
-
* `document.documentElement` (the viewport) if none is found.
|
|
7635
|
-
*
|
|
7636
|
-
* Also aware of `element` itself being promoted to the top layer: a
|
|
7637
|
-
* `<dialog>` actually shown modally (`showModal()`, matches `:modal` — a
|
|
7638
|
-
* `.show()`'d, non-modal dialog does NOT match and is positioned like any
|
|
7639
|
-
* other in-flow element instead, walked up normally below), or *any*
|
|
7640
|
-
* `[popover]` element, always uses the initial containing block (the
|
|
7641
|
-
* viewport) regardless of its own `position` or DOM ancestry — walking up
|
|
7642
|
-
* its own parent chain (what the rest of this function does) would give
|
|
7643
|
-
* the wrong answer for these two specifically, since their real DOM
|
|
7644
|
-
* position becomes irrelevant to their own containing block the moment
|
|
7645
|
-
* they're actually promoted. Checked via the `popover` attribute itself,
|
|
7646
|
-
* not the live `:popover-open` state — unlike `<dialog>`, a `[popover]`
|
|
7647
|
-
* element has no "local" mode: it's always top-layer-bound once shown,
|
|
7648
|
-
* regardless of whether it happens to be open right this moment, so the
|
|
7649
|
-
* static attribute alone is enough (and correct even when called just
|
|
7650
|
-
* before `showPopover()` actually runs, when `:popover-open` isn't true
|
|
7651
|
-
* yet).
|
|
7652
|
-
*
|
|
7653
|
-
* `document.documentElement` (not `document.body`, not `null`) is this
|
|
7654
|
-
* function's own "no real container — use the viewport" sentinel:
|
|
7655
|
-
* `documentElement` is the actual initial containing block, so the walk
|
|
7656
|
-
* below stops there without testing its own `position` (there's nothing
|
|
7657
|
-
* beyond it to fall back to anyway) — unlike the previous version of this
|
|
7658
|
-
* function, which stopped one level too early, at `document.body`, without
|
|
7659
|
-
* ever testing *its* `position` either (a `position: relative` body, for
|
|
7660
|
-
* instance, would have been silently skipped). Returning `documentElement`
|
|
7661
|
-
* instead of `null` also means no special-casing is needed by callers that
|
|
7662
|
-
* already compare a resolved container against `document.documentElement`
|
|
7663
|
-
* (see e.g. visible_rect.js's own `hasRealContainer` check).
|
|
7664
|
-
*/
|
|
7665
|
-
const getPositionedParent = (element) => {
|
|
7666
|
-
const isPromotedToTopLayer =
|
|
7667
|
-
(element.tagName === "DIALOG" && element.matches(":modal")) ||
|
|
7668
|
-
element.hasAttribute("popover");
|
|
7669
|
-
if (isPromotedToTopLayer) {
|
|
7670
|
-
return document.documentElement;
|
|
7671
|
-
}
|
|
7672
|
-
let parent = element.parentElement;
|
|
7673
|
-
while (parent && parent !== document.documentElement) {
|
|
7674
|
-
const position = window.getComputedStyle(parent).position;
|
|
7675
|
-
if (
|
|
7676
|
-
position === "relative" ||
|
|
7677
|
-
position === "absolute" ||
|
|
7678
|
-
position === "fixed"
|
|
7679
|
-
) {
|
|
7680
|
-
return parent;
|
|
7681
|
-
}
|
|
7682
|
-
parent = parent.parentElement;
|
|
7683
|
-
}
|
|
7684
|
-
return document.documentElement;
|
|
7685
|
-
};
|
|
7631
|
+
const installImportMetaCssBuild = (importMeta) => {
|
|
7632
|
+
const IMPORT_META_CSS_BUILD = "jsenv_import_meta_css_build";
|
|
7686
7633
|
|
|
7687
|
-
|
|
7688
|
-
|
|
7689
|
-
* `document.documentElement`) looking for the first one whose *computed*
|
|
7690
|
-
* `position` is `fixed` — i.e. pinned to the viewport, ignoring document
|
|
7691
|
-
* scroll, regardless of what `element` itself is positioned relative to.
|
|
7692
|
-
*
|
|
7693
|
-
* @param {Element} element
|
|
7694
|
-
* @returns {[left: number, top: number] | null} The fixed ancestor's own
|
|
7695
|
-
* viewport-relative `getBoundingClientRect()` origin, or `null` if neither
|
|
7696
|
-
* `element` nor any ancestor is fixed (i.e. `element` genuinely scrolls
|
|
7697
|
-
* with the document).
|
|
7698
|
-
*/
|
|
7699
|
-
const findSelfOrAncestorFixedPosition = (element) => {
|
|
7700
|
-
let current = element;
|
|
7701
|
-
while (true) {
|
|
7702
|
-
const computedStyle = window.getComputedStyle(current);
|
|
7703
|
-
if (computedStyle.position === "fixed") {
|
|
7704
|
-
const { left, top } = current.getBoundingClientRect();
|
|
7705
|
-
return [left, top];
|
|
7706
|
-
}
|
|
7707
|
-
current = current.parentElement;
|
|
7708
|
-
if (!current || current === document.documentElement) {
|
|
7709
|
-
break;
|
|
7710
|
-
}
|
|
7634
|
+
if (importMeta.css === IMPORT_META_CSS_BUILD) {
|
|
7635
|
+
return;
|
|
7711
7636
|
}
|
|
7712
|
-
|
|
7637
|
+
|
|
7638
|
+
const stylesheetMap = new Map();
|
|
7639
|
+
const adopt = (url, value) => {
|
|
7640
|
+
const stylesheet = new CSSStyleSheet({ baseUrl: importMeta.url });
|
|
7641
|
+
stylesheet.replaceSync(value);
|
|
7642
|
+
stylesheetMap.set(url, stylesheet);
|
|
7643
|
+
document.adoptedStyleSheets = [...document.adoptedStyleSheets, stylesheet];
|
|
7644
|
+
};
|
|
7645
|
+
const update = (url, value) => {
|
|
7646
|
+
stylesheetMap.get(url).replaceSync(value);
|
|
7647
|
+
};
|
|
7648
|
+
const remove = (url) => {
|
|
7649
|
+
const stylesheet = stylesheetMap.get(url);
|
|
7650
|
+
document.adoptedStyleSheets = document.adoptedStyleSheets.filter(
|
|
7651
|
+
(s) => s !== stylesheet,
|
|
7652
|
+
);
|
|
7653
|
+
stylesheetMap.delete(url);
|
|
7654
|
+
};
|
|
7655
|
+
|
|
7656
|
+
const currentCssSourceMap = new Map();
|
|
7657
|
+
Object.defineProperty(importMeta, "css", {
|
|
7658
|
+
configurable: true,
|
|
7659
|
+
get() {
|
|
7660
|
+
return IMPORT_META_CSS_BUILD;
|
|
7661
|
+
},
|
|
7662
|
+
set([value, url]) {
|
|
7663
|
+
if (value === undefined) {
|
|
7664
|
+
if (stylesheetMap.has(url)) {
|
|
7665
|
+
remove(url);
|
|
7666
|
+
currentCssSourceMap.delete(url);
|
|
7667
|
+
}
|
|
7668
|
+
return;
|
|
7669
|
+
}
|
|
7670
|
+
if (!stylesheetMap.has(url)) {
|
|
7671
|
+
adopt(url, value);
|
|
7672
|
+
currentCssSourceMap.set(url, value);
|
|
7673
|
+
} else if (currentCssSourceMap.get(url) !== value) {
|
|
7674
|
+
update(url, value);
|
|
7675
|
+
currentCssSourceMap.set(url, value);
|
|
7676
|
+
}
|
|
7677
|
+
},
|
|
7678
|
+
});
|
|
7713
7679
|
};
|
|
7714
7680
|
|
|
7715
7681
|
/**
|
|
7716
|
-
*
|
|
7717
|
-
*
|
|
7718
|
-
* PURPOSE:
|
|
7719
|
-
* During a drag gesture, the system tracks mouse movement as "scrollable coordinates"
|
|
7720
|
-
* relative to the scroll container. This function converts those coordinates into
|
|
7721
|
-
* the actual CSS transform values needed to visually move an element (or a separate
|
|
7722
|
-
* elementToMove) to follow the mouse.
|
|
7723
|
-
*
|
|
7724
|
-
* PARAMETERS:
|
|
7725
|
-
* - element: The element being grabbed / tracked for drag detection and auto-scroll.
|
|
7726
|
-
* - referenceElement: Optional. The element whose coordinate system defines the input space.
|
|
7727
|
-
* When provided, scrollable coords are relative to its scroll container.
|
|
7728
|
-
* Defaults to element itself.
|
|
7729
|
-
* - elementToMove: Optional. A different element to apply the transform to (e.g. a clone
|
|
7730
|
-
* or a table that moves as a whole when a column is dragged).
|
|
7731
|
-
* When provided, its offsetParent is used as the positioning context.
|
|
7732
|
-
*
|
|
7733
|
-
* THE COORDINATE PIPELINE:
|
|
7734
|
-
*
|
|
7735
|
-
* Mouse position
|
|
7736
|
-
* → scrollable coords (relative to referenceScrollContainer, scroll-independent)
|
|
7737
|
-
* → positioned coords (relative to elementToMove's offsetParent, for CSS transform)
|
|
7738
|
-
*
|
|
7739
|
-
* Two types of offsets bridge these spaces:
|
|
7682
|
+
* Isolates user interactions to only the specified elements, making everything else non-interactive.
|
|
7740
7683
|
*
|
|
7741
|
-
*
|
|
7742
|
-
*
|
|
7743
|
-
*
|
|
7744
|
-
*
|
|
7745
|
-
* This offset is the spatial difference between those two positioned ancestors.
|
|
7746
|
-
* Called dynamically because parents can move (e.g. overlay elements).
|
|
7684
|
+
* This creates a controlled interaction environment where only the target elements (and their ancestors)
|
|
7685
|
+
* can receive user input like clicks, keyboard events, focus, etc. All other DOM elements become
|
|
7686
|
+
* non-interactive, preventing conflicting or unwanted interactions during critical operations
|
|
7687
|
+
* like drag gestures, modal dialogs, or complex UI states.
|
|
7747
7688
|
*
|
|
7748
|
-
*
|
|
7749
|
-
*
|
|
7750
|
-
* The math ensures that at grab time, the transform delta is zero (element
|
|
7751
|
-
* stays at its visual position), and subsequent mouse movement maps 1:1
|
|
7752
|
-
* to transform change.
|
|
7689
|
+
* The function uses the `inert` attribute to achieve this isolation, applying it strategically
|
|
7690
|
+
* to parts of the DOM tree while preserving the interactive elements and their ancestor chains.
|
|
7753
7691
|
*
|
|
7754
|
-
*
|
|
7755
|
-
* When elementToMove's offsetParent is NOT inside the referenceScrollContainer
|
|
7756
|
-
* (e.g. a clone appended to document.body while tracking an element inside
|
|
7757
|
-
* an overflow:auto div), the scroll offset must be FROZEN at grab time.
|
|
7758
|
-
* Using a live scroll value would double-move the clone during auto-scroll:
|
|
7759
|
-
* the scrollable coordinate decreases (element appears to move up) AND the
|
|
7760
|
-
* live scroll value increases — both applied to the same transform.
|
|
7761
|
-
* Freezing the scroll at grab time cancels this out while still correctly
|
|
7762
|
-
* placing the clone at the right initial position.
|
|
7692
|
+
* Example DOM structure and inert application:
|
|
7763
7693
|
*
|
|
7764
|
-
*
|
|
7765
|
-
*
|
|
7766
|
-
*
|
|
7767
|
-
*
|
|
7768
|
-
*
|
|
7769
|
-
*
|
|
7770
|
-
*
|
|
7771
|
-
*
|
|
7694
|
+
* Before calling isolateInteractions:
|
|
7695
|
+
* ```
|
|
7696
|
+
* <body>
|
|
7697
|
+
* <header>...</header>
|
|
7698
|
+
* <main>
|
|
7699
|
+
* <div>
|
|
7700
|
+
* <span>some content</span>
|
|
7701
|
+
* <div class="modal">modal content</div>
|
|
7702
|
+
* <span>more content</span>
|
|
7703
|
+
* </div>
|
|
7704
|
+
* <aside inert>already inert</aside>
|
|
7705
|
+
* <div class="dropdown">dropdown menu</div>
|
|
7706
|
+
* </main>
|
|
7707
|
+
* <footer>...</footer>
|
|
7708
|
+
* </body>
|
|
7709
|
+
* ```
|
|
7772
7710
|
*
|
|
7773
|
-
*
|
|
7774
|
-
*
|
|
7711
|
+
* After calling isolateInteractions([modal, dropdown]):
|
|
7712
|
+
* ```
|
|
7713
|
+
* <body>
|
|
7714
|
+
* <header inert>...</header> ← made inert (no active descendants)
|
|
7715
|
+
* <main> ← not inert because it contains active elements
|
|
7716
|
+
* <div> ← not inert because it contains .modal
|
|
7717
|
+
* <span inert>some content</span> ← made inert selectively
|
|
7718
|
+
* <div class="modal">modal content</div> ← stays active
|
|
7719
|
+
* <span inert>more content</span> ← made inert selectively
|
|
7720
|
+
* </div>
|
|
7721
|
+
* <aside inert>already inert</aside>
|
|
7722
|
+
* <div class="dropdown">dropdown menu</div> ← stays active
|
|
7723
|
+
* </main>
|
|
7724
|
+
* <footer inert>...</footer>
|
|
7725
|
+
* </body>
|
|
7726
|
+
* ```
|
|
7775
7727
|
*
|
|
7776
|
-
*
|
|
7777
|
-
*
|
|
7778
|
-
*
|
|
7728
|
+
* After calling cleanup():
|
|
7729
|
+
* ```
|
|
7730
|
+
* <body>
|
|
7731
|
+
* <header>...</header>
|
|
7732
|
+
* <main>
|
|
7733
|
+
* <div>
|
|
7734
|
+
* <span>some content</span>
|
|
7735
|
+
* <div class="modal">modal content</div>
|
|
7736
|
+
* <span>more content</span>
|
|
7737
|
+
* </div>
|
|
7738
|
+
* <aside inert>already inert</aside> ← [inert] preserved
|
|
7739
|
+
* <div class="dropdown">dropdown menu</div>
|
|
7740
|
+
* </main>
|
|
7741
|
+
* <footer>...</footer>
|
|
7742
|
+
* </body>
|
|
7743
|
+
* ```
|
|
7779
7744
|
*
|
|
7780
|
-
* -
|
|
7781
|
-
*
|
|
7782
|
-
* coordinate suitable for CSS transform. The gesture system computes:
|
|
7783
|
-
* topDelta = convertScrollablePosition(layout.scrollableTop) - topAtGrab
|
|
7784
|
-
* and applies that as translateY. At grab time, delta = 0. As the mouse moves,
|
|
7785
|
-
* delta tracks the movement exactly, regardless of scroll context differences.
|
|
7745
|
+
* @param {Array<Element>} elements - Array of elements to keep interactive (non-inert)
|
|
7746
|
+
* @returns {Function} cleanup - Function to restore original inert states
|
|
7786
7747
|
*/
|
|
7787
|
-
const
|
|
7788
|
-
|
|
7789
|
-
|
|
7790
|
-
|
|
7791
|
-
)
|
|
7792
|
-
|
|
7793
|
-
|
|
7794
|
-
|
|
7795
|
-
|
|
7796
|
-
// getPositionedParent, not raw .offsetParent — offsetParent is null for a
|
|
7797
|
-
// position: fixed element, and also for one promoted to the top layer
|
|
7798
|
-
// (e.g. a <dialog>/[popover] being dragged by its own handle), which
|
|
7799
|
-
// crashes the fixed-position lookup below (findSelfOrAncestorFixedPosition
|
|
7800
|
-
// assumes a real starting element, not null). getPositionedParent never
|
|
7801
|
-
// returns null (document.documentElement instead — see its own doc).
|
|
7802
|
-
const positionedParent = getPositionedParent(elementToMove || element);
|
|
7803
|
-
const scrollContainer = getScrollContainer(element);
|
|
7804
|
-
const [getPositionOffsets, getScrollOffsets] = createGetOffsets({
|
|
7805
|
-
positionedParent,
|
|
7806
|
-
referencePositionedParent: referenceElement
|
|
7807
|
-
? getPositionedParent(referenceElement)
|
|
7808
|
-
: positionedParent,
|
|
7809
|
-
scrollContainer,
|
|
7810
|
-
referenceScrollContainer: referenceElement
|
|
7811
|
-
? getScrollContainer(referenceElement)
|
|
7812
|
-
: scrollContainer,
|
|
7813
|
-
});
|
|
7748
|
+
const isolateInteractions = (elements) => {
|
|
7749
|
+
const cleanupCallbackSet = new Set();
|
|
7750
|
+
const cleanup = () => {
|
|
7751
|
+
for (const cleanupCallback of cleanupCallbackSet) {
|
|
7752
|
+
cleanupCallback();
|
|
7753
|
+
}
|
|
7754
|
+
cleanupCallbackSet.clear();
|
|
7755
|
+
};
|
|
7814
7756
|
|
|
7815
|
-
|
|
7816
|
-
|
|
7817
|
-
|
|
7818
|
-
|
|
7819
|
-
|
|
7820
|
-
const
|
|
7821
|
-
|
|
7822
|
-
|
|
7823
|
-
|
|
7824
|
-
|
|
7825
|
-
|
|
7826
|
-
scrollableLeftToConvert,
|
|
7827
|
-
scrollableTopToConvert,
|
|
7828
|
-
) => {
|
|
7829
|
-
const [positionOffsetLeft, positionOffsetTop] = getPositionOffsets();
|
|
7830
|
-
const [scrollOffsetLeft, scrollOffsetTop] = getScrollOffsets();
|
|
7757
|
+
const toKeepInteractiveSet = new Set();
|
|
7758
|
+
const keepSelfAndAncestors = (el) => {
|
|
7759
|
+
if (toKeepInteractiveSet.has(el)) {
|
|
7760
|
+
return;
|
|
7761
|
+
}
|
|
7762
|
+
const associatedElements = getAssociatedElements(el);
|
|
7763
|
+
if (associatedElements) {
|
|
7764
|
+
for (const associatedElement of associatedElements) {
|
|
7765
|
+
keepSelfAndAncestors(associatedElement);
|
|
7766
|
+
}
|
|
7767
|
+
}
|
|
7831
7768
|
|
|
7832
|
-
|
|
7833
|
-
|
|
7834
|
-
|
|
7835
|
-
|
|
7836
|
-
|
|
7837
|
-
|
|
7769
|
+
// Add the element itself
|
|
7770
|
+
toKeepInteractiveSet.add(el);
|
|
7771
|
+
// Add all its ancestors up to document.body
|
|
7772
|
+
let ancestor = el.parentNode;
|
|
7773
|
+
while (ancestor && ancestor !== document.body) {
|
|
7774
|
+
toKeepInteractiveSet.add(ancestor);
|
|
7775
|
+
ancestor = ancestor.parentNode;
|
|
7776
|
+
}
|
|
7777
|
+
};
|
|
7838
7778
|
|
|
7839
|
-
|
|
7840
|
-
|
|
7779
|
+
// Build set of elements to keep interactive
|
|
7780
|
+
for (const element of elements) {
|
|
7781
|
+
keepSelfAndAncestors(element);
|
|
7841
7782
|
}
|
|
7842
|
-
|
|
7843
|
-
|
|
7844
|
-
|
|
7845
|
-
const
|
|
7846
|
-
|
|
7847
|
-
element.getBoundingClientRect();
|
|
7848
|
-
const scrollContainerIsDocument = scrollContainer === documentElement;
|
|
7849
|
-
if (scrollContainerIsDocument) {
|
|
7850
|
-
return [elementViewportLeft, elementViewportTop];
|
|
7783
|
+
// backdrop elements are meant to control interactions happening at document level
|
|
7784
|
+
// and should stay interactive
|
|
7785
|
+
const backdropElements = document.querySelectorAll("[data-backdrop]");
|
|
7786
|
+
for (const backdropElement of backdropElements) {
|
|
7787
|
+
keepSelfAndAncestors(backdropElement);
|
|
7851
7788
|
}
|
|
7852
|
-
const { left: scrollContainerLeft, top: scrollContainerTop } =
|
|
7853
|
-
scrollContainer.getBoundingClientRect();
|
|
7854
|
-
const scrollableLeft = elementViewportLeft - scrollContainerLeft;
|
|
7855
|
-
const scrollableTop = elementViewportTop - scrollContainerTop;
|
|
7856
|
-
|
|
7857
|
-
return [scrollableLeft, scrollableTop];
|
|
7858
|
-
};
|
|
7859
7789
|
|
|
7860
|
-
const
|
|
7861
|
-
|
|
7862
|
-
|
|
7863
|
-
|
|
7864
|
-
|
|
7865
|
-
|
|
7866
|
-
|
|
7867
|
-
|
|
7868
|
-
|
|
7869
|
-
|
|
7870
|
-
|
|
7871
|
-
|
|
7872
|
-
);
|
|
7790
|
+
const setInert = (el) => {
|
|
7791
|
+
if (toKeepInteractiveSet.has(el)) {
|
|
7792
|
+
// element should stay interactive
|
|
7793
|
+
return;
|
|
7794
|
+
}
|
|
7795
|
+
const restoreAttributes = setAttributes(el, {
|
|
7796
|
+
inert: "",
|
|
7797
|
+
});
|
|
7798
|
+
cleanupCallbackSet.add(() => {
|
|
7799
|
+
restoreAttributes();
|
|
7800
|
+
});
|
|
7801
|
+
};
|
|
7873
7802
|
|
|
7874
|
-
|
|
7875
|
-
|
|
7876
|
-
|
|
7803
|
+
const makeElementInertSelectivelyOrCompletely = (el) => {
|
|
7804
|
+
// If this element should stay interactive, keep it active
|
|
7805
|
+
if (toKeepInteractiveSet.has(el)) {
|
|
7806
|
+
return;
|
|
7807
|
+
}
|
|
7877
7808
|
|
|
7878
|
-
|
|
7879
|
-
|
|
7880
|
-
|
|
7881
|
-
|
|
7882
|
-
|
|
7883
|
-
|
|
7884
|
-
if (isOverlayOf(positionedParent, referencePositionedParent)) {
|
|
7885
|
-
return createGetOffsetsForOverlay(
|
|
7886
|
-
positionedParent,
|
|
7887
|
-
referencePositionedParent,
|
|
7888
|
-
{
|
|
7889
|
-
scrollContainer,
|
|
7890
|
-
referenceScrollContainer,
|
|
7891
|
-
getScrollOffsets,
|
|
7892
|
-
},
|
|
7893
|
-
);
|
|
7894
|
-
}
|
|
7895
|
-
if (isOverlayOf(referencePositionedParent, positionedParent)) {
|
|
7896
|
-
return createGetOffsetsForOverlay(
|
|
7897
|
-
referencePositionedParent,
|
|
7898
|
-
positionedParent,
|
|
7899
|
-
{
|
|
7900
|
-
scrollContainer,
|
|
7901
|
-
referenceScrollContainer,
|
|
7902
|
-
getScrollOffsets,
|
|
7903
|
-
},
|
|
7809
|
+
// Since we put all ancestors in toKeepInteractiveSet, if this element
|
|
7810
|
+
// is not in the set, we can check if any of its direct children are.
|
|
7811
|
+
// If none of the direct children are in the set, then no descendants are either.
|
|
7812
|
+
const children = Array.from(el.children);
|
|
7813
|
+
const hasInteractiveChildren = children.some((child) =>
|
|
7814
|
+
toKeepInteractiveSet.has(child),
|
|
7904
7815
|
);
|
|
7905
|
-
}
|
|
7906
|
-
const scrollContainerIsDocument = scrollContainer === documentElement;
|
|
7907
|
-
if (scrollContainerIsDocument) {
|
|
7908
|
-
// Document case: getBoundingClientRect already includes document scroll effects
|
|
7909
|
-
// Add current scroll position to get the static offset
|
|
7910
|
-
const getPositionOffsetsDocumentScrolling = () => {
|
|
7911
|
-
const { scrollLeft: documentScrollLeft, scrollTop: documentScrollTop } =
|
|
7912
|
-
scrollContainer;
|
|
7913
|
-
const aRect = positionedParent.getBoundingClientRect();
|
|
7914
|
-
const bRect = referencePositionedParent.getBoundingClientRect();
|
|
7915
|
-
const aLeft = aRect.left;
|
|
7916
|
-
const aTop = aRect.top;
|
|
7917
|
-
const bLeft = bRect.left;
|
|
7918
|
-
const bTop = bRect.top;
|
|
7919
|
-
const aLeftDocument = documentScrollLeft + aLeft;
|
|
7920
|
-
const aTopDocument = documentScrollTop + aTop;
|
|
7921
|
-
const bLeftDocument = documentScrollLeft + bLeft;
|
|
7922
|
-
const bTopDocument = documentScrollTop + bTop;
|
|
7923
|
-
const offsetLeft = bLeftDocument - aLeftDocument;
|
|
7924
|
-
const offsetTop = bTopDocument - aTopDocument;
|
|
7925
|
-
return [offsetLeft, offsetTop];
|
|
7926
|
-
};
|
|
7927
|
-
return [getPositionOffsetsDocumentScrolling, getScrollOffsets];
|
|
7928
|
-
}
|
|
7929
|
-
// Custom scroll container case: account for container's position and scroll
|
|
7930
|
-
const getPositionOffsetsCustomScrollContainer = () => {
|
|
7931
|
-
const aRect = positionedParent.getBoundingClientRect();
|
|
7932
|
-
const bRect = referencePositionedParent.getBoundingClientRect();
|
|
7933
|
-
const aLeft = aRect.left;
|
|
7934
|
-
const aTop = aRect.top;
|
|
7935
|
-
const bLeft = bRect.left;
|
|
7936
|
-
const bTop = bRect.top;
|
|
7937
7816
|
|
|
7938
|
-
|
|
7939
|
-
|
|
7940
|
-
|
|
7941
|
-
|
|
7942
|
-
|
|
7943
|
-
return [offsetLeft, offsetTop];
|
|
7944
|
-
};
|
|
7945
|
-
return [getPositionOffsetsCustomScrollContainer, getScrollOffsets];
|
|
7946
|
-
};
|
|
7947
|
-
const createGetOffsetsForOverlay = (
|
|
7948
|
-
overlay,
|
|
7949
|
-
overlayTarget,
|
|
7950
|
-
{ scrollContainer, referenceScrollContainer, getScrollOffsets },
|
|
7951
|
-
) => {
|
|
7952
|
-
const sameScrollContainer = scrollContainer === referenceScrollContainer;
|
|
7953
|
-
const scrollContainerIsDocument =
|
|
7954
|
-
scrollContainer === document.documentElement;
|
|
7955
|
-
const referenceScrollContainerIsDocument =
|
|
7956
|
-
referenceScrollContainer === documentElement;
|
|
7817
|
+
if (!hasInteractiveChildren) {
|
|
7818
|
+
// No interactive descendants, make the entire element inert
|
|
7819
|
+
setInert(el);
|
|
7820
|
+
return;
|
|
7821
|
+
}
|
|
7957
7822
|
|
|
7958
|
-
|
|
7959
|
-
|
|
7960
|
-
|
|
7961
|
-
return [0, 0];
|
|
7962
|
-
};
|
|
7963
|
-
return [getPositionOffsetsFixedOverlay, getScrollOffsets];
|
|
7823
|
+
// Some children need to stay interactive, process them selectively
|
|
7824
|
+
for (const child of children) {
|
|
7825
|
+
makeElementInertSelectivelyOrCompletely(child);
|
|
7964
7826
|
}
|
|
7965
|
-
|
|
7966
|
-
|
|
7967
|
-
|
|
7968
|
-
|
|
7969
|
-
|
|
7970
|
-
|
|
7971
|
-
let offsetTopBetweenScrollContainers =
|
|
7972
|
-
referenceScrollContainerRect.top - scrollContainerRect.top;
|
|
7973
|
-
if (scrollContainerIsDocument) {
|
|
7974
|
-
offsetLeftBetweenScrollContainers -= scrollContainer.scrollLeft;
|
|
7975
|
-
offsetTopBetweenScrollContainers -= scrollContainer.scrollTop;
|
|
7976
|
-
}
|
|
7977
|
-
return [
|
|
7978
|
-
-offsetLeftBetweenScrollContainers,
|
|
7979
|
-
-offsetTopBetweenScrollContainers,
|
|
7980
|
-
];
|
|
7981
|
-
};
|
|
7982
|
-
return [getPositionOffsetsFixedOverlay, getScrollOffsets];
|
|
7827
|
+
};
|
|
7828
|
+
|
|
7829
|
+
// Apply inert to all top-level elements that aren't in our keep-interactive set
|
|
7830
|
+
const bodyChildren = Array.from(document.body.children);
|
|
7831
|
+
for (const child of bodyChildren) {
|
|
7832
|
+
makeElementInertSelectivelyOrCompletely(child);
|
|
7983
7833
|
}
|
|
7984
7834
|
|
|
7985
|
-
|
|
7986
|
-
|
|
7987
|
-
|
|
7988
|
-
|
|
7989
|
-
const overlayLeft = overlayRect.left;
|
|
7990
|
-
const overlayTop = overlayRect.top;
|
|
7991
|
-
let overlayTargetLeft = overlayTargetRect.left;
|
|
7992
|
-
let overlayTargetTop = overlayTargetRect.top;
|
|
7993
|
-
if (scrollContainerIsDocument) {
|
|
7994
|
-
overlayTargetLeft += scrollContainer.scrollLeft;
|
|
7995
|
-
overlayTargetTop += scrollContainer.scrollTop;
|
|
7996
|
-
}
|
|
7997
|
-
const offsetLeftBetweenTargetAndOverlay = overlayTargetLeft - overlayLeft;
|
|
7998
|
-
const offsetTopBetweenTargetAndOverlay = overlayTargetTop - overlayTop;
|
|
7999
|
-
return [
|
|
8000
|
-
-scrollContainer.scrollLeft + offsetLeftBetweenTargetAndOverlay,
|
|
8001
|
-
-scrollContainer.scrollTop + offsetTopBetweenTargetAndOverlay,
|
|
8002
|
-
];
|
|
8003
|
-
}
|
|
7835
|
+
return () => {
|
|
7836
|
+
cleanup();
|
|
7837
|
+
};
|
|
7838
|
+
};
|
|
8004
7839
|
|
|
8005
|
-
|
|
8006
|
-
|
|
8007
|
-
|
|
8008
|
-
|
|
8009
|
-
|
|
8010
|
-
|
|
8011
|
-
|
|
8012
|
-
|
|
8013
|
-
|
|
8014
|
-
|
|
8015
|
-
|
|
8016
|
-
|
|
8017
|
-
|
|
8018
|
-
|
|
8019
|
-
|
|
8020
|
-
|
|
8021
|
-
|
|
8022
|
-
|
|
8023
|
-
|
|
7840
|
+
installImportMetaCssBuild(import.meta);/**
|
|
7841
|
+
* Drag Gesture System
|
|
7842
|
+
*
|
|
7843
|
+
* TODO: rename moveX/moveY en juste x/y
|
|
7844
|
+
* puisque move c'est perturbant sachant que c'est drag + scroll
|
|
7845
|
+
* et que drag c'est juste la partie mouvement de la souris
|
|
7846
|
+
*
|
|
7847
|
+
* donc juste x/y ca seras surement mieux
|
|
7848
|
+
*
|
|
7849
|
+
*/
|
|
7850
|
+
const css$5 = /* css */`
|
|
7851
|
+
.navi_drag_gesture_backdrop {
|
|
7852
|
+
position: fixed;
|
|
7853
|
+
inset: 0;
|
|
7854
|
+
/* A finger dragging must not also pan the page under it. The backdrop is
|
|
7855
|
+
the only element the finger can be over once the gesture is running. */
|
|
7856
|
+
touch-action: none;
|
|
7857
|
+
user-select: none;
|
|
7858
|
+
}
|
|
7859
|
+
`;
|
|
7860
|
+
const createDragGestureController = (options = {}) => {
|
|
7861
|
+
const {
|
|
7862
|
+
name,
|
|
7863
|
+
onGrab,
|
|
7864
|
+
onDragStart,
|
|
7865
|
+
onDrag,
|
|
7866
|
+
onRelease,
|
|
7867
|
+
threshold = 5,
|
|
7868
|
+
direction: defaultDirection = {
|
|
7869
|
+
x: true,
|
|
7870
|
+
y: true
|
|
7871
|
+
},
|
|
7872
|
+
documentInteractions = "auto",
|
|
7873
|
+
backdrop = true,
|
|
7874
|
+
backdropZIndex = 999999
|
|
7875
|
+
} = options;
|
|
7876
|
+
const dragGestureController = {
|
|
7877
|
+
grab: null,
|
|
7878
|
+
gravViaPointer: null
|
|
8024
7879
|
};
|
|
8025
|
-
const
|
|
8026
|
-
|
|
8027
|
-
|
|
7880
|
+
const grab = ({
|
|
7881
|
+
element,
|
|
7882
|
+
direction = defaultDirection,
|
|
7883
|
+
event = new CustomEvent("programmatic"),
|
|
7884
|
+
grabX = 0,
|
|
7885
|
+
grabY = 0,
|
|
7886
|
+
cursor = "grabbing",
|
|
7887
|
+
scrollContainer = document.documentElement,
|
|
7888
|
+
layoutScrollableLeft: scrollableLeftAtGrab = 0,
|
|
7889
|
+
layoutScrollableTop: scrollableTopAtGrab = 0
|
|
7890
|
+
} = {}) => {
|
|
7891
|
+
if (!element) {
|
|
7892
|
+
throw new Error("element is required");
|
|
8028
7893
|
}
|
|
8029
|
-
|
|
8030
|
-
|
|
8031
|
-
const referenceScrollContainerRect =
|
|
8032
|
-
referenceScrollContainer.getBoundingClientRect();
|
|
8033
|
-
let offsetLeftBetweenScrollContainers =
|
|
8034
|
-
referenceScrollContainerRect.left - scrollContainerRect.left;
|
|
8035
|
-
let offsetTopBetweenScrollContainers =
|
|
8036
|
-
referenceScrollContainerRect.top - scrollContainerRect.top;
|
|
8037
|
-
if (scrollContainerIsDocument) {
|
|
8038
|
-
offsetLeftBetweenScrollContainers -= scrollContainer.scrollLeft;
|
|
8039
|
-
offsetTopBetweenScrollContainers -= scrollContainer.scrollTop;
|
|
7894
|
+
if (!direction.x && !direction.y) {
|
|
7895
|
+
return null;
|
|
8040
7896
|
}
|
|
8041
|
-
|
|
8042
|
-
|
|
8043
|
-
|
|
8044
|
-
|
|
8045
|
-
|
|
8046
|
-
};
|
|
8047
|
-
return [getPositionOffsetsOverlay, getScrollOffsetsOverlay];
|
|
8048
|
-
};
|
|
8049
|
-
const isOverlayOf = (element, potentialTarget) => {
|
|
8050
|
-
const overlayForAttribute = element.getAttribute("data-overlay-for");
|
|
8051
|
-
if (!overlayForAttribute) {
|
|
8052
|
-
return false;
|
|
8053
|
-
}
|
|
8054
|
-
const overlayTarget = document.querySelector(`#${overlayForAttribute}`);
|
|
8055
|
-
if (!overlayTarget) {
|
|
8056
|
-
return false;
|
|
8057
|
-
}
|
|
8058
|
-
if (overlayTarget === potentialTarget) {
|
|
8059
|
-
return true;
|
|
8060
|
-
}
|
|
8061
|
-
const overlayTargetPositionedParent = getPositionedParent(overlayTarget);
|
|
8062
|
-
if (overlayTargetPositionedParent === potentialTarget) {
|
|
8063
|
-
return true;
|
|
8064
|
-
}
|
|
8065
|
-
return false;
|
|
8066
|
-
};
|
|
8067
|
-
|
|
8068
|
-
const { documentElement } =
|
|
8069
|
-
typeof document === "object" ? document : { documentElement: null };
|
|
8070
|
-
|
|
8071
|
-
const createGetScrollOffsets = (
|
|
8072
|
-
scrollContainer,
|
|
8073
|
-
referenceScrollContainer,
|
|
8074
|
-
positionedParent,
|
|
8075
|
-
samePositionedParent,
|
|
8076
|
-
) => {
|
|
8077
|
-
const getGetScrollOffsetsSameContainer = () => {
|
|
8078
|
-
const scrollContainerIsDocument = scrollContainer === documentElement;
|
|
8079
|
-
// I don't really get why we have to add scrollLeft (scrollLeft at grab)
|
|
8080
|
-
// to properly position the element in this scenario
|
|
8081
|
-
// It happens since we use translateX to position the element
|
|
8082
|
-
// Or maybe since something else. In any case it works
|
|
8083
|
-
const { scrollLeft, scrollTop } = samePositionedParent
|
|
8084
|
-
? { scrollLeft: 0, scrollTop: 0 }
|
|
8085
|
-
: referenceScrollContainer;
|
|
8086
|
-
if (scrollContainerIsDocument) {
|
|
8087
|
-
const fixedPosition = findSelfOrAncestorFixedPosition(positionedParent);
|
|
8088
|
-
if (fixedPosition) {
|
|
8089
|
-
const getScrollOffsetsFixed = () => {
|
|
8090
|
-
const leftScrollToAdd = scrollLeft + fixedPosition[0];
|
|
8091
|
-
const topScrollToAdd = scrollTop + fixedPosition[1];
|
|
8092
|
-
return [leftScrollToAdd, topScrollToAdd];
|
|
8093
|
-
};
|
|
8094
|
-
return getScrollOffsetsFixed;
|
|
8095
|
-
}
|
|
7897
|
+
const [publishBeforeDrag, addBeforeDragCallback] = createPubSub();
|
|
7898
|
+
const [publishDrag, addDragCallback] = createPubSub();
|
|
7899
|
+
const [publishRelease, addReleaseCallback] = createPubSub();
|
|
7900
|
+
if (onDrag) {
|
|
7901
|
+
addDragCallback(onDrag);
|
|
8096
7902
|
}
|
|
8097
|
-
|
|
8098
|
-
|
|
8099
|
-
referenceScrollContainer.contains(positionedParent);
|
|
8100
|
-
if (!positionedParentIsInsideScrollContainer) {
|
|
8101
|
-
// positionedParent is outside the scroll container (e.g. clone in document.body
|
|
8102
|
-
// while tracking an element inside a custom scroll container).
|
|
8103
|
-
// We must add the scroll at grab time as a frozen offset so that:
|
|
8104
|
-
// - initial topDelta = 0 (clone starts at correct position)
|
|
8105
|
-
// - auto-scroll doesn't double-move the clone (scroll changes cancel out in layout)
|
|
8106
|
-
const scrollLeftAtGrab = referenceScrollContainer.scrollLeft;
|
|
8107
|
-
const scrollTopAtGrab = referenceScrollContainer.scrollTop;
|
|
8108
|
-
return () => [scrollLeft + scrollLeftAtGrab, scrollTop + scrollTopAtGrab];
|
|
7903
|
+
if (onRelease) {
|
|
7904
|
+
addReleaseCallback(onRelease);
|
|
8109
7905
|
}
|
|
8110
|
-
const
|
|
8111
|
-
|
|
8112
|
-
|
|
8113
|
-
|
|
7906
|
+
const scrollLeftAtGrab = scrollContainer.scrollLeft;
|
|
7907
|
+
const scrollTopAtGrab = scrollContainer.scrollTop;
|
|
7908
|
+
const leftAtGrab = scrollLeftAtGrab + scrollableLeftAtGrab;
|
|
7909
|
+
const topAtGrab = scrollTopAtGrab + scrollableTopAtGrab;
|
|
7910
|
+
const createLayout = (x, y) => {
|
|
7911
|
+
const {
|
|
7912
|
+
scrollLeft,
|
|
7913
|
+
scrollTop
|
|
7914
|
+
} = scrollContainer;
|
|
7915
|
+
const left = scrollableLeftAtGrab + x;
|
|
7916
|
+
const top = scrollableTopAtGrab + y;
|
|
7917
|
+
const scrollableLeft = left - scrollLeft;
|
|
7918
|
+
const scrollableTop = top - scrollTop;
|
|
7919
|
+
const layoutProps = {
|
|
7920
|
+
// Raw input coordinates (dragX - grabX + scrollContainer.scrollLeft)
|
|
7921
|
+
x,
|
|
7922
|
+
y,
|
|
7923
|
+
// container scrolls when layout is created
|
|
7924
|
+
scrollLeft,
|
|
7925
|
+
scrollTop,
|
|
7926
|
+
// Position relative to container excluding scrolls
|
|
7927
|
+
scrollableLeft,
|
|
7928
|
+
scrollableTop,
|
|
7929
|
+
// Position relative to container including scrolls
|
|
7930
|
+
left,
|
|
7931
|
+
top,
|
|
7932
|
+
// Delta since grab (number representing how much we dragged)
|
|
7933
|
+
xDelta: left - leftAtGrab,
|
|
7934
|
+
yDelta: top - topAtGrab
|
|
7935
|
+
};
|
|
7936
|
+
return layoutProps;
|
|
8114
7937
|
};
|
|
8115
|
-
|
|
8116
|
-
|
|
7938
|
+
const grabLayout = createLayout(grabX + scrollContainer.scrollLeft, grabY + scrollContainer.scrollTop);
|
|
7939
|
+
const gestureInfo = {
|
|
7940
|
+
name,
|
|
7941
|
+
direction,
|
|
7942
|
+
started: !threshold,
|
|
7943
|
+
status: "grabbed",
|
|
7944
|
+
element,
|
|
7945
|
+
scrollContainer,
|
|
7946
|
+
grabX,
|
|
7947
|
+
// x grab coordinate (excluding scroll)
|
|
7948
|
+
grabY,
|
|
7949
|
+
// y grab coordinate (excluding scroll)
|
|
7950
|
+
grabLayout,
|
|
7951
|
+
leftAtGrab,
|
|
7952
|
+
topAtGrab,
|
|
7953
|
+
dragX: grabX,
|
|
7954
|
+
// coordinate of the last drag (excluding scroll of the scrollContainer)
|
|
7955
|
+
dragY: grabY,
|
|
7956
|
+
// coordinate of the last drag (excluding scroll of the scrollContainer)
|
|
7957
|
+
layout: grabLayout,
|
|
7958
|
+
isGoingUp: undefined,
|
|
7959
|
+
isGoingDown: undefined,
|
|
7960
|
+
isGoingLeft: undefined,
|
|
7961
|
+
isGoingRight: undefined,
|
|
7962
|
+
intentGoingUp: false,
|
|
7963
|
+
intentGoingDown: false,
|
|
7964
|
+
intentGoingLeft: false,
|
|
7965
|
+
intentGoingRight: false,
|
|
7966
|
+
// How fast the pointer is going, in px/ms, signed per axis
|
|
7967
|
+
// (see measureVelocity)
|
|
7968
|
+
velocityX: 0,
|
|
7969
|
+
velocityY: 0,
|
|
7970
|
+
velocity: 0,
|
|
7971
|
+
// metadata about interaction sources
|
|
7972
|
+
grabEvent: event,
|
|
7973
|
+
dragEvent: null,
|
|
7974
|
+
releaseEvent: null
|
|
7975
|
+
};
|
|
7976
|
+
definePropertyAsReadOnly(gestureInfo, "name");
|
|
7977
|
+
definePropertyAsReadOnly(gestureInfo, "direction");
|
|
7978
|
+
definePropertyAsReadOnly(gestureInfo, "scrollContainer");
|
|
7979
|
+
definePropertyAsReadOnly(gestureInfo, "grabX");
|
|
7980
|
+
definePropertyAsReadOnly(gestureInfo, "grabY");
|
|
7981
|
+
definePropertyAsReadOnly(gestureInfo, "grabLayout");
|
|
7982
|
+
definePropertyAsReadOnly(gestureInfo, "leftAtGrab");
|
|
7983
|
+
definePropertyAsReadOnly(gestureInfo, "topAtGrab");
|
|
7984
|
+
definePropertyAsReadOnly(gestureInfo, "grabEvent");
|
|
8117
7985
|
|
|
8118
|
-
|
|
8119
|
-
|
|
8120
|
-
|
|
8121
|
-
|
|
8122
|
-
|
|
8123
|
-
|
|
8124
|
-
|
|
8125
|
-
|
|
8126
|
-
|
|
8127
|
-
|
|
8128
|
-
|
|
8129
|
-
|
|
8130
|
-
|
|
8131
|
-
|
|
8132
|
-
|
|
8133
|
-
|
|
8134
|
-
|
|
8135
|
-
|
|
8136
|
-
|
|
8137
|
-
|
|
8138
|
-
|
|
8139
|
-
|
|
8140
|
-
|
|
8141
|
-
const { scrollLeft, scrollTop } = scrollContainer;
|
|
8142
|
-
const leftRelativeToScrollContainer = scrollableLeft + scrollLeft;
|
|
8143
|
-
const topRelativeToScrollContainer = scrollableTop + scrollTop;
|
|
8144
|
-
return [leftRelativeToScrollContainer, topRelativeToScrollContainer];
|
|
8145
|
-
};
|
|
8146
|
-
|
|
8147
|
-
const installImportMetaCssBuild = (importMeta) => {
|
|
8148
|
-
const IMPORT_META_CSS_BUILD = "jsenv_import_meta_css_build";
|
|
7986
|
+
// Where the pointer IS is not where it is going: throwing something is a
|
|
7987
|
+
// matter of speed, and the gesture is the only place that sees the timing of
|
|
7988
|
+
// the events it receives.
|
|
7989
|
+
const measureVelocity = createVelocityMeter(grabX, grabY);
|
|
7990
|
+
document_interactions: {
|
|
7991
|
+
if (documentInteractions === "manual") {
|
|
7992
|
+
break document_interactions;
|
|
7993
|
+
}
|
|
7994
|
+
/*
|
|
7995
|
+
GOAL: Take control of document-level interactions during drag gestures
|
|
7996
|
+
|
|
7997
|
+
WHY: During drag operations, we need to prevent conflicting user interactions that would:
|
|
7998
|
+
1. Interfere with the drag gesture (competing pointer events, focus changes)
|
|
7999
|
+
2. Break the visual feedback (inconsistent cursors, hover states)
|
|
8000
|
+
3. Cause unwanted scrolling (keyboard shortcuts, wheel events in restricted directions)
|
|
8001
|
+
4. Create accessibility issues (focus jumping, screen reader confusion)
|
|
8002
|
+
STRATEGY: Create a controlled interaction environment by:
|
|
8003
|
+
1. VISUAL CONTROL: Use a backdrop to unify cursor appearance and block pointer events
|
|
8004
|
+
2. INTERACTION ISOLATION: Make non-dragged elements inert to prevent interference
|
|
8005
|
+
3. FOCUS MANAGEMENT: Control focus location and prevent focus changes during drag
|
|
8006
|
+
4. SELECTIVE SCROLLING: Allow scrolling only in directions supported by the drag gesture
|
|
8007
|
+
IMPLEMENTATION:
|
|
8008
|
+
*/
|
|
8149
8009
|
|
|
8150
|
-
|
|
8151
|
-
|
|
8152
|
-
|
|
8010
|
+
// 1. INTERACTION ISOLATION: Make everything except the dragged element inert
|
|
8011
|
+
// This prevents keyboard events, pointer interactions, and screen reader navigation
|
|
8012
|
+
// on non-relevant elements during the drag operation
|
|
8013
|
+
const cleanupInert = isolateInteractions([element, ...Array.from(document.querySelectorAll("[data-droppable]"))]);
|
|
8014
|
+
addReleaseCallback(() => {
|
|
8015
|
+
cleanupInert();
|
|
8016
|
+
});
|
|
8153
8017
|
|
|
8154
|
-
|
|
8155
|
-
|
|
8156
|
-
|
|
8157
|
-
|
|
8158
|
-
|
|
8159
|
-
|
|
8160
|
-
|
|
8161
|
-
|
|
8162
|
-
|
|
8163
|
-
};
|
|
8164
|
-
const remove = (url) => {
|
|
8165
|
-
const stylesheet = stylesheetMap.get(url);
|
|
8166
|
-
document.adoptedStyleSheets = document.adoptedStyleSheets.filter(
|
|
8167
|
-
(s) => s !== stylesheet,
|
|
8168
|
-
);
|
|
8169
|
-
stylesheetMap.delete(url);
|
|
8170
|
-
};
|
|
8018
|
+
// 2. VISUAL CONTROL: Backdrop for consistent cursor and pointer event blocking
|
|
8019
|
+
if (backdrop) {
|
|
8020
|
+
import.meta.css = [css$5, "@jsenv/dom/src/interaction/drag/drag_gesture.js"];
|
|
8021
|
+
const backdropElement = document.createElement("div");
|
|
8022
|
+
backdropElement.className = "navi_drag_gesture_backdrop";
|
|
8023
|
+
backdropElement.ariaHidden = "true";
|
|
8024
|
+
backdropElement.setAttribute("data-backdrop", "");
|
|
8025
|
+
backdropElement.style.zIndex = backdropZIndex;
|
|
8026
|
+
backdropElement.style.cursor = cursor;
|
|
8171
8027
|
|
|
8172
|
-
|
|
8173
|
-
|
|
8174
|
-
|
|
8175
|
-
|
|
8176
|
-
|
|
8177
|
-
|
|
8178
|
-
|
|
8179
|
-
|
|
8180
|
-
|
|
8181
|
-
|
|
8182
|
-
|
|
8028
|
+
// Handle wheel events on backdrop for directionally-constrained drag gestures
|
|
8029
|
+
// (e.g., table column resize should only allow horizontal scrolling)
|
|
8030
|
+
if (!direction.x || !direction.y) {
|
|
8031
|
+
backdropElement.onwheel = e => {
|
|
8032
|
+
e.preventDefault();
|
|
8033
|
+
const scrollX = direction.x ? e.deltaX : 0;
|
|
8034
|
+
const scrollY = direction.y ? e.deltaY : 0;
|
|
8035
|
+
scrollContainer.scrollBy({
|
|
8036
|
+
left: scrollX,
|
|
8037
|
+
top: scrollY,
|
|
8038
|
+
behavior: "auto"
|
|
8039
|
+
});
|
|
8040
|
+
};
|
|
8183
8041
|
}
|
|
8184
|
-
|
|
8185
|
-
|
|
8186
|
-
|
|
8187
|
-
|
|
8188
|
-
currentCssSourceMap.set(url, value);
|
|
8189
|
-
} else if (currentCssSourceMap.get(url) !== value) {
|
|
8190
|
-
update(url, value);
|
|
8191
|
-
currentCssSourceMap.set(url, value);
|
|
8042
|
+
document.body.appendChild(backdropElement);
|
|
8043
|
+
addReleaseCallback(() => {
|
|
8044
|
+
backdropElement.remove();
|
|
8045
|
+
});
|
|
8192
8046
|
}
|
|
8193
|
-
},
|
|
8194
|
-
});
|
|
8195
|
-
};
|
|
8196
8047
|
|
|
8197
|
-
|
|
8198
|
-
|
|
8199
|
-
|
|
8200
|
-
|
|
8201
|
-
|
|
8202
|
-
|
|
8203
|
-
|
|
8204
|
-
|
|
8205
|
-
|
|
8206
|
-
|
|
8207
|
-
|
|
8208
|
-
|
|
8209
|
-
|
|
8210
|
-
|
|
8211
|
-
|
|
8212
|
-
|
|
8213
|
-
|
|
8214
|
-
|
|
8215
|
-
|
|
8216
|
-
|
|
8217
|
-
|
|
8218
|
-
|
|
8219
|
-
|
|
8220
|
-
|
|
8221
|
-
|
|
8222
|
-
|
|
8223
|
-
|
|
8224
|
-
|
|
8225
|
-
|
|
8226
|
-
*
|
|
8227
|
-
* After calling isolateInteractions([modal, dropdown]):
|
|
8228
|
-
* ```
|
|
8229
|
-
* <body>
|
|
8230
|
-
* <header inert>...</header> ← made inert (no active descendants)
|
|
8231
|
-
* <main> ← not inert because it contains active elements
|
|
8232
|
-
* <div> ← not inert because it contains .modal
|
|
8233
|
-
* <span inert>some content</span> ← made inert selectively
|
|
8234
|
-
* <div class="modal">modal content</div> ← stays active
|
|
8235
|
-
* <span inert>more content</span> ← made inert selectively
|
|
8236
|
-
* </div>
|
|
8237
|
-
* <aside inert>already inert</aside>
|
|
8238
|
-
* <div class="dropdown">dropdown menu</div> ← stays active
|
|
8239
|
-
* </main>
|
|
8240
|
-
* <footer inert>...</footer>
|
|
8241
|
-
* </body>
|
|
8242
|
-
* ```
|
|
8243
|
-
*
|
|
8244
|
-
* After calling cleanup():
|
|
8245
|
-
* ```
|
|
8246
|
-
* <body>
|
|
8247
|
-
* <header>...</header>
|
|
8248
|
-
* <main>
|
|
8249
|
-
* <div>
|
|
8250
|
-
* <span>some content</span>
|
|
8251
|
-
* <div class="modal">modal content</div>
|
|
8252
|
-
* <span>more content</span>
|
|
8253
|
-
* </div>
|
|
8254
|
-
* <aside inert>already inert</aside> ← [inert] preserved
|
|
8255
|
-
* <div class="dropdown">dropdown menu</div>
|
|
8256
|
-
* </main>
|
|
8257
|
-
* <footer>...</footer>
|
|
8258
|
-
* </body>
|
|
8259
|
-
* ```
|
|
8260
|
-
*
|
|
8261
|
-
* @param {Array<Element>} elements - Array of elements to keep interactive (non-inert)
|
|
8262
|
-
* @returns {Function} cleanup - Function to restore original inert states
|
|
8263
|
-
*/
|
|
8264
|
-
const isolateInteractions = (elements) => {
|
|
8265
|
-
const cleanupCallbackSet = new Set();
|
|
8266
|
-
const cleanup = () => {
|
|
8267
|
-
for (const cleanupCallback of cleanupCallbackSet) {
|
|
8268
|
-
cleanupCallback();
|
|
8269
|
-
}
|
|
8270
|
-
cleanupCallbackSet.clear();
|
|
8271
|
-
};
|
|
8048
|
+
// 3. FOCUS MANAGEMENT: Control and stabilize focus during drag
|
|
8049
|
+
const {
|
|
8050
|
+
activeElement
|
|
8051
|
+
} = document;
|
|
8052
|
+
const focusableElement = findFocusable(element);
|
|
8053
|
+
// Focus the dragged element (or document.body as fallback) to establish clear focus context
|
|
8054
|
+
// This also ensure any keydown event listened by the currently focused element
|
|
8055
|
+
// won't be available during drag
|
|
8056
|
+
const elementToFocus = focusableElement || document.body;
|
|
8057
|
+
elementToFocus.focus({
|
|
8058
|
+
preventScroll: true
|
|
8059
|
+
});
|
|
8060
|
+
addReleaseCallback(() => {
|
|
8061
|
+
// Restore original focus on release
|
|
8062
|
+
activeElement.focus({
|
|
8063
|
+
preventScroll: true
|
|
8064
|
+
});
|
|
8065
|
+
});
|
|
8066
|
+
// Prevent Tab navigation entirely (focus should stay stable)
|
|
8067
|
+
const onkeydown = e => {
|
|
8068
|
+
if (e.key === "Tab") {
|
|
8069
|
+
e.preventDefault();
|
|
8070
|
+
return;
|
|
8071
|
+
}
|
|
8072
|
+
};
|
|
8073
|
+
document.addEventListener("keydown", onkeydown);
|
|
8074
|
+
addReleaseCallback(() => {
|
|
8075
|
+
document.removeEventListener("keydown", onkeydown);
|
|
8076
|
+
});
|
|
8272
8077
|
|
|
8273
|
-
|
|
8274
|
-
|
|
8275
|
-
|
|
8276
|
-
|
|
8277
|
-
|
|
8278
|
-
|
|
8279
|
-
|
|
8280
|
-
|
|
8281
|
-
|
|
8078
|
+
// 4. SELECTIVE SCROLLING: Allow keyboard scrolling only in supported directions
|
|
8079
|
+
{
|
|
8080
|
+
const onDocumentKeydown = keyboardEvent => {
|
|
8081
|
+
// Vertical scrolling keys - prevent if vertical movement not supported
|
|
8082
|
+
if (keyboardEvent.key === "ArrowUp" || keyboardEvent.key === "ArrowDown" || keyboardEvent.key === " " || keyboardEvent.key === "PageUp" || keyboardEvent.key === "PageDown" || keyboardEvent.key === "Home" || keyboardEvent.key === "End") {
|
|
8083
|
+
if (!direction.y) {
|
|
8084
|
+
keyboardEvent.preventDefault();
|
|
8085
|
+
}
|
|
8086
|
+
return;
|
|
8087
|
+
}
|
|
8088
|
+
// Horizontal scrolling keys - prevent if horizontal movement not supported
|
|
8089
|
+
if (keyboardEvent.key === "ArrowLeft" || keyboardEvent.key === "ArrowRight") {
|
|
8090
|
+
if (!direction.x) {
|
|
8091
|
+
keyboardEvent.preventDefault();
|
|
8092
|
+
}
|
|
8093
|
+
return;
|
|
8094
|
+
}
|
|
8095
|
+
};
|
|
8096
|
+
document.addEventListener("keydown", onDocumentKeydown);
|
|
8097
|
+
addReleaseCallback(() => {
|
|
8098
|
+
document.removeEventListener("keydown", onDocumentKeydown);
|
|
8099
|
+
});
|
|
8282
8100
|
}
|
|
8283
8101
|
}
|
|
8284
8102
|
|
|
8285
|
-
//
|
|
8286
|
-
|
|
8287
|
-
|
|
8288
|
-
|
|
8289
|
-
|
|
8290
|
-
|
|
8291
|
-
|
|
8103
|
+
// Set up scroll event handling to adjust drag position when scrolling occurs
|
|
8104
|
+
{
|
|
8105
|
+
let isHandlingScroll = false;
|
|
8106
|
+
const handleScroll = scrollEvent => {
|
|
8107
|
+
if (isHandlingScroll) {
|
|
8108
|
+
return;
|
|
8109
|
+
}
|
|
8110
|
+
isHandlingScroll = true;
|
|
8111
|
+
drag(gestureInfo.dragX, gestureInfo.dragY, {
|
|
8112
|
+
event: scrollEvent
|
|
8113
|
+
});
|
|
8114
|
+
isHandlingScroll = false;
|
|
8115
|
+
};
|
|
8116
|
+
const scrollEventReceiver = scrollContainer === document.documentElement ? document : scrollContainer;
|
|
8117
|
+
scrollEventReceiver.addEventListener("scroll", handleScroll, {
|
|
8118
|
+
passive: true
|
|
8119
|
+
});
|
|
8120
|
+
addReleaseCallback(() => {
|
|
8121
|
+
scrollEventReceiver.removeEventListener("scroll", handleScroll, {
|
|
8122
|
+
passive: true
|
|
8123
|
+
});
|
|
8124
|
+
});
|
|
8292
8125
|
}
|
|
8293
|
-
|
|
8294
|
-
|
|
8295
|
-
|
|
8296
|
-
|
|
8297
|
-
|
|
8298
|
-
|
|
8299
|
-
|
|
8300
|
-
|
|
8301
|
-
|
|
8302
|
-
|
|
8303
|
-
|
|
8304
|
-
|
|
8126
|
+
const determineDragData = ({
|
|
8127
|
+
dragX,
|
|
8128
|
+
dragY,
|
|
8129
|
+
dragEvent,
|
|
8130
|
+
isRelease = false
|
|
8131
|
+
}) => {
|
|
8132
|
+
// === ÉTAT INITIAL (au moment du grab) ===
|
|
8133
|
+
const {
|
|
8134
|
+
grabX,
|
|
8135
|
+
grabY,
|
|
8136
|
+
grabLayout
|
|
8137
|
+
} = gestureInfo;
|
|
8138
|
+
// === CE QUI EST DEMANDÉ (où on veut aller) ===
|
|
8139
|
+
// Calcul de la direction basé sur le mouvement précédent
|
|
8140
|
+
// (ne tient pas compte du mouvement final une fois les contraintes appliquées)
|
|
8141
|
+
// (ici on veut connaitre l'intention)
|
|
8142
|
+
// on va utiliser cela pour savoir vers où on scroll si nécéssaire par ex
|
|
8143
|
+
const currentDragX = gestureInfo.dragX;
|
|
8144
|
+
const currentDragY = gestureInfo.dragY;
|
|
8145
|
+
const isGoingLeft = dragX < currentDragX;
|
|
8146
|
+
const isGoingRight = dragX > currentDragX;
|
|
8147
|
+
const isGoingUp = dragY < currentDragY;
|
|
8148
|
+
const isGoingDown = dragY > currentDragY;
|
|
8149
|
+
const layoutXRequested = direction.x ? scrollContainer.scrollLeft + (dragX - grabX) : grabLayout.scrollLeft;
|
|
8150
|
+
const layoutYRequested = direction.y ? scrollContainer.scrollTop + (dragY - grabY) : grabLayout.scrollTop;
|
|
8151
|
+
const layoutRequested = createLayout(layoutXRequested, layoutYRequested);
|
|
8152
|
+
const currentLayout = gestureInfo.layout;
|
|
8153
|
+
let layout;
|
|
8154
|
+
if (layoutRequested.x === currentLayout.x && layoutRequested.y === currentLayout.y) {
|
|
8155
|
+
layout = currentLayout;
|
|
8156
|
+
} else {
|
|
8157
|
+
// === APPLICATION DES CONTRAINTES ===
|
|
8158
|
+
let layoutConstrained = layoutRequested;
|
|
8159
|
+
const limitLayout = (left, top) => {
|
|
8160
|
+
layoutConstrained = createLayout(left === undefined ? layoutConstrained.x : left - scrollableLeftAtGrab, top === undefined ? layoutConstrained.y : top - scrollableTopAtGrab);
|
|
8161
|
+
};
|
|
8162
|
+
publishBeforeDrag(layoutRequested, currentLayout, limitLayout, {
|
|
8163
|
+
dragEvent,
|
|
8164
|
+
isRelease
|
|
8165
|
+
});
|
|
8166
|
+
// === ÉTAT FINAL ===
|
|
8167
|
+
layout = layoutConstrained;
|
|
8168
|
+
}
|
|
8169
|
+
const dragData = {
|
|
8170
|
+
dragX,
|
|
8171
|
+
dragY,
|
|
8172
|
+
layout,
|
|
8173
|
+
isGoingLeft,
|
|
8174
|
+
isGoingRight,
|
|
8175
|
+
isGoingUp,
|
|
8176
|
+
isGoingDown,
|
|
8177
|
+
status: isRelease ? "released" : "dragging",
|
|
8178
|
+
dragEvent: isRelease ? gestureInfo.dragEvent : dragEvent,
|
|
8179
|
+
releaseEvent: isRelease ? dragEvent : null
|
|
8180
|
+
};
|
|
8181
|
+
if (isRelease) {
|
|
8182
|
+
return dragData;
|
|
8183
|
+
}
|
|
8184
|
+
if (!gestureInfo.started && threshold) {
|
|
8185
|
+
const deltaX = Math.abs(dragX - grabX);
|
|
8186
|
+
const deltaY = Math.abs(dragY - grabY);
|
|
8187
|
+
if (direction.x && direction.y) {
|
|
8188
|
+
// Both directions: check both axes
|
|
8189
|
+
if (deltaX < threshold && deltaY < threshold) {
|
|
8190
|
+
return dragData;
|
|
8191
|
+
}
|
|
8192
|
+
} else if (direction.x) {
|
|
8193
|
+
if (deltaX < threshold) {
|
|
8194
|
+
return dragData;
|
|
8195
|
+
}
|
|
8196
|
+
} else if (direction.y) {
|
|
8197
|
+
if (deltaY < threshold) {
|
|
8198
|
+
return dragData;
|
|
8199
|
+
}
|
|
8200
|
+
}
|
|
8201
|
+
dragData.started = true;
|
|
8202
|
+
}
|
|
8203
|
+
return dragData;
|
|
8204
|
+
};
|
|
8205
|
+
const markAsStarted = () => {
|
|
8206
|
+
// Suppress the click that the browser fires after pointerup following a real drag.
|
|
8207
|
+
// The capture phase runs before any element onClick handler.
|
|
8208
|
+
const suppressClick = clickEvent => {
|
|
8209
|
+
clickEvent.stopPropagation();
|
|
8210
|
+
clickEvent.preventDefault();
|
|
8211
|
+
stopSuppressingClick();
|
|
8212
|
+
};
|
|
8213
|
+
// That click is dispatched AFTER the pointerup that ends the drag, so
|
|
8214
|
+
// this cannot be taken down with the gesture — it would be gone one event
|
|
8215
|
+
// too early, and the drag would end on the link it started from being
|
|
8216
|
+
// followed. It goes once it has swallowed the click, or at the next press
|
|
8217
|
+
// if the drag produced none: a click is always preceded by a press, so a
|
|
8218
|
+
// suppressor that outlives one press can never reach the click of
|
|
8219
|
+
// another.
|
|
8220
|
+
const stopSuppressingClick = () => {
|
|
8221
|
+
document.removeEventListener("click", suppressClick, {
|
|
8222
|
+
capture: true
|
|
8223
|
+
});
|
|
8224
|
+
document.removeEventListener("pointerdown", stopSuppressingClick, {
|
|
8225
|
+
capture: true
|
|
8226
|
+
});
|
|
8227
|
+
};
|
|
8228
|
+
document.addEventListener("click", suppressClick, {
|
|
8229
|
+
capture: true
|
|
8230
|
+
});
|
|
8231
|
+
addReleaseCallback(() => {
|
|
8232
|
+
document.addEventListener("pointerdown", stopSuppressingClick, {
|
|
8233
|
+
capture: true
|
|
8234
|
+
});
|
|
8235
|
+
});
|
|
8236
|
+
// Everything this gesture puts on the document is in place, and undoable,
|
|
8237
|
+
// BEFORE anybody is told it started: a listener may end the gesture from
|
|
8238
|
+
// inside this very notification — that is how a press becomes a drag (see
|
|
8239
|
+
// dragAfterIntent, where the gesture that measured the distance releases
|
|
8240
|
+
// itself the moment it is confirmed). Set up afterwards, a listener would
|
|
8241
|
+
// be registering its own removal with a gesture that is already over, and
|
|
8242
|
+
// would then outlive it: what one sees is a click swallowed long after
|
|
8243
|
+
// the drag it belonged to.
|
|
8244
|
+
dispatchPublicCustomEvent(element, "navi_drag_start", {
|
|
8245
|
+
gestureInfo
|
|
8246
|
+
});
|
|
8247
|
+
onDragStart?.(gestureInfo);
|
|
8248
|
+
};
|
|
8305
8249
|
|
|
8306
|
-
|
|
8307
|
-
|
|
8308
|
-
|
|
8309
|
-
|
|
8310
|
-
|
|
8311
|
-
|
|
8312
|
-
|
|
8313
|
-
|
|
8314
|
-
|
|
8315
|
-
|
|
8250
|
+
// Declares the gesture confirmed without waiting for the distance threshold,
|
|
8251
|
+
// for callers who established the intent some other way (a dedicated handle,
|
|
8252
|
+
// a long press).
|
|
8253
|
+
const start = () => {
|
|
8254
|
+
if (gestureInfo.started) {
|
|
8255
|
+
return;
|
|
8256
|
+
}
|
|
8257
|
+
gestureInfo.started = true;
|
|
8258
|
+
markAsStarted();
|
|
8259
|
+
};
|
|
8260
|
+
const drag = (dragX = gestureInfo.dragX,
|
|
8261
|
+
// Scroll container relative X coordinate
|
|
8262
|
+
dragY = gestureInfo.dragY,
|
|
8263
|
+
// Scroll container relative Y coordinate
|
|
8264
|
+
{
|
|
8265
|
+
event = new CustomEvent("programmatic"),
|
|
8266
|
+
isRelease = false
|
|
8267
|
+
} = {}) => {
|
|
8268
|
+
const dragData = determineDragData({
|
|
8269
|
+
dragX,
|
|
8270
|
+
dragY,
|
|
8271
|
+
dragEvent: event,
|
|
8272
|
+
isRelease
|
|
8273
|
+
});
|
|
8274
|
+
const [velocityX, velocityY] = measureVelocity(dragX, dragY);
|
|
8275
|
+
const startedPrevious = gestureInfo.started;
|
|
8276
|
+
const layoutPrevious = gestureInfo.layout;
|
|
8277
|
+
// previousGestureInfo = { ...gestureInfo };
|
|
8278
|
+
Object.assign(gestureInfo, dragData);
|
|
8279
|
+
gestureInfo.velocityX = velocityX;
|
|
8280
|
+
gestureInfo.velocityY = velocityY;
|
|
8281
|
+
gestureInfo.velocity = Math.hypot(velocityX, velocityY);
|
|
8282
|
+
if (gestureInfo.isGoingDown) {
|
|
8283
|
+
gestureInfo.intentGoingDown = true;
|
|
8284
|
+
gestureInfo.intentGoingUp = false;
|
|
8285
|
+
} else if (gestureInfo.isGoingUp) {
|
|
8286
|
+
gestureInfo.intentGoingUp = true;
|
|
8287
|
+
gestureInfo.intentGoingDown = false;
|
|
8288
|
+
}
|
|
8289
|
+
if (gestureInfo.isGoingRight) {
|
|
8290
|
+
gestureInfo.intentGoingRight = true;
|
|
8291
|
+
gestureInfo.intentGoingLeft = false;
|
|
8292
|
+
} else if (gestureInfo.isGoingLeft) {
|
|
8293
|
+
gestureInfo.intentGoingLeft = true;
|
|
8294
|
+
gestureInfo.intentGoingRight = false;
|
|
8295
|
+
}
|
|
8296
|
+
if (!startedPrevious && gestureInfo.started) {
|
|
8297
|
+
markAsStarted();
|
|
8298
|
+
}
|
|
8299
|
+
const someLayoutChange = gestureInfo.layout !== layoutPrevious;
|
|
8300
|
+
dispatchPublicCustomEvent(element, "navi_drag", {
|
|
8301
|
+
gestureInfo,
|
|
8302
|
+
someLayoutChange
|
|
8303
|
+
});
|
|
8304
|
+
publishDrag(gestureInfo,
|
|
8305
|
+
// we still publish drag event even when unchanged
|
|
8306
|
+
// because UI might need to adjust when document scrolls
|
|
8307
|
+
// even if nothing truly changes visually the element
|
|
8308
|
+
// can decide to stick to the scroll for example
|
|
8309
|
+
someLayoutChange);
|
|
8310
|
+
};
|
|
8311
|
+
const release = ({
|
|
8312
|
+
event = new CustomEvent("programmatic"),
|
|
8313
|
+
releaseX = gestureInfo.dragX,
|
|
8314
|
+
releaseY = gestureInfo.dragY
|
|
8315
|
+
} = {}) => {
|
|
8316
|
+
drag(releaseX, releaseY, {
|
|
8317
|
+
event,
|
|
8318
|
+
isRelease: true
|
|
8319
|
+
});
|
|
8320
|
+
dispatchPublicCustomEvent(element, "navi_drag_release", {
|
|
8321
|
+
gestureInfo
|
|
8322
|
+
});
|
|
8323
|
+
publishRelease(gestureInfo);
|
|
8324
|
+
};
|
|
8325
|
+
dispatchPublicCustomEvent(element, "navi_drag_grab", {
|
|
8326
|
+
gestureInfo
|
|
8316
8327
|
});
|
|
8328
|
+
onGrab?.(gestureInfo);
|
|
8329
|
+
const dragGesture = {
|
|
8330
|
+
gestureInfo,
|
|
8331
|
+
addBeforeDragCallback,
|
|
8332
|
+
addDragCallback,
|
|
8333
|
+
addReleaseCallback,
|
|
8334
|
+
start,
|
|
8335
|
+
drag,
|
|
8336
|
+
release
|
|
8337
|
+
};
|
|
8338
|
+
return dragGesture;
|
|
8317
8339
|
};
|
|
8318
|
-
|
|
8319
|
-
const
|
|
8320
|
-
|
|
8321
|
-
|
|
8322
|
-
return;
|
|
8323
|
-
}
|
|
8324
|
-
|
|
8325
|
-
// Since we put all ancestors in toKeepInteractiveSet, if this element
|
|
8326
|
-
// is not in the set, we can check if any of its direct children are.
|
|
8327
|
-
// If none of the direct children are in the set, then no descendants are either.
|
|
8328
|
-
const children = Array.from(el.children);
|
|
8329
|
-
const hasInteractiveChildren = children.some((child) =>
|
|
8330
|
-
toKeepInteractiveSet.has(child),
|
|
8331
|
-
);
|
|
8332
|
-
|
|
8333
|
-
if (!hasInteractiveChildren) {
|
|
8334
|
-
// No interactive descendants, make the entire element inert
|
|
8335
|
-
setInert(el);
|
|
8336
|
-
return;
|
|
8340
|
+
dragGestureController.grab = grab;
|
|
8341
|
+
const initDragByPointer = (grabEvent, dragOptions, initializer) => {
|
|
8342
|
+
if (!isPrimaryButtonEvent(grabEvent)) {
|
|
8343
|
+
return null;
|
|
8337
8344
|
}
|
|
8338
|
-
|
|
8339
|
-
|
|
8340
|
-
|
|
8341
|
-
|
|
8345
|
+
const target = grabEvent.target;
|
|
8346
|
+
if (!target.closest) {
|
|
8347
|
+
// target is a text node
|
|
8348
|
+
return null;
|
|
8342
8349
|
}
|
|
8350
|
+
const mouseEventCoords = mouseEvent => {
|
|
8351
|
+
const {
|
|
8352
|
+
clientX,
|
|
8353
|
+
clientY
|
|
8354
|
+
} = mouseEvent;
|
|
8355
|
+
return [clientX, clientY];
|
|
8356
|
+
};
|
|
8357
|
+
const [grabX, grabY] = mouseEventCoords(grabEvent);
|
|
8358
|
+
const dragGesture = dragGestureController.grab({
|
|
8359
|
+
grabX,
|
|
8360
|
+
grabY,
|
|
8361
|
+
event: grabEvent,
|
|
8362
|
+
...dragOptions
|
|
8363
|
+
});
|
|
8364
|
+
const dragViaPointer = dragEvent => {
|
|
8365
|
+
const [mouseDragX, mouseDragY] = mouseEventCoords(dragEvent);
|
|
8366
|
+
dragGesture.drag(mouseDragX, mouseDragY, {
|
|
8367
|
+
event: dragEvent
|
|
8368
|
+
});
|
|
8369
|
+
};
|
|
8370
|
+
const releaseViaPointer = mouseupEvent => {
|
|
8371
|
+
const [mouseReleaseX, mouseReleaseY] = mouseEventCoords(mouseupEvent);
|
|
8372
|
+
dragGesture.release({
|
|
8373
|
+
event: mouseupEvent,
|
|
8374
|
+
releaseX: mouseReleaseX,
|
|
8375
|
+
releaseY: mouseReleaseY
|
|
8376
|
+
});
|
|
8377
|
+
};
|
|
8378
|
+
dragGesture.dragViaPointer = dragViaPointer;
|
|
8379
|
+
dragGesture.releaseViaPointer = releaseViaPointer;
|
|
8380
|
+
const cleanup = initializer({
|
|
8381
|
+
onMove: dragViaPointer,
|
|
8382
|
+
onRelease: releaseViaPointer,
|
|
8383
|
+
gestureInfo: dragGesture.gestureInfo
|
|
8384
|
+
});
|
|
8385
|
+
dragGesture.addReleaseCallback(() => {
|
|
8386
|
+
cleanup();
|
|
8387
|
+
});
|
|
8388
|
+
return dragGesture;
|
|
8389
|
+
};
|
|
8390
|
+
const grabViaPointer = (grabEvent, options) => {
|
|
8391
|
+
if (grabEvent.type === "pointerdown") {
|
|
8392
|
+
return initDragByPointer(grabEvent, options, ({
|
|
8393
|
+
onMove,
|
|
8394
|
+
onRelease,
|
|
8395
|
+
gestureInfo
|
|
8396
|
+
}) => {
|
|
8397
|
+
// Captured on something that will still be there at the end of the
|
|
8398
|
+
// gesture: the browser releases the capture when its element leaves the
|
|
8399
|
+
// document, and a gesture whose own effect replaces the DOM under the
|
|
8400
|
+
// finger would lose the pointer at its first move. Callers whose target
|
|
8401
|
+
// is stable have nothing to say and keep it.
|
|
8402
|
+
const target = options?.pointerCaptureElement || grabEvent.target;
|
|
8403
|
+
target.setPointerCapture(grabEvent.pointerId);
|
|
8404
|
+
/*
|
|
8405
|
+
* A touchmove left alone is the browser deciding the touch belongs to
|
|
8406
|
+
* it: it takes it to scroll with, and a touch it has taken is a pointer
|
|
8407
|
+
* stream it CANCELS — the gesture dies mid-move, the finger is still
|
|
8408
|
+
* down, and nothing reads it anymore.
|
|
8409
|
+
*
|
|
8410
|
+
* Refused only once the gesture is established (a `touch-action: none`
|
|
8411
|
+
* would take the touch from everyone who merely brushes past the
|
|
8412
|
+
* element), but LISTENED FOR from the grab: whether a touchmove can be
|
|
8413
|
+
* refused at all is decided when the touch begins, from the listeners
|
|
8414
|
+
* present at that moment. Registered later, the listener is handed
|
|
8415
|
+
* events that are already `cancelable: false` — refusing them does
|
|
8416
|
+
* nothing, and the reason is invisible in the code that refuses.
|
|
8417
|
+
*
|
|
8418
|
+
* On the window in capture AND on the grabbed element: a touch keeps
|
|
8419
|
+
* being dispatched at the node it started on, and a gesture may take
|
|
8420
|
+
* that node out of the document (a page that travels navigates) — from
|
|
8421
|
+
* then on the event never passes through the window on its way
|
|
8422
|
+
* anywhere.
|
|
8423
|
+
*/
|
|
8424
|
+
const preventTouchScroll = touchMoveEvent => {
|
|
8425
|
+
if (gestureInfo.started && touchMoveEvent.cancelable) {
|
|
8426
|
+
touchMoveEvent.preventDefault();
|
|
8427
|
+
}
|
|
8428
|
+
};
|
|
8429
|
+
const grabTarget = grabEvent.target;
|
|
8430
|
+
window.addEventListener("touchmove", preventTouchScroll, {
|
|
8431
|
+
passive: false,
|
|
8432
|
+
capture: true
|
|
8433
|
+
});
|
|
8434
|
+
grabTarget.addEventListener("touchmove", preventTouchScroll, {
|
|
8435
|
+
passive: false
|
|
8436
|
+
});
|
|
8437
|
+
// Only OUR capture ending means this gesture is over:
|
|
8438
|
+
// lostpointercapture bubbles, so a descendant giving up its own capture
|
|
8439
|
+
// walks straight into this listener. That is not a rare shape — it is
|
|
8440
|
+
// exactly what happens when a gesture hands over to another one (a
|
|
8441
|
+
// press that becomes a drag releases its intermediate gesture, held on
|
|
8442
|
+
// the pressed element, while the real one is being held on a container
|
|
8443
|
+
// above it), and taken as our own it kills the new gesture one
|
|
8444
|
+
// millisecond after it started.
|
|
8445
|
+
const onCaptureLost = pointerEvent => {
|
|
8446
|
+
if (pointerEvent.target !== target) {
|
|
8447
|
+
return;
|
|
8448
|
+
}
|
|
8449
|
+
onRelease(pointerEvent);
|
|
8450
|
+
};
|
|
8451
|
+
target.addEventListener("lostpointercapture", onCaptureLost);
|
|
8452
|
+
target.addEventListener("pointercancel", onRelease);
|
|
8453
|
+
target.addEventListener("pointermove", onMove);
|
|
8454
|
+
target.addEventListener("pointerup", onRelease);
|
|
8455
|
+
// The end of the pointer is also listened for on the window, because
|
|
8456
|
+
// the end is the one event a gesture cannot afford to miss and the
|
|
8457
|
+
// element it is captured on is not always on its way: a pointer can
|
|
8458
|
+
// be delivered somewhere else entirely (a browser view transition
|
|
8459
|
+
// sends presses to the document root), and a cancel dispatched there
|
|
8460
|
+
// never passes through this element. Missed, the gesture never ends —
|
|
8461
|
+
// whatever it was holding stays held.
|
|
8462
|
+
let released = false;
|
|
8463
|
+
const onPointerEnd = pointerEvent => {
|
|
8464
|
+
if (pointerEvent.pointerId !== grabEvent.pointerId || released) {
|
|
8465
|
+
return;
|
|
8466
|
+
}
|
|
8467
|
+
released = true;
|
|
8468
|
+
onRelease(pointerEvent);
|
|
8469
|
+
};
|
|
8470
|
+
window.addEventListener("pointerup", onPointerEnd, true);
|
|
8471
|
+
window.addEventListener("pointercancel", onPointerEnd, true);
|
|
8472
|
+
return () => {
|
|
8473
|
+
// Listeners first, capture last: giving the pointer back is the
|
|
8474
|
+
// one thing here that can throw, and a gesture that fails to clean
|
|
8475
|
+
// up half way is worse than one that never cleaned up at all — its
|
|
8476
|
+
// listeners stay on the element and answer the NEXT gesture, from
|
|
8477
|
+
// a gesture whose pointer is long gone.
|
|
8478
|
+
window.removeEventListener("touchmove", preventTouchScroll, {
|
|
8479
|
+
capture: true
|
|
8480
|
+
});
|
|
8481
|
+
grabTarget.removeEventListener("touchmove", preventTouchScroll);
|
|
8482
|
+
target.removeEventListener("lostpointercapture", onCaptureLost);
|
|
8483
|
+
target.removeEventListener("pointercancel", onRelease);
|
|
8484
|
+
target.removeEventListener("pointermove", onMove);
|
|
8485
|
+
target.removeEventListener("pointerup", onRelease);
|
|
8486
|
+
window.removeEventListener("pointerup", onPointerEnd, true);
|
|
8487
|
+
window.removeEventListener("pointercancel", onPointerEnd, true);
|
|
8488
|
+
// Asked for only while there is something to give back: a pointer
|
|
8489
|
+
// that is up no longer exists, the browser has already dropped the
|
|
8490
|
+
// capture with it, and asking again throws ("No active pointer with
|
|
8491
|
+
// the given id is found") — on the most ordinary release there is.
|
|
8492
|
+
if (target.hasPointerCapture(grabEvent.pointerId)) {
|
|
8493
|
+
target.releasePointerCapture(grabEvent.pointerId);
|
|
8494
|
+
}
|
|
8495
|
+
};
|
|
8496
|
+
});
|
|
8497
|
+
}
|
|
8498
|
+
if (grabEvent.type === "mousedown") {
|
|
8499
|
+
console.warn(`Received "mousedown" event, "pointerdown" events are recommended to perform drag gestures.`);
|
|
8500
|
+
return initDragByPointer(grabEvent, options, ({
|
|
8501
|
+
onMove,
|
|
8502
|
+
onRelease
|
|
8503
|
+
}) => {
|
|
8504
|
+
const onPointerUp = pointerEvent => {
|
|
8505
|
+
// <button disabled> for example does not emit mouseup if we release mouse over it
|
|
8506
|
+
// -> we add "pointerup" to catch mouseup occuring on disabled element
|
|
8507
|
+
if (pointerEvent.pointerType === "mouse") {
|
|
8508
|
+
onRelease(pointerEvent);
|
|
8509
|
+
}
|
|
8510
|
+
};
|
|
8511
|
+
document.addEventListener("mousemove", onMove);
|
|
8512
|
+
document.addEventListener("mouseup", onRelease);
|
|
8513
|
+
document.addEventListener("pointerup", onPointerUp);
|
|
8514
|
+
return () => {
|
|
8515
|
+
document.removeEventListener("mousemove", onMove);
|
|
8516
|
+
document.removeEventListener("mouseup", onRelease);
|
|
8517
|
+
document.removeEventListener("pointerup", onPointerUp);
|
|
8518
|
+
};
|
|
8519
|
+
});
|
|
8520
|
+
}
|
|
8521
|
+
throw new Error(`Unsupported "${grabEvent.type}" evenet passed to grabViaPointer. "pointerdown" was expected.`);
|
|
8343
8522
|
};
|
|
8344
|
-
|
|
8345
|
-
|
|
8346
|
-
|
|
8347
|
-
|
|
8348
|
-
|
|
8349
|
-
|
|
8350
|
-
|
|
8351
|
-
|
|
8352
|
-
|
|
8353
|
-
|
|
8523
|
+
dragGestureController.grabViaPointer = grabViaPointer;
|
|
8524
|
+
return dragGestureController;
|
|
8525
|
+
};
|
|
8526
|
+
|
|
8527
|
+
// Only the primary button drags: a right click (or any secondary button) opens
|
|
8528
|
+
// a context menu, it never grabs anything.
|
|
8529
|
+
const isPrimaryButtonEvent = event => event.button === undefined || event.button === 0;
|
|
8530
|
+
|
|
8531
|
+
/*
|
|
8532
|
+
* Speed over the last VELOCITY_WINDOW_MS rather than between the last two
|
|
8533
|
+
* events: pointer events arrive irregularly, and the last one before a release
|
|
8534
|
+
* often repeats the previous coordinates — measured on that pair alone, every
|
|
8535
|
+
* throw would end at zero.
|
|
8536
|
+
* A pointer held still keeps producing samples at the same place, so the window
|
|
8537
|
+
* empties itself of movement and the speed falls back to zero on its own: put
|
|
8538
|
+
* down slowly is not thrown.
|
|
8539
|
+
*/
|
|
8540
|
+
const VELOCITY_WINDOW_MS = 100;
|
|
8541
|
+
const createVelocityMeter = (grabX, grabY) => {
|
|
8542
|
+
const samples = [{
|
|
8543
|
+
time: performance.now(),
|
|
8544
|
+
x: grabX,
|
|
8545
|
+
y: grabY
|
|
8546
|
+
}];
|
|
8547
|
+
const measureVelocity = (x, y) => {
|
|
8548
|
+
const time = performance.now();
|
|
8549
|
+
samples.push({
|
|
8550
|
+
time,
|
|
8551
|
+
x,
|
|
8552
|
+
y
|
|
8553
|
+
});
|
|
8554
|
+
while (samples.length > 2 && time - samples[1].time > VELOCITY_WINDOW_MS) {
|
|
8555
|
+
samples.shift();
|
|
8556
|
+
}
|
|
8557
|
+
const oldestSample = samples[0];
|
|
8558
|
+
const elapsed = time - oldestSample.time;
|
|
8559
|
+
if (elapsed === 0) {
|
|
8560
|
+
return [0, 0];
|
|
8561
|
+
}
|
|
8562
|
+
return [(x - oldestSample.x) / elapsed, (y - oldestSample.y) / elapsed];
|
|
8563
|
+
};
|
|
8564
|
+
return measureVelocity;
|
|
8565
|
+
};
|
|
8566
|
+
const definePropertyAsReadOnly = (object, propertyName) => {
|
|
8567
|
+
Object.defineProperty(object, propertyName, {
|
|
8568
|
+
writable: false,
|
|
8569
|
+
value: object[propertyName]
|
|
8570
|
+
});
|
|
8354
8571
|
};
|
|
8355
8572
|
|
|
8356
8573
|
installImportMetaCssBuild(import.meta);/**
|
|
8357
|
-
*
|
|
8574
|
+
* When a press becomes a drag.
|
|
8358
8575
|
*
|
|
8359
|
-
*
|
|
8360
|
-
*
|
|
8361
|
-
*
|
|
8576
|
+
* A pointer going down on a draggable element is ambiguous — it may be a click,
|
|
8577
|
+
* a text selection, a scroll, or a drag — and starting the gesture right away
|
|
8578
|
+
* would steal all the others. This module owns the wait that resolves the
|
|
8579
|
+
* ambiguity, and only then hands over to the real gesture.
|
|
8362
8580
|
*
|
|
8363
|
-
*
|
|
8581
|
+
* There is one gesture, with a trigger per pointer:
|
|
8582
|
+
* - a dedicated handle ([data-drag-handle]) says it outright: drag on contact
|
|
8583
|
+
* - a mouse resolves it by distance — a mouse scrolls with its wheel, so travel
|
|
8584
|
+
* can only mean drag
|
|
8585
|
+
* - a finger resolves it by time — travel is exactly what a scroll looks like,
|
|
8586
|
+
* so the only unambiguous signal left is a finger that does NOT move
|
|
8364
8587
|
*
|
|
8588
|
+
* Whichever trigger fired, it has established the intent: the gesture then
|
|
8589
|
+
* starts at the first pixel, without a second threshold to cross.
|
|
8365
8590
|
*/
|
|
8591
|
+
|
|
8592
|
+
/* iOS shows its callout (Copy / Look Up) and selects the text under the finger
|
|
8593
|
+
on a long press, and does not always route that through an event that can be
|
|
8594
|
+
refused — see preventContextMenu below for the half that is an event.
|
|
8595
|
+
At module scope, and on the markers rather than on the pressed element: it has
|
|
8596
|
+
to be true before the finger lands. */
|
|
8366
8597
|
const css$4 = /* css */`
|
|
8367
|
-
|
|
8368
|
-
|
|
8369
|
-
|
|
8370
|
-
|
|
8598
|
+
[data-drag-handle],
|
|
8599
|
+
[data-drag-source] {
|
|
8600
|
+
-webkit-touch-callout: none;
|
|
8601
|
+
}
|
|
8602
|
+
[data-drag-ignore] {
|
|
8603
|
+
-webkit-touch-callout: default;
|
|
8371
8604
|
}
|
|
8372
8605
|
`;
|
|
8373
|
-
|
|
8606
|
+
import.meta.css = [css$4, "@jsenv/dom/src/interaction/drag/drag_after_intent.js"];
|
|
8607
|
+
|
|
8608
|
+
/**
|
|
8609
|
+
* Waits for the user to mean it, then starts a drag gesture.
|
|
8610
|
+
*
|
|
8611
|
+
* @param {PointerEvent} grabEvent
|
|
8612
|
+
* The `pointerdown` event that may become a drag.
|
|
8613
|
+
* @param {function} dragGestureInitializer
|
|
8614
|
+
* Called once the intent is established; must create and return the real drag
|
|
8615
|
+
* gesture (typically via `grabViaPointer(grabEvent)`). Returning a falsy value
|
|
8616
|
+
* aborts the gesture.
|
|
8617
|
+
* @param {object} [options]
|
|
8618
|
+
* @param {number} [options.threshold=5]
|
|
8619
|
+
* Distance (px) the pointer must travel to start a drag, when the trigger is
|
|
8620
|
+
* distance-based.
|
|
8621
|
+
* @param {boolean|"if-touch"} [options.longPress="if-touch"]
|
|
8622
|
+
* Which pointers start a drag by holding still instead of by travelling.
|
|
8623
|
+
* @param {number} [options.longPressDelay=400]
|
|
8624
|
+
* How long (ms) the pointer must stay down. Kept under the system context-menu
|
|
8625
|
+
* delay so the object is picked up before the menu would have opened.
|
|
8626
|
+
* @param {number} [options.longPressSlop=8]
|
|
8627
|
+
* How far (px) the pointer may drift during the wait before the press is
|
|
8628
|
+
* abandoned — beyond it, the finger is scrolling, not holding.
|
|
8629
|
+
* @param {function} [options.onPressStart]
|
|
8630
|
+
* The pointer went down and the wait began (a cue that the press counts).
|
|
8631
|
+
* @param {function} [options.onPressCancel]
|
|
8632
|
+
* The pointer moved or lifted before the wait was over.
|
|
8633
|
+
* @param {function} [options.onPress]
|
|
8634
|
+
* The wait completed and the object is now held (haptics, scale…).
|
|
8635
|
+
*/
|
|
8636
|
+
const dragAfterIntent = (grabEvent, dragGestureInitializer, {
|
|
8637
|
+
threshold = 5,
|
|
8638
|
+
longPress = "if-touch",
|
|
8639
|
+
longPressDelay = 400,
|
|
8640
|
+
longPressSlop = 8,
|
|
8641
|
+
onPressStart,
|
|
8642
|
+
onPressCancel,
|
|
8643
|
+
onPress
|
|
8644
|
+
} = {}) => {
|
|
8645
|
+
if (!isPrimaryButtonEvent(grabEvent)) {
|
|
8646
|
+
return;
|
|
8647
|
+
}
|
|
8648
|
+
const target = grabEvent.target;
|
|
8649
|
+
const isDedicatedHandle = target.closest && target.closest("[data-drag-handle]");
|
|
8650
|
+
if (isDedicatedHandle) {
|
|
8651
|
+
startDragGesture(dragGestureInitializer);
|
|
8652
|
+
return;
|
|
8653
|
+
}
|
|
8654
|
+
const startsOnLongPress = longPress === true || longPress === "if-touch" && grabEvent.pointerType === "touch";
|
|
8655
|
+
if (startsOnLongPress) {
|
|
8656
|
+
dragAfterLongPress(grabEvent, dragGestureInitializer, {
|
|
8657
|
+
longPressDelay,
|
|
8658
|
+
longPressSlop,
|
|
8659
|
+
onPressStart,
|
|
8660
|
+
onPressCancel,
|
|
8661
|
+
onPress
|
|
8662
|
+
});
|
|
8663
|
+
return;
|
|
8664
|
+
}
|
|
8665
|
+
dragAfterDistance(grabEvent, dragGestureInitializer, threshold);
|
|
8666
|
+
};
|
|
8667
|
+
const startDragGesture = (dragGestureInitializer, catchUpEvent) => {
|
|
8668
|
+
const dragGesture = dragGestureInitializer();
|
|
8669
|
+
if (!dragGesture) {
|
|
8670
|
+
return null;
|
|
8671
|
+
}
|
|
8672
|
+
// The wait is what established the intent; a distance threshold on top of it
|
|
8673
|
+
// would ask the user to prove the same thing twice.
|
|
8674
|
+
dragGesture.start();
|
|
8675
|
+
if (catchUpEvent) {
|
|
8676
|
+
dragGesture.dragViaPointer(catchUpEvent);
|
|
8677
|
+
}
|
|
8678
|
+
return dragGesture;
|
|
8679
|
+
};
|
|
8680
|
+
const dragAfterDistance = (grabEvent, dragGestureInitializer, threshold) => {
|
|
8681
|
+
const significantDragGestureController = createDragGestureController({
|
|
8682
|
+
threshold,
|
|
8683
|
+
// allow interaction for this intermediate gesture:
|
|
8684
|
+
// user should still be able to scroll or interact with the document
|
|
8685
|
+
// only once the gesture is significant we take control
|
|
8686
|
+
documentInteractions: "manual",
|
|
8687
|
+
onDragStart: gestureInfo => {
|
|
8688
|
+
significantDragGesture.release(); // kill that gesture
|
|
8689
|
+
startDragGesture(dragGestureInitializer, gestureInfo.dragEvent);
|
|
8690
|
+
}
|
|
8691
|
+
});
|
|
8692
|
+
const significantDragGesture = significantDragGestureController.grabViaPointer(grabEvent, {
|
|
8693
|
+
element: grabEvent.target
|
|
8694
|
+
});
|
|
8695
|
+
};
|
|
8696
|
+
const dragAfterLongPress = (grabEvent, dragGestureInitializer, {
|
|
8697
|
+
longPressDelay,
|
|
8698
|
+
longPressSlop,
|
|
8699
|
+
onPressStart,
|
|
8700
|
+
onPressCancel,
|
|
8701
|
+
onPress
|
|
8702
|
+
}) => {
|
|
8374
8703
|
const {
|
|
8375
|
-
|
|
8376
|
-
|
|
8377
|
-
|
|
8378
|
-
|
|
8379
|
-
|
|
8380
|
-
|
|
8381
|
-
|
|
8382
|
-
|
|
8383
|
-
|
|
8384
|
-
|
|
8385
|
-
documentInteractions = "auto",
|
|
8386
|
-
backdrop = true,
|
|
8387
|
-
backdropZIndex = 999999
|
|
8388
|
-
} = options;
|
|
8389
|
-
const dragGestureController = {
|
|
8390
|
-
grab: null,
|
|
8391
|
-
gravViaPointer: null
|
|
8704
|
+
pointerId,
|
|
8705
|
+
clientX,
|
|
8706
|
+
clientY
|
|
8707
|
+
} = grabEvent;
|
|
8708
|
+
const pressCleanupCallbacks = [];
|
|
8709
|
+
const endPress = () => {
|
|
8710
|
+
for (const pressCleanupCallback of pressCleanupCallbacks) {
|
|
8711
|
+
pressCleanupCallback();
|
|
8712
|
+
}
|
|
8713
|
+
pressCleanupCallbacks.length = 0;
|
|
8392
8714
|
};
|
|
8393
|
-
|
|
8394
|
-
|
|
8395
|
-
|
|
8396
|
-
|
|
8397
|
-
|
|
8398
|
-
|
|
8399
|
-
|
|
8400
|
-
|
|
8401
|
-
|
|
8402
|
-
|
|
8403
|
-
|
|
8404
|
-
|
|
8405
|
-
|
|
8715
|
+
|
|
8716
|
+
/*
|
|
8717
|
+
* A press held long enough IS the system's context-menu gesture: Android opens
|
|
8718
|
+
* its menu around 500ms, iOS its callout — both a tenth of a second after the
|
|
8719
|
+
* object has been picked up, landing on top of something the finger is already
|
|
8720
|
+
* carrying.
|
|
8721
|
+
* The listener goes on window, in capture: once the gesture runs, the drag
|
|
8722
|
+
* backdrop covers the page, so the contextmenu event is aimed at the backdrop
|
|
8723
|
+
* and never reaches the element being dragged.
|
|
8724
|
+
* It is removed on release — a right click with a mouse remains a right click.
|
|
8725
|
+
*/
|
|
8726
|
+
const preventContextMenu = contextMenuEvent => {
|
|
8727
|
+
contextMenuEvent.preventDefault();
|
|
8728
|
+
};
|
|
8729
|
+
window.addEventListener("contextmenu", preventContextMenu, true);
|
|
8730
|
+
pressCleanupCallbacks.push(() => {
|
|
8731
|
+
window.removeEventListener("contextmenu", preventContextMenu, true);
|
|
8732
|
+
});
|
|
8733
|
+
const countdownCleanupCallbacks = [];
|
|
8734
|
+
const endCountdown = () => {
|
|
8735
|
+
for (const countdownCleanupCallback of countdownCleanupCallbacks) {
|
|
8736
|
+
countdownCleanupCallback();
|
|
8406
8737
|
}
|
|
8407
|
-
|
|
8408
|
-
|
|
8738
|
+
countdownCleanupCallbacks.length = 0;
|
|
8739
|
+
};
|
|
8740
|
+
const timeout = setTimeout(() => {
|
|
8741
|
+
endCountdown();
|
|
8742
|
+
onPress?.(grabEvent);
|
|
8743
|
+
// Scrolling is taken away by the gesture itself, from the moment it starts
|
|
8744
|
+
// (see markAsStarted in drag_gesture.js) — one place refuses the touchmove,
|
|
8745
|
+
// for every way a drag can begin.
|
|
8746
|
+
const dragGesture = startDragGesture(dragGestureInitializer);
|
|
8747
|
+
if (!dragGesture) {
|
|
8748
|
+
endPress();
|
|
8749
|
+
return;
|
|
8409
8750
|
}
|
|
8410
|
-
|
|
8411
|
-
|
|
8412
|
-
|
|
8413
|
-
|
|
8414
|
-
|
|
8751
|
+
dragGesture.addReleaseCallback(endPress);
|
|
8752
|
+
}, longPressDelay);
|
|
8753
|
+
countdownCleanupCallbacks.push(() => {
|
|
8754
|
+
clearTimeout(timeout);
|
|
8755
|
+
});
|
|
8756
|
+
const cancelPress = pointerEvent => {
|
|
8757
|
+
endCountdown();
|
|
8758
|
+
endPress();
|
|
8759
|
+
onPressCancel?.(pointerEvent);
|
|
8760
|
+
};
|
|
8761
|
+
const onPointerMove = pointerMoveEvent => {
|
|
8762
|
+
if (pointerMoveEvent.pointerId !== pointerId) {
|
|
8763
|
+
return;
|
|
8415
8764
|
}
|
|
8416
|
-
|
|
8417
|
-
|
|
8765
|
+
const xDrift = Math.abs(pointerMoveEvent.clientX - clientX);
|
|
8766
|
+
const yDrift = Math.abs(pointerMoveEvent.clientY - clientY);
|
|
8767
|
+
if (xDrift < longPressSlop && yDrift < longPressSlop) {
|
|
8768
|
+
return;
|
|
8418
8769
|
}
|
|
8419
|
-
|
|
8420
|
-
|
|
8421
|
-
|
|
8422
|
-
|
|
8423
|
-
|
|
8424
|
-
|
|
8425
|
-
|
|
8426
|
-
|
|
8427
|
-
|
|
8428
|
-
|
|
8429
|
-
|
|
8430
|
-
|
|
8431
|
-
|
|
8432
|
-
|
|
8433
|
-
|
|
8434
|
-
|
|
8435
|
-
|
|
8436
|
-
|
|
8437
|
-
|
|
8438
|
-
|
|
8439
|
-
|
|
8440
|
-
|
|
8441
|
-
scrollableTop,
|
|
8442
|
-
// Position relative to container including scrolls
|
|
8443
|
-
left,
|
|
8444
|
-
top,
|
|
8445
|
-
// Delta since grab (number representing how much we dragged)
|
|
8446
|
-
xDelta: left - leftAtGrab,
|
|
8447
|
-
yDelta: top - topAtGrab
|
|
8448
|
-
};
|
|
8449
|
-
return layoutProps;
|
|
8450
|
-
};
|
|
8451
|
-
const grabLayout = createLayout(grabX + scrollContainer.scrollLeft, grabY + scrollContainer.scrollTop);
|
|
8452
|
-
const gestureInfo = {
|
|
8453
|
-
name,
|
|
8454
|
-
direction,
|
|
8455
|
-
started: !threshold,
|
|
8456
|
-
status: "grabbed",
|
|
8457
|
-
element,
|
|
8458
|
-
scrollContainer,
|
|
8459
|
-
grabX,
|
|
8460
|
-
// x grab coordinate (excluding scroll)
|
|
8461
|
-
grabY,
|
|
8462
|
-
// y grab coordinate (excluding scroll)
|
|
8463
|
-
grabLayout,
|
|
8464
|
-
leftAtGrab,
|
|
8465
|
-
topAtGrab,
|
|
8466
|
-
dragX: grabX,
|
|
8467
|
-
// coordinate of the last drag (excluding scroll of the scrollContainer)
|
|
8468
|
-
dragY: grabY,
|
|
8469
|
-
// coordinate of the last drag (excluding scroll of the scrollContainer)
|
|
8470
|
-
layout: grabLayout,
|
|
8471
|
-
isGoingUp: undefined,
|
|
8472
|
-
isGoingDown: undefined,
|
|
8473
|
-
isGoingLeft: undefined,
|
|
8474
|
-
isGoingRight: undefined,
|
|
8475
|
-
intentGoingUp: false,
|
|
8476
|
-
intentGoingDown: false,
|
|
8477
|
-
intentGoingLeft: false,
|
|
8478
|
-
intentGoingRight: false,
|
|
8479
|
-
// metadata about interaction sources
|
|
8480
|
-
grabEvent: event,
|
|
8481
|
-
dragEvent: null,
|
|
8482
|
-
releaseEvent: null
|
|
8483
|
-
};
|
|
8484
|
-
definePropertyAsReadOnly(gestureInfo, "name");
|
|
8485
|
-
definePropertyAsReadOnly(gestureInfo, "direction");
|
|
8486
|
-
definePropertyAsReadOnly(gestureInfo, "scrollContainer");
|
|
8487
|
-
definePropertyAsReadOnly(gestureInfo, "grabX");
|
|
8488
|
-
definePropertyAsReadOnly(gestureInfo, "grabY");
|
|
8489
|
-
definePropertyAsReadOnly(gestureInfo, "grabLayout");
|
|
8490
|
-
definePropertyAsReadOnly(gestureInfo, "leftAtGrab");
|
|
8491
|
-
definePropertyAsReadOnly(gestureInfo, "topAtGrab");
|
|
8492
|
-
definePropertyAsReadOnly(gestureInfo, "grabEvent");
|
|
8493
|
-
document_interactions: {
|
|
8494
|
-
if (documentInteractions === "manual") {
|
|
8495
|
-
break document_interactions;
|
|
8496
|
-
}
|
|
8497
|
-
/*
|
|
8498
|
-
GOAL: Take control of document-level interactions during drag gestures
|
|
8499
|
-
|
|
8500
|
-
WHY: During drag operations, we need to prevent conflicting user interactions that would:
|
|
8501
|
-
1. Interfere with the drag gesture (competing pointer events, focus changes)
|
|
8502
|
-
2. Break the visual feedback (inconsistent cursors, hover states)
|
|
8503
|
-
3. Cause unwanted scrolling (keyboard shortcuts, wheel events in restricted directions)
|
|
8504
|
-
4. Create accessibility issues (focus jumping, screen reader confusion)
|
|
8505
|
-
STRATEGY: Create a controlled interaction environment by:
|
|
8506
|
-
1. VISUAL CONTROL: Use a backdrop to unify cursor appearance and block pointer events
|
|
8507
|
-
2. INTERACTION ISOLATION: Make non-dragged elements inert to prevent interference
|
|
8508
|
-
3. FOCUS MANAGEMENT: Control focus location and prevent focus changes during drag
|
|
8509
|
-
4. SELECTIVE SCROLLING: Allow scrolling only in directions supported by the drag gesture
|
|
8510
|
-
IMPLEMENTATION:
|
|
8511
|
-
*/
|
|
8512
|
-
|
|
8513
|
-
// 1. INTERACTION ISOLATION: Make everything except the dragged element inert
|
|
8514
|
-
// This prevents keyboard events, pointer interactions, and screen reader navigation
|
|
8515
|
-
// on non-relevant elements during the drag operation
|
|
8516
|
-
const cleanupInert = isolateInteractions([element, ...Array.from(document.querySelectorAll("[data-droppable]"))]);
|
|
8517
|
-
addReleaseCallback(() => {
|
|
8518
|
-
cleanupInert();
|
|
8519
|
-
});
|
|
8520
|
-
|
|
8521
|
-
// 2. VISUAL CONTROL: Backdrop for consistent cursor and pointer event blocking
|
|
8522
|
-
if (backdrop) {
|
|
8523
|
-
import.meta.css = [css$4, "@jsenv/dom/src/interaction/drag/drag_gesture.js"];
|
|
8524
|
-
const backdropElement = document.createElement("div");
|
|
8525
|
-
backdropElement.className = "navi_drag_gesture_backdrop";
|
|
8526
|
-
backdropElement.ariaHidden = "true";
|
|
8527
|
-
backdropElement.setAttribute("data-backdrop", "");
|
|
8528
|
-
backdropElement.style.zIndex = backdropZIndex;
|
|
8529
|
-
backdropElement.style.cursor = cursor;
|
|
8530
|
-
|
|
8531
|
-
// Handle wheel events on backdrop for directionally-constrained drag gestures
|
|
8532
|
-
// (e.g., table column resize should only allow horizontal scrolling)
|
|
8533
|
-
if (!direction.x || !direction.y) {
|
|
8534
|
-
backdropElement.onwheel = e => {
|
|
8535
|
-
e.preventDefault();
|
|
8536
|
-
const scrollX = direction.x ? e.deltaX : 0;
|
|
8537
|
-
const scrollY = direction.y ? e.deltaY : 0;
|
|
8538
|
-
scrollContainer.scrollBy({
|
|
8539
|
-
left: scrollX,
|
|
8540
|
-
top: scrollY,
|
|
8541
|
-
behavior: "auto"
|
|
8542
|
-
});
|
|
8543
|
-
};
|
|
8544
|
-
}
|
|
8545
|
-
document.body.appendChild(backdropElement);
|
|
8546
|
-
addReleaseCallback(() => {
|
|
8547
|
-
backdropElement.remove();
|
|
8548
|
-
});
|
|
8549
|
-
}
|
|
8550
|
-
|
|
8551
|
-
// 3. FOCUS MANAGEMENT: Control and stabilize focus during drag
|
|
8552
|
-
const {
|
|
8553
|
-
activeElement
|
|
8554
|
-
} = document;
|
|
8555
|
-
const focusableElement = findFocusable(element);
|
|
8556
|
-
// Focus the dragged element (or document.body as fallback) to establish clear focus context
|
|
8557
|
-
// This also ensure any keydown event listened by the currently focused element
|
|
8558
|
-
// won't be available during drag
|
|
8559
|
-
const elementToFocus = focusableElement || document.body;
|
|
8560
|
-
elementToFocus.focus({
|
|
8561
|
-
preventScroll: true
|
|
8562
|
-
});
|
|
8563
|
-
addReleaseCallback(() => {
|
|
8564
|
-
// Restore original focus on release
|
|
8565
|
-
activeElement.focus({
|
|
8566
|
-
preventScroll: true
|
|
8567
|
-
});
|
|
8568
|
-
});
|
|
8569
|
-
// Prevent Tab navigation entirely (focus should stay stable)
|
|
8570
|
-
const onkeydown = e => {
|
|
8571
|
-
if (e.key === "Tab") {
|
|
8572
|
-
e.preventDefault();
|
|
8573
|
-
return;
|
|
8574
|
-
}
|
|
8575
|
-
};
|
|
8576
|
-
document.addEventListener("keydown", onkeydown);
|
|
8577
|
-
addReleaseCallback(() => {
|
|
8578
|
-
document.removeEventListener("keydown", onkeydown);
|
|
8579
|
-
});
|
|
8770
|
+
// The finger is going somewhere: it is scrolling the page, or running down
|
|
8771
|
+
// the list. Letting the countdown survive would unhook an object in passing.
|
|
8772
|
+
cancelPress(pointerMoveEvent);
|
|
8773
|
+
};
|
|
8774
|
+
const onPointerEnd = pointerEndEvent => {
|
|
8775
|
+
if (pointerEndEvent.pointerId !== pointerId) {
|
|
8776
|
+
return;
|
|
8777
|
+
}
|
|
8778
|
+
cancelPress(pointerEndEvent);
|
|
8779
|
+
};
|
|
8780
|
+
// On window rather than on the element: the finger can leave it, and the
|
|
8781
|
+
// element itself can be taken out of the document while the press is waiting.
|
|
8782
|
+
window.addEventListener("pointermove", onPointerMove);
|
|
8783
|
+
window.addEventListener("pointerup", onPointerEnd);
|
|
8784
|
+
window.addEventListener("pointercancel", onPointerEnd);
|
|
8785
|
+
countdownCleanupCallbacks.push(() => {
|
|
8786
|
+
window.removeEventListener("pointermove", onPointerMove);
|
|
8787
|
+
window.removeEventListener("pointerup", onPointerEnd);
|
|
8788
|
+
window.removeEventListener("pointercancel", onPointerEnd);
|
|
8789
|
+
});
|
|
8790
|
+
onPressStart?.(grabEvent);
|
|
8791
|
+
};
|
|
8580
8792
|
|
|
8581
|
-
|
|
8582
|
-
|
|
8583
|
-
|
|
8584
|
-
|
|
8585
|
-
|
|
8586
|
-
|
|
8587
|
-
|
|
8588
|
-
|
|
8589
|
-
|
|
8590
|
-
|
|
8591
|
-
|
|
8592
|
-
|
|
8593
|
-
|
|
8594
|
-
|
|
8595
|
-
|
|
8596
|
-
|
|
8597
|
-
|
|
8598
|
-
|
|
8599
|
-
|
|
8600
|
-
|
|
8601
|
-
|
|
8602
|
-
|
|
8603
|
-
|
|
8793
|
+
/**
|
|
8794
|
+
* The element `element` is genuinely `position: absolute`/`fixed` relative
|
|
8795
|
+
* to: its own nearest positioned ancestor (walking up the DOM tree), or
|
|
8796
|
+
* `document.documentElement` (the viewport) if none is found.
|
|
8797
|
+
*
|
|
8798
|
+
* Also aware of `element` itself being promoted to the top layer: a
|
|
8799
|
+
* `<dialog>` actually shown modally (`showModal()`, matches `:modal` — a
|
|
8800
|
+
* `.show()`'d, non-modal dialog does NOT match and is positioned like any
|
|
8801
|
+
* other in-flow element instead, walked up normally below), or *any*
|
|
8802
|
+
* `[popover]` element, always uses the initial containing block (the
|
|
8803
|
+
* viewport) regardless of its own `position` or DOM ancestry — walking up
|
|
8804
|
+
* its own parent chain (what the rest of this function does) would give
|
|
8805
|
+
* the wrong answer for these two specifically, since their real DOM
|
|
8806
|
+
* position becomes irrelevant to their own containing block the moment
|
|
8807
|
+
* they're actually promoted. Checked via the `popover` attribute itself,
|
|
8808
|
+
* not the live `:popover-open` state — unlike `<dialog>`, a `[popover]`
|
|
8809
|
+
* element has no "local" mode: it's always top-layer-bound once shown,
|
|
8810
|
+
* regardless of whether it happens to be open right this moment, so the
|
|
8811
|
+
* static attribute alone is enough (and correct even when called just
|
|
8812
|
+
* before `showPopover()` actually runs, when `:popover-open` isn't true
|
|
8813
|
+
* yet).
|
|
8814
|
+
*
|
|
8815
|
+
* `document.documentElement` (not `document.body`, not `null`) is this
|
|
8816
|
+
* function's own "no real container — use the viewport" sentinel:
|
|
8817
|
+
* `documentElement` is the actual initial containing block, so the walk
|
|
8818
|
+
* below stops there without testing its own `position` (there's nothing
|
|
8819
|
+
* beyond it to fall back to anyway) — unlike the previous version of this
|
|
8820
|
+
* function, which stopped one level too early, at `document.body`, without
|
|
8821
|
+
* ever testing *its* `position` either (a `position: relative` body, for
|
|
8822
|
+
* instance, would have been silently skipped). Returning `documentElement`
|
|
8823
|
+
* instead of `null` also means no special-casing is needed by callers that
|
|
8824
|
+
* already compare a resolved container against `document.documentElement`
|
|
8825
|
+
* (see e.g. visible_rect.js's own `hasRealContainer` check).
|
|
8826
|
+
*/
|
|
8827
|
+
const getPositionedParent = (element) => {
|
|
8828
|
+
const isPromotedToTopLayer =
|
|
8829
|
+
(element.tagName === "DIALOG" && element.matches(":modal")) ||
|
|
8830
|
+
element.hasAttribute("popover");
|
|
8831
|
+
if (isPromotedToTopLayer) {
|
|
8832
|
+
return document.documentElement;
|
|
8833
|
+
}
|
|
8834
|
+
let parent = element.parentElement;
|
|
8835
|
+
while (parent && parent !== document.documentElement) {
|
|
8836
|
+
const position = window.getComputedStyle(parent).position;
|
|
8837
|
+
if (
|
|
8838
|
+
position === "relative" ||
|
|
8839
|
+
position === "absolute" ||
|
|
8840
|
+
position === "fixed"
|
|
8841
|
+
) {
|
|
8842
|
+
return parent;
|
|
8604
8843
|
}
|
|
8844
|
+
parent = parent.parentElement;
|
|
8845
|
+
}
|
|
8846
|
+
return document.documentElement;
|
|
8847
|
+
};
|
|
8605
8848
|
|
|
8606
|
-
|
|
8607
|
-
|
|
8608
|
-
|
|
8609
|
-
|
|
8610
|
-
|
|
8611
|
-
|
|
8612
|
-
|
|
8613
|
-
|
|
8614
|
-
|
|
8615
|
-
|
|
8616
|
-
|
|
8617
|
-
|
|
8618
|
-
|
|
8619
|
-
|
|
8620
|
-
|
|
8621
|
-
|
|
8622
|
-
|
|
8623
|
-
|
|
8624
|
-
|
|
8625
|
-
passive: true
|
|
8626
|
-
});
|
|
8627
|
-
});
|
|
8849
|
+
/**
|
|
8850
|
+
* Walks `element` and its ancestors (stopping at, but not including,
|
|
8851
|
+
* `document.documentElement`) looking for the first one whose *computed*
|
|
8852
|
+
* `position` is `fixed` — i.e. pinned to the viewport, ignoring document
|
|
8853
|
+
* scroll, regardless of what `element` itself is positioned relative to.
|
|
8854
|
+
*
|
|
8855
|
+
* @param {Element} element
|
|
8856
|
+
* @returns {[left: number, top: number] | null} The fixed ancestor's own
|
|
8857
|
+
* viewport-relative `getBoundingClientRect()` origin, or `null` if neither
|
|
8858
|
+
* `element` nor any ancestor is fixed (i.e. `element` genuinely scrolls
|
|
8859
|
+
* with the document).
|
|
8860
|
+
*/
|
|
8861
|
+
const findSelfOrAncestorFixedPosition = (element) => {
|
|
8862
|
+
let current = element;
|
|
8863
|
+
while (true) {
|
|
8864
|
+
const computedStyle = window.getComputedStyle(current);
|
|
8865
|
+
if (computedStyle.position === "fixed") {
|
|
8866
|
+
const { left, top } = current.getBoundingClientRect();
|
|
8867
|
+
return [left, top];
|
|
8628
8868
|
}
|
|
8629
|
-
|
|
8630
|
-
|
|
8631
|
-
|
|
8632
|
-
|
|
8633
|
-
|
|
8634
|
-
|
|
8635
|
-
|
|
8636
|
-
|
|
8637
|
-
|
|
8638
|
-
|
|
8639
|
-
|
|
8640
|
-
|
|
8641
|
-
|
|
8642
|
-
|
|
8643
|
-
|
|
8644
|
-
|
|
8645
|
-
|
|
8646
|
-
|
|
8647
|
-
|
|
8648
|
-
|
|
8649
|
-
|
|
8650
|
-
|
|
8651
|
-
|
|
8652
|
-
|
|
8653
|
-
|
|
8654
|
-
|
|
8655
|
-
|
|
8656
|
-
|
|
8657
|
-
|
|
8658
|
-
|
|
8659
|
-
|
|
8660
|
-
|
|
8661
|
-
|
|
8662
|
-
|
|
8663
|
-
|
|
8664
|
-
|
|
8665
|
-
|
|
8666
|
-
|
|
8667
|
-
|
|
8668
|
-
|
|
8669
|
-
|
|
8670
|
-
|
|
8671
|
-
|
|
8672
|
-
|
|
8673
|
-
|
|
8674
|
-
|
|
8675
|
-
|
|
8676
|
-
|
|
8677
|
-
|
|
8678
|
-
|
|
8679
|
-
|
|
8680
|
-
|
|
8681
|
-
|
|
8682
|
-
|
|
8683
|
-
|
|
8684
|
-
|
|
8685
|
-
|
|
8686
|
-
|
|
8687
|
-
|
|
8688
|
-
|
|
8689
|
-
|
|
8690
|
-
|
|
8691
|
-
|
|
8692
|
-
|
|
8693
|
-
|
|
8694
|
-
|
|
8695
|
-
|
|
8696
|
-
|
|
8697
|
-
|
|
8698
|
-
|
|
8699
|
-
|
|
8700
|
-
|
|
8701
|
-
|
|
8702
|
-
|
|
8703
|
-
|
|
8704
|
-
|
|
8705
|
-
|
|
8706
|
-
|
|
8707
|
-
|
|
8708
|
-
|
|
8709
|
-
|
|
8710
|
-
|
|
8711
|
-
|
|
8712
|
-
|
|
8713
|
-
|
|
8714
|
-
|
|
8715
|
-
|
|
8716
|
-
|
|
8717
|
-
|
|
8718
|
-
|
|
8719
|
-
|
|
8720
|
-
|
|
8721
|
-
|
|
8722
|
-
|
|
8723
|
-
|
|
8724
|
-
|
|
8725
|
-
|
|
8726
|
-
|
|
8727
|
-
|
|
8728
|
-
|
|
8729
|
-
|
|
8730
|
-
|
|
8731
|
-
|
|
8732
|
-
|
|
8733
|
-
|
|
8734
|
-
|
|
8735
|
-
|
|
8736
|
-
|
|
8737
|
-
|
|
8738
|
-
|
|
8739
|
-
|
|
8740
|
-
|
|
8741
|
-
|
|
8742
|
-
|
|
8743
|
-
|
|
8744
|
-
|
|
8745
|
-
|
|
8746
|
-
|
|
8747
|
-
|
|
8748
|
-
|
|
8749
|
-
|
|
8750
|
-
|
|
8751
|
-
|
|
8752
|
-
|
|
8753
|
-
|
|
8754
|
-
|
|
8755
|
-
|
|
8756
|
-
|
|
8757
|
-
|
|
8758
|
-
|
|
8759
|
-
|
|
8760
|
-
|
|
8761
|
-
|
|
8762
|
-
}
|
|
8763
|
-
const someLayoutChange = gestureInfo.layout !== layoutPrevious;
|
|
8764
|
-
dispatchPublicCustomEvent(element, "navi_drag", {
|
|
8765
|
-
gestureInfo,
|
|
8766
|
-
someLayoutChange
|
|
8767
|
-
});
|
|
8768
|
-
publishDrag(gestureInfo,
|
|
8769
|
-
// we still publish drag event even when unchanged
|
|
8770
|
-
// because UI might need to adjust when document scrolls
|
|
8771
|
-
// even if nothing truly changes visually the element
|
|
8772
|
-
// can decide to stick to the scroll for example
|
|
8773
|
-
someLayoutChange);
|
|
8774
|
-
};
|
|
8775
|
-
const release = ({
|
|
8776
|
-
event = new CustomEvent("programmatic"),
|
|
8777
|
-
releaseX = gestureInfo.dragX,
|
|
8778
|
-
releaseY = gestureInfo.dragY
|
|
8779
|
-
} = {}) => {
|
|
8780
|
-
drag(releaseX, releaseY, {
|
|
8781
|
-
event,
|
|
8782
|
-
isRelease: true
|
|
8783
|
-
});
|
|
8784
|
-
dispatchPublicCustomEvent(element, "navi_drag_release", {
|
|
8785
|
-
gestureInfo
|
|
8786
|
-
});
|
|
8787
|
-
publishRelease(gestureInfo);
|
|
8869
|
+
current = current.parentElement;
|
|
8870
|
+
if (!current || current === document.documentElement) {
|
|
8871
|
+
break;
|
|
8872
|
+
}
|
|
8873
|
+
}
|
|
8874
|
+
return null;
|
|
8875
|
+
};
|
|
8876
|
+
|
|
8877
|
+
/**
|
|
8878
|
+
* Creates a coordinate system positioner for drag operations.
|
|
8879
|
+
*
|
|
8880
|
+
* PURPOSE:
|
|
8881
|
+
* During a drag gesture, the system tracks mouse movement as "scrollable coordinates"
|
|
8882
|
+
* relative to the scroll container. This function converts those coordinates into
|
|
8883
|
+
* the actual CSS transform values needed to visually move an element (or a separate
|
|
8884
|
+
* elementToMove) to follow the mouse.
|
|
8885
|
+
*
|
|
8886
|
+
* PARAMETERS:
|
|
8887
|
+
* - element: The element being grabbed / tracked for drag detection and auto-scroll.
|
|
8888
|
+
* - referenceElement: Optional. The element whose coordinate system defines the input space.
|
|
8889
|
+
* When provided, scrollable coords are relative to its scroll container.
|
|
8890
|
+
* Defaults to element itself.
|
|
8891
|
+
* - elementToMove: Optional. A different element to apply the transform to (e.g. a clone
|
|
8892
|
+
* or a table that moves as a whole when a column is dragged).
|
|
8893
|
+
* When provided, its offsetParent is used as the positioning context.
|
|
8894
|
+
*
|
|
8895
|
+
* THE COORDINATE PIPELINE:
|
|
8896
|
+
*
|
|
8897
|
+
* Mouse position
|
|
8898
|
+
* → scrollable coords (relative to referenceScrollContainer, scroll-independent)
|
|
8899
|
+
* → positioned coords (relative to elementToMove's offsetParent, for CSS transform)
|
|
8900
|
+
*
|
|
8901
|
+
* Two types of offsets bridge these spaces:
|
|
8902
|
+
*
|
|
8903
|
+
* 1. POSITION OFFSETS (getPositionOffsets):
|
|
8904
|
+
* Compensate for the fact that positionedParent and referencePositionedParent
|
|
8905
|
+
* may differ. For example, if `element` lives inside a <table> and `elementToMove`
|
|
8906
|
+
* is a full table clone, their offsetParents are different elements.
|
|
8907
|
+
* This offset is the spatial difference between those two positioned ancestors.
|
|
8908
|
+
* Called dynamically because parents can move (e.g. overlay elements).
|
|
8909
|
+
*
|
|
8910
|
+
* 2. SCROLL OFFSETS (getScrollOffsets):
|
|
8911
|
+
* Account for the scroll position of the relevant scroll container(s).
|
|
8912
|
+
* The math ensures that at grab time, the transform delta is zero (element
|
|
8913
|
+
* stays at its visual position), and subsequent mouse movement maps 1:1
|
|
8914
|
+
* to transform change.
|
|
8915
|
+
*
|
|
8916
|
+
* CRITICAL CASE — positionedParent outside referenceScrollContainer:
|
|
8917
|
+
* When elementToMove's offsetParent is NOT inside the referenceScrollContainer
|
|
8918
|
+
* (e.g. a clone appended to document.body while tracking an element inside
|
|
8919
|
+
* an overflow:auto div), the scroll offset must be FROZEN at grab time.
|
|
8920
|
+
* Using a live scroll value would double-move the clone during auto-scroll:
|
|
8921
|
+
* the scrollable coordinate decreases (element appears to move up) AND the
|
|
8922
|
+
* live scroll value increases — both applied to the same transform.
|
|
8923
|
+
* Freezing the scroll at grab time cancels this out while still correctly
|
|
8924
|
+
* placing the clone at the right initial position.
|
|
8925
|
+
*
|
|
8926
|
+
* KEY SCENARIOS SUPPORTED:
|
|
8927
|
+
* 1. Same positioned parent, same scroll container — minimal offsets
|
|
8928
|
+
* 2. Different positioned parents, same scroll container — position offset compensation
|
|
8929
|
+
* 3. Same positioned parent, different scroll containers — scroll offset bridging
|
|
8930
|
+
* 4. Different positioned parents, different containers — full offset compensation
|
|
8931
|
+
* 5. Overlay elements (data-overlay-for) — specialized offset path
|
|
8932
|
+
* 6. Fixed positioned elements — special scroll handling
|
|
8933
|
+
* 7. elementToMove outside referenceScrollContainer — frozen scroll offset at grab
|
|
8934
|
+
*
|
|
8935
|
+
* API CONTRACT:
|
|
8936
|
+
* Returns [scrollableLeft, scrollableTop, convertScrollablePosition] where:
|
|
8937
|
+
*
|
|
8938
|
+
* - scrollableLeft/scrollableTop:
|
|
8939
|
+
* The element's current position in the reference coordinate system at grab time.
|
|
8940
|
+
* Used as the layout starting point (layoutScrollableLeft/Top) by the gesture system.
|
|
8941
|
+
*
|
|
8942
|
+
* - convertScrollablePosition(scrollableLeft, scrollableTop):
|
|
8943
|
+
* Converts a scrollable coordinate (from the gesture layout) into a positioned
|
|
8944
|
+
* coordinate suitable for CSS transform. The gesture system computes:
|
|
8945
|
+
* topDelta = convertScrollablePosition(layout.scrollableTop) - topAtGrab
|
|
8946
|
+
* and applies that as translateY. At grab time, delta = 0. As the mouse moves,
|
|
8947
|
+
* delta tracks the movement exactly, regardless of scroll context differences.
|
|
8948
|
+
*/
|
|
8949
|
+
const createDragElementPositioner = (
|
|
8950
|
+
element,
|
|
8951
|
+
referenceElement,
|
|
8952
|
+
elementToMove,
|
|
8953
|
+
) => {
|
|
8954
|
+
let scrollableLeft;
|
|
8955
|
+
let scrollableTop;
|
|
8956
|
+
let convertScrollablePosition;
|
|
8957
|
+
|
|
8958
|
+
// getPositionedParent, not raw .offsetParent — offsetParent is null for a
|
|
8959
|
+
// position: fixed element, and also for one promoted to the top layer
|
|
8960
|
+
// (e.g. a <dialog>/[popover] being dragged by its own handle), which
|
|
8961
|
+
// crashes the fixed-position lookup below (findSelfOrAncestorFixedPosition
|
|
8962
|
+
// assumes a real starting element, not null). getPositionedParent never
|
|
8963
|
+
// returns null (document.documentElement instead — see its own doc).
|
|
8964
|
+
const positionedParent = getPositionedParent(elementToMove || element);
|
|
8965
|
+
const scrollContainer = getScrollContainer(element);
|
|
8966
|
+
const [getPositionOffsets, getScrollOffsets] = createGetOffsets({
|
|
8967
|
+
positionedParent,
|
|
8968
|
+
referencePositionedParent: referenceElement
|
|
8969
|
+
? getPositionedParent(referenceElement)
|
|
8970
|
+
: positionedParent,
|
|
8971
|
+
scrollContainer,
|
|
8972
|
+
referenceScrollContainer: referenceElement
|
|
8973
|
+
? getScrollContainer(referenceElement)
|
|
8974
|
+
: scrollContainer,
|
|
8975
|
+
});
|
|
8976
|
+
|
|
8977
|
+
{
|
|
8978
|
+
[scrollableLeft, scrollableTop] = getScrollablePosition(
|
|
8979
|
+
element,
|
|
8980
|
+
scrollContainer,
|
|
8981
|
+
);
|
|
8982
|
+
const [positionOffsetLeft, positionOffsetTop] = getPositionOffsets();
|
|
8983
|
+
scrollableLeft += positionOffsetLeft;
|
|
8984
|
+
scrollableTop += positionOffsetTop;
|
|
8985
|
+
}
|
|
8986
|
+
{
|
|
8987
|
+
convertScrollablePosition = (
|
|
8988
|
+
scrollableLeftToConvert,
|
|
8989
|
+
scrollableTopToConvert,
|
|
8990
|
+
) => {
|
|
8991
|
+
const [positionOffsetLeft, positionOffsetTop] = getPositionOffsets();
|
|
8992
|
+
const [scrollOffsetLeft, scrollOffsetTop] = getScrollOffsets();
|
|
8993
|
+
|
|
8994
|
+
const positionedLeftWithoutScroll =
|
|
8995
|
+
scrollableLeftToConvert + positionOffsetLeft;
|
|
8996
|
+
const positionedTopWithoutScroll =
|
|
8997
|
+
scrollableTopToConvert + positionOffsetTop;
|
|
8998
|
+
const positionedLeft = positionedLeftWithoutScroll + scrollOffsetLeft;
|
|
8999
|
+
const positionedTop = positionedTopWithoutScroll + scrollOffsetTop;
|
|
9000
|
+
|
|
9001
|
+
return [positionedLeft, positionedTop];
|
|
8788
9002
|
};
|
|
8789
|
-
|
|
8790
|
-
|
|
8791
|
-
|
|
8792
|
-
|
|
8793
|
-
|
|
8794
|
-
|
|
8795
|
-
|
|
8796
|
-
|
|
8797
|
-
|
|
8798
|
-
|
|
8799
|
-
|
|
9003
|
+
}
|
|
9004
|
+
return [scrollableLeft, scrollableTop, convertScrollablePosition];
|
|
9005
|
+
};
|
|
9006
|
+
|
|
9007
|
+
const getScrollablePosition = (element, scrollContainer) => {
|
|
9008
|
+
const { left: elementViewportLeft, top: elementViewportTop } =
|
|
9009
|
+
element.getBoundingClientRect();
|
|
9010
|
+
const scrollContainerIsDocument = scrollContainer === documentElement;
|
|
9011
|
+
if (scrollContainerIsDocument) {
|
|
9012
|
+
return [elementViewportLeft, elementViewportTop];
|
|
9013
|
+
}
|
|
9014
|
+
const { left: scrollContainerLeft, top: scrollContainerTop } =
|
|
9015
|
+
scrollContainer.getBoundingClientRect();
|
|
9016
|
+
const scrollableLeft = elementViewportLeft - scrollContainerLeft;
|
|
9017
|
+
const scrollableTop = elementViewportTop - scrollContainerTop;
|
|
9018
|
+
|
|
9019
|
+
return [scrollableLeft, scrollableTop];
|
|
9020
|
+
};
|
|
9021
|
+
|
|
9022
|
+
const createGetOffsets = ({
|
|
9023
|
+
positionedParent,
|
|
9024
|
+
referencePositionedParent,
|
|
9025
|
+
scrollContainer,
|
|
9026
|
+
referenceScrollContainer,
|
|
9027
|
+
}) => {
|
|
9028
|
+
const samePositionedParent = positionedParent === referencePositionedParent;
|
|
9029
|
+
const getScrollOffsets = createGetScrollOffsets(
|
|
9030
|
+
scrollContainer,
|
|
9031
|
+
referenceScrollContainer,
|
|
9032
|
+
positionedParent,
|
|
9033
|
+
samePositionedParent,
|
|
9034
|
+
);
|
|
9035
|
+
|
|
9036
|
+
if (samePositionedParent) {
|
|
9037
|
+
return [() => [0, 0], getScrollOffsets];
|
|
9038
|
+
}
|
|
9039
|
+
|
|
9040
|
+
// parents are different, oh boy let's go
|
|
9041
|
+
// The overlay case is problematic because the overlay adjust its position to the target dynamically
|
|
9042
|
+
// This creates something complex to support properly.
|
|
9043
|
+
// When overlay is fixed we there will never be any offset
|
|
9044
|
+
// When overlay is absolute there is a diff relative to the scroll
|
|
9045
|
+
// and eventually if the overlay is positioned differently than the other parent
|
|
9046
|
+
if (isOverlayOf(positionedParent, referencePositionedParent)) {
|
|
9047
|
+
return createGetOffsetsForOverlay(
|
|
9048
|
+
positionedParent,
|
|
9049
|
+
referencePositionedParent,
|
|
9050
|
+
{
|
|
9051
|
+
scrollContainer,
|
|
9052
|
+
referenceScrollContainer,
|
|
9053
|
+
getScrollOffsets,
|
|
9054
|
+
},
|
|
9055
|
+
);
|
|
9056
|
+
}
|
|
9057
|
+
if (isOverlayOf(referencePositionedParent, positionedParent)) {
|
|
9058
|
+
return createGetOffsetsForOverlay(
|
|
9059
|
+
referencePositionedParent,
|
|
9060
|
+
positionedParent,
|
|
9061
|
+
{
|
|
9062
|
+
scrollContainer,
|
|
9063
|
+
referenceScrollContainer,
|
|
9064
|
+
getScrollOffsets,
|
|
9065
|
+
},
|
|
9066
|
+
);
|
|
9067
|
+
}
|
|
9068
|
+
const scrollContainerIsDocument = scrollContainer === documentElement;
|
|
9069
|
+
if (scrollContainerIsDocument) {
|
|
9070
|
+
// Document case: getBoundingClientRect already includes document scroll effects
|
|
9071
|
+
// Add current scroll position to get the static offset
|
|
9072
|
+
const getPositionOffsetsDocumentScrolling = () => {
|
|
9073
|
+
const { scrollLeft: documentScrollLeft, scrollTop: documentScrollTop } =
|
|
9074
|
+
scrollContainer;
|
|
9075
|
+
const aRect = positionedParent.getBoundingClientRect();
|
|
9076
|
+
const bRect = referencePositionedParent.getBoundingClientRect();
|
|
9077
|
+
const aLeft = aRect.left;
|
|
9078
|
+
const aTop = aRect.top;
|
|
9079
|
+
const bLeft = bRect.left;
|
|
9080
|
+
const bTop = bRect.top;
|
|
9081
|
+
const aLeftDocument = documentScrollLeft + aLeft;
|
|
9082
|
+
const aTopDocument = documentScrollTop + aTop;
|
|
9083
|
+
const bLeftDocument = documentScrollLeft + bLeft;
|
|
9084
|
+
const bTopDocument = documentScrollTop + bTop;
|
|
9085
|
+
const offsetLeft = bLeftDocument - aLeftDocument;
|
|
9086
|
+
const offsetTop = bTopDocument - aTopDocument;
|
|
9087
|
+
return [offsetLeft, offsetTop];
|
|
8800
9088
|
};
|
|
8801
|
-
return
|
|
9089
|
+
return [getPositionOffsetsDocumentScrolling, getScrollOffsets];
|
|
9090
|
+
}
|
|
9091
|
+
// Custom scroll container case: account for container's position and scroll
|
|
9092
|
+
const getPositionOffsetsCustomScrollContainer = () => {
|
|
9093
|
+
const aRect = positionedParent.getBoundingClientRect();
|
|
9094
|
+
const bRect = referencePositionedParent.getBoundingClientRect();
|
|
9095
|
+
const aLeft = aRect.left;
|
|
9096
|
+
const aTop = aRect.top;
|
|
9097
|
+
const bLeft = bRect.left;
|
|
9098
|
+
const bTop = bRect.top;
|
|
9099
|
+
|
|
9100
|
+
const scrollContainerRect = scrollContainer.getBoundingClientRect();
|
|
9101
|
+
const offsetLeft =
|
|
9102
|
+
bLeft - aLeft + scrollContainer.scrollLeft - scrollContainerRect.left;
|
|
9103
|
+
const offsetTop =
|
|
9104
|
+
bTop - aTop + scrollContainer.scrollTop - scrollContainerRect.top;
|
|
9105
|
+
return [offsetLeft, offsetTop];
|
|
8802
9106
|
};
|
|
8803
|
-
|
|
8804
|
-
|
|
8805
|
-
|
|
8806
|
-
|
|
8807
|
-
|
|
8808
|
-
|
|
8809
|
-
|
|
8810
|
-
|
|
8811
|
-
|
|
9107
|
+
return [getPositionOffsetsCustomScrollContainer, getScrollOffsets];
|
|
9108
|
+
};
|
|
9109
|
+
const createGetOffsetsForOverlay = (
|
|
9110
|
+
overlay,
|
|
9111
|
+
overlayTarget,
|
|
9112
|
+
{ scrollContainer, referenceScrollContainer, getScrollOffsets },
|
|
9113
|
+
) => {
|
|
9114
|
+
const sameScrollContainer = scrollContainer === referenceScrollContainer;
|
|
9115
|
+
const scrollContainerIsDocument =
|
|
9116
|
+
scrollContainer === document.documentElement;
|
|
9117
|
+
const referenceScrollContainerIsDocument =
|
|
9118
|
+
referenceScrollContainer === documentElement;
|
|
9119
|
+
|
|
9120
|
+
if (getComputedStyle(overlay).position === "fixed") {
|
|
9121
|
+
if (referenceScrollContainerIsDocument) {
|
|
9122
|
+
const getPositionOffsetsFixedOverlay = () => {
|
|
9123
|
+
return [0, 0];
|
|
9124
|
+
};
|
|
9125
|
+
return [getPositionOffsetsFixedOverlay, getScrollOffsets];
|
|
8812
9126
|
}
|
|
8813
|
-
const
|
|
8814
|
-
const
|
|
8815
|
-
|
|
8816
|
-
|
|
8817
|
-
|
|
8818
|
-
|
|
8819
|
-
|
|
8820
|
-
|
|
8821
|
-
|
|
8822
|
-
|
|
8823
|
-
|
|
8824
|
-
|
|
8825
|
-
|
|
8826
|
-
|
|
8827
|
-
|
|
8828
|
-
|
|
8829
|
-
dragGesture.drag(mouseDragX, mouseDragY, {
|
|
8830
|
-
event: dragEvent
|
|
8831
|
-
});
|
|
8832
|
-
};
|
|
8833
|
-
const releaseViaPointer = mouseupEvent => {
|
|
8834
|
-
const [mouseReleaseX, mouseReleaseY] = mouseEventCoords(mouseupEvent);
|
|
8835
|
-
dragGesture.release({
|
|
8836
|
-
event: mouseupEvent,
|
|
8837
|
-
releaseX: mouseReleaseX,
|
|
8838
|
-
releaseY: mouseReleaseY
|
|
8839
|
-
});
|
|
9127
|
+
const getPositionOffsetsFixedOverlay = () => {
|
|
9128
|
+
const scrollContainerRect = scrollContainer.getBoundingClientRect();
|
|
9129
|
+
const referenceScrollContainerRect =
|
|
9130
|
+
referenceScrollContainer.getBoundingClientRect();
|
|
9131
|
+
let offsetLeftBetweenScrollContainers =
|
|
9132
|
+
referenceScrollContainerRect.left - scrollContainerRect.left;
|
|
9133
|
+
let offsetTopBetweenScrollContainers =
|
|
9134
|
+
referenceScrollContainerRect.top - scrollContainerRect.top;
|
|
9135
|
+
if (scrollContainerIsDocument) {
|
|
9136
|
+
offsetLeftBetweenScrollContainers -= scrollContainer.scrollLeft;
|
|
9137
|
+
offsetTopBetweenScrollContainers -= scrollContainer.scrollTop;
|
|
9138
|
+
}
|
|
9139
|
+
return [
|
|
9140
|
+
-offsetLeftBetweenScrollContainers,
|
|
9141
|
+
-offsetTopBetweenScrollContainers,
|
|
9142
|
+
];
|
|
8840
9143
|
};
|
|
8841
|
-
|
|
8842
|
-
|
|
8843
|
-
|
|
8844
|
-
|
|
8845
|
-
|
|
8846
|
-
|
|
8847
|
-
|
|
8848
|
-
|
|
8849
|
-
|
|
8850
|
-
|
|
9144
|
+
return [getPositionOffsetsFixedOverlay, getScrollOffsets];
|
|
9145
|
+
}
|
|
9146
|
+
|
|
9147
|
+
const getPositionOffsetsOverlay = () => {
|
|
9148
|
+
if (sameScrollContainer) {
|
|
9149
|
+
const overlayRect = overlay.getBoundingClientRect();
|
|
9150
|
+
const overlayTargetRect = overlayTarget.getBoundingClientRect();
|
|
9151
|
+
const overlayLeft = overlayRect.left;
|
|
9152
|
+
const overlayTop = overlayRect.top;
|
|
9153
|
+
let overlayTargetLeft = overlayTargetRect.left;
|
|
9154
|
+
let overlayTargetTop = overlayTargetRect.top;
|
|
9155
|
+
if (scrollContainerIsDocument) {
|
|
9156
|
+
overlayTargetLeft += scrollContainer.scrollLeft;
|
|
9157
|
+
overlayTargetTop += scrollContainer.scrollTop;
|
|
9158
|
+
}
|
|
9159
|
+
const offsetLeftBetweenTargetAndOverlay = overlayTargetLeft - overlayLeft;
|
|
9160
|
+
const offsetTopBetweenTargetAndOverlay = overlayTargetTop - overlayTop;
|
|
9161
|
+
return [
|
|
9162
|
+
-scrollContainer.scrollLeft + offsetLeftBetweenTargetAndOverlay,
|
|
9163
|
+
-scrollContainer.scrollTop + offsetTopBetweenTargetAndOverlay,
|
|
9164
|
+
];
|
|
9165
|
+
}
|
|
9166
|
+
|
|
9167
|
+
const scrollContainerRect = scrollContainer.getBoundingClientRect();
|
|
9168
|
+
const referenceScrollContainerRect =
|
|
9169
|
+
referenceScrollContainer.getBoundingClientRect();
|
|
9170
|
+
let scrollContainerLeft = scrollContainerRect.left;
|
|
9171
|
+
let scrollContainerTop = scrollContainerRect.top;
|
|
9172
|
+
let referenceScrollContainerLeft = referenceScrollContainerRect.left;
|
|
9173
|
+
let referenceScrollContainerTop = referenceScrollContainerRect.top;
|
|
9174
|
+
if (scrollContainerIsDocument) {
|
|
9175
|
+
scrollContainerLeft += scrollContainer.scrollLeft;
|
|
9176
|
+
scrollContainerTop += scrollContainer.scrollTop;
|
|
9177
|
+
}
|
|
9178
|
+
const offsetLeftBetweenScrollContainers =
|
|
9179
|
+
referenceScrollContainerLeft - scrollContainerLeft;
|
|
9180
|
+
const offsetTopBetweenScrollContainers =
|
|
9181
|
+
referenceScrollContainerTop - scrollContainerTop;
|
|
9182
|
+
return [
|
|
9183
|
+
-offsetLeftBetweenScrollContainers - referenceScrollContainer.scrollLeft,
|
|
9184
|
+
-offsetTopBetweenScrollContainers - referenceScrollContainer.scrollTop,
|
|
9185
|
+
];
|
|
8851
9186
|
};
|
|
8852
|
-
const
|
|
8853
|
-
if (
|
|
8854
|
-
return
|
|
8855
|
-
onMove,
|
|
8856
|
-
onRelease
|
|
8857
|
-
}) => {
|
|
8858
|
-
const target = grabEvent.target;
|
|
8859
|
-
target.setPointerCapture(grabEvent.pointerId);
|
|
8860
|
-
target.addEventListener("lostpointercapture", onRelease);
|
|
8861
|
-
target.addEventListener("pointercancel", onRelease);
|
|
8862
|
-
target.addEventListener("pointermove", onMove);
|
|
8863
|
-
target.addEventListener("pointerup", onRelease);
|
|
8864
|
-
return () => {
|
|
8865
|
-
target.releasePointerCapture(grabEvent.pointerId);
|
|
8866
|
-
target.removeEventListener("lostpointercapture", onRelease);
|
|
8867
|
-
target.removeEventListener("pointercancel", onRelease);
|
|
8868
|
-
target.removeEventListener("pointermove", onMove);
|
|
8869
|
-
target.removeEventListener("pointerup", onRelease);
|
|
8870
|
-
};
|
|
8871
|
-
});
|
|
9187
|
+
const getScrollOffsetsOverlay = () => {
|
|
9188
|
+
if (sameScrollContainer) {
|
|
9189
|
+
return [scrollContainer.scrollLeft, scrollContainer.scrollTop];
|
|
8872
9190
|
}
|
|
8873
|
-
|
|
8874
|
-
|
|
8875
|
-
|
|
8876
|
-
|
|
8877
|
-
|
|
8878
|
-
|
|
8879
|
-
|
|
8880
|
-
|
|
8881
|
-
|
|
8882
|
-
|
|
8883
|
-
|
|
8884
|
-
|
|
8885
|
-
|
|
8886
|
-
|
|
8887
|
-
|
|
8888
|
-
|
|
8889
|
-
|
|
8890
|
-
|
|
8891
|
-
|
|
8892
|
-
|
|
9191
|
+
|
|
9192
|
+
const scrollContainerRect = scrollContainer.getBoundingClientRect();
|
|
9193
|
+
const referenceScrollContainerRect =
|
|
9194
|
+
referenceScrollContainer.getBoundingClientRect();
|
|
9195
|
+
let offsetLeftBetweenScrollContainers =
|
|
9196
|
+
referenceScrollContainerRect.left - scrollContainerRect.left;
|
|
9197
|
+
let offsetTopBetweenScrollContainers =
|
|
9198
|
+
referenceScrollContainerRect.top - scrollContainerRect.top;
|
|
9199
|
+
if (scrollContainerIsDocument) {
|
|
9200
|
+
offsetLeftBetweenScrollContainers -= scrollContainer.scrollLeft;
|
|
9201
|
+
offsetTopBetweenScrollContainers -= scrollContainer.scrollTop;
|
|
9202
|
+
}
|
|
9203
|
+
|
|
9204
|
+
return [
|
|
9205
|
+
referenceScrollContainer.scrollLeft + offsetLeftBetweenScrollContainers,
|
|
9206
|
+
referenceScrollContainer.scrollTop + offsetTopBetweenScrollContainers,
|
|
9207
|
+
];
|
|
9208
|
+
};
|
|
9209
|
+
return [getPositionOffsetsOverlay, getScrollOffsetsOverlay];
|
|
9210
|
+
};
|
|
9211
|
+
const isOverlayOf = (element, potentialTarget) => {
|
|
9212
|
+
const overlayForAttribute = element.getAttribute("data-overlay-for");
|
|
9213
|
+
if (!overlayForAttribute) {
|
|
9214
|
+
return false;
|
|
9215
|
+
}
|
|
9216
|
+
const overlayTarget = document.querySelector(`#${overlayForAttribute}`);
|
|
9217
|
+
if (!overlayTarget) {
|
|
9218
|
+
return false;
|
|
9219
|
+
}
|
|
9220
|
+
if (overlayTarget === potentialTarget) {
|
|
9221
|
+
return true;
|
|
9222
|
+
}
|
|
9223
|
+
const overlayTargetPositionedParent = getPositionedParent(overlayTarget);
|
|
9224
|
+
if (overlayTargetPositionedParent === potentialTarget) {
|
|
9225
|
+
return true;
|
|
9226
|
+
}
|
|
9227
|
+
return false;
|
|
9228
|
+
};
|
|
9229
|
+
|
|
9230
|
+
const { documentElement } =
|
|
9231
|
+
typeof document === "object" ? document : { documentElement: null };
|
|
9232
|
+
|
|
9233
|
+
const createGetScrollOffsets = (
|
|
9234
|
+
scrollContainer,
|
|
9235
|
+
referenceScrollContainer,
|
|
9236
|
+
positionedParent,
|
|
9237
|
+
samePositionedParent,
|
|
9238
|
+
) => {
|
|
9239
|
+
const getGetScrollOffsetsSameContainer = () => {
|
|
9240
|
+
const scrollContainerIsDocument = scrollContainer === documentElement;
|
|
9241
|
+
// I don't really get why we have to add scrollLeft (scrollLeft at grab)
|
|
9242
|
+
// to properly position the element in this scenario
|
|
9243
|
+
// It happens since we use translateX to position the element
|
|
9244
|
+
// Or maybe since something else. In any case it works
|
|
9245
|
+
const { scrollLeft, scrollTop } = samePositionedParent
|
|
9246
|
+
? { scrollLeft: 0, scrollTop: 0 }
|
|
9247
|
+
: referenceScrollContainer;
|
|
9248
|
+
if (scrollContainerIsDocument) {
|
|
9249
|
+
const fixedPosition = findSelfOrAncestorFixedPosition(positionedParent);
|
|
9250
|
+
if (fixedPosition) {
|
|
9251
|
+
const getScrollOffsetsFixed = () => {
|
|
9252
|
+
const leftScrollToAdd = scrollLeft + fixedPosition[0];
|
|
9253
|
+
const topScrollToAdd = scrollTop + fixedPosition[1];
|
|
9254
|
+
return [leftScrollToAdd, topScrollToAdd];
|
|
8893
9255
|
};
|
|
8894
|
-
|
|
9256
|
+
return getScrollOffsetsFixed;
|
|
9257
|
+
}
|
|
8895
9258
|
}
|
|
8896
|
-
|
|
9259
|
+
const positionedParentIsInsideScrollContainer =
|
|
9260
|
+
referenceScrollContainer === documentElement ||
|
|
9261
|
+
referenceScrollContainer.contains(positionedParent);
|
|
9262
|
+
if (!positionedParentIsInsideScrollContainer) {
|
|
9263
|
+
// positionedParent is outside the scroll container (e.g. clone in document.body
|
|
9264
|
+
// while tracking an element inside a custom scroll container).
|
|
9265
|
+
// We must add the scroll at grab time as a frozen offset so that:
|
|
9266
|
+
// - initial topDelta = 0 (clone starts at correct position)
|
|
9267
|
+
// - auto-scroll doesn't double-move the clone (scroll changes cancel out in layout)
|
|
9268
|
+
const scrollLeftAtGrab = referenceScrollContainer.scrollLeft;
|
|
9269
|
+
const scrollTopAtGrab = referenceScrollContainer.scrollTop;
|
|
9270
|
+
return () => [scrollLeft + scrollLeftAtGrab, scrollTop + scrollTopAtGrab];
|
|
9271
|
+
}
|
|
9272
|
+
const getScrollOffsets = () => {
|
|
9273
|
+
const leftScrollToAdd = scrollLeft + referenceScrollContainer.scrollLeft;
|
|
9274
|
+
const topScrollToAdd = scrollTop + referenceScrollContainer.scrollTop;
|
|
9275
|
+
return [leftScrollToAdd, topScrollToAdd];
|
|
9276
|
+
};
|
|
9277
|
+
return getScrollOffsets;
|
|
8897
9278
|
};
|
|
8898
|
-
dragGestureController.grabViaPointer = grabViaPointer;
|
|
8899
|
-
return dragGestureController;
|
|
8900
|
-
};
|
|
8901
9279
|
|
|
8902
|
-
|
|
8903
|
-
|
|
8904
|
-
|
|
8905
|
-
|
|
8906
|
-
if (!isPrimaryButtonEvent(grabEvent)) {
|
|
8907
|
-
return;
|
|
8908
|
-
}
|
|
8909
|
-
const target = grabEvent.target;
|
|
8910
|
-
const isDedicatedHandle = target.closest && target.closest("[data-drag-handle]");
|
|
8911
|
-
if (isDedicatedHandle) {
|
|
8912
|
-
// Element is dedicated to drag — skip the threshold and start immediately.
|
|
8913
|
-
const dragGesture = dragGestureInitializer();
|
|
8914
|
-
if (!dragGesture) {
|
|
8915
|
-
return;
|
|
8916
|
-
}
|
|
8917
|
-
dragGesture.dragViaPointer(grabEvent);
|
|
8918
|
-
return;
|
|
9280
|
+
const sameScrollContainer = scrollContainer === referenceScrollContainer;
|
|
9281
|
+
const getScrollOffsetsSameContainer = getGetScrollOffsetsSameContainer();
|
|
9282
|
+
if (sameScrollContainer) {
|
|
9283
|
+
return getScrollOffsetsSameContainer;
|
|
8919
9284
|
}
|
|
8920
|
-
const
|
|
8921
|
-
|
|
8922
|
-
|
|
8923
|
-
|
|
8924
|
-
|
|
8925
|
-
|
|
8926
|
-
|
|
8927
|
-
|
|
8928
|
-
|
|
8929
|
-
dragGesture.dragViaPointer(gestureInfo.dragEvent);
|
|
8930
|
-
}
|
|
8931
|
-
});
|
|
8932
|
-
const significantDragGesture = significantDragGestureController.grabViaPointer(grabEvent, {
|
|
8933
|
-
element: grabEvent.target
|
|
8934
|
-
});
|
|
9285
|
+
const getScrollOffsetsDifferentContainers = () => {
|
|
9286
|
+
const [scrollLeftToAdd, scrollTopToAdd] = getScrollOffsetsSameContainer();
|
|
9287
|
+
const rect = scrollContainer.getBoundingClientRect();
|
|
9288
|
+
const referenceRect = referenceScrollContainer.getBoundingClientRect();
|
|
9289
|
+
const leftDiff = referenceRect.left - rect.left;
|
|
9290
|
+
const topDiff = referenceRect.top - rect.top;
|
|
9291
|
+
return [scrollLeftToAdd + leftDiff, scrollTopToAdd + topDiff];
|
|
9292
|
+
};
|
|
9293
|
+
return getScrollOffsetsDifferentContainers;
|
|
8935
9294
|
};
|
|
8936
|
-
const
|
|
8937
|
-
|
|
8938
|
-
|
|
8939
|
-
|
|
8940
|
-
|
|
9295
|
+
const getDragCoordinates = (
|
|
9296
|
+
element,
|
|
9297
|
+
scrollContainer = getScrollContainer(element),
|
|
9298
|
+
) => {
|
|
9299
|
+
const [scrollableLeft, scrollableTop] = getScrollablePosition(
|
|
9300
|
+
element,
|
|
9301
|
+
scrollContainer,
|
|
9302
|
+
);
|
|
9303
|
+
const { scrollLeft, scrollTop } = scrollContainer;
|
|
9304
|
+
const leftRelativeToScrollContainer = scrollableLeft + scrollLeft;
|
|
9305
|
+
const topRelativeToScrollContainer = scrollableTop + scrollTop;
|
|
9306
|
+
return [leftRelativeToScrollContainer, topRelativeToScrollContainer];
|
|
8941
9307
|
};
|
|
8942
9308
|
|
|
8943
9309
|
installImportMetaCssBuild(import.meta);const css$3 = /* css */`
|
|
@@ -10258,6 +10624,11 @@ const dragStyleController = createStyleController("drag_to_move");
|
|
|
10258
10624
|
* If omitted, `element` is translated. The translate is read from `dragStyleController`
|
|
10259
10625
|
* at grab time so any pre-existing translate is accumulated rather than reset.
|
|
10260
10626
|
*
|
|
10627
|
+
* A `transform` already on the moved element (rotate, scale…) is preserved and does
|
|
10628
|
+
* not disturb the movement. `rotate` and `scale` set as individual CSS properties do:
|
|
10629
|
+
* they apply outside `transform`, where nothing the gesture writes can reach them —
|
|
10630
|
+
* put those on a child element instead (a warning says so in dev).
|
|
10631
|
+
*
|
|
10261
10632
|
* @param {object} [options]
|
|
10262
10633
|
* @param {boolean} [options.stickyFrontiers=true]
|
|
10263
10634
|
* Shrinks the auto-scroll area at sticky boundaries (elements with `data-sticky-left` /
|
|
@@ -10279,13 +10650,24 @@ const dragStyleController = createStyleController("drag_to_move");
|
|
|
10279
10650
|
* Renders a visual line when the pointer deviates from the element due to constraints.
|
|
10280
10651
|
* @param {boolean} [options.showDebugMarkers=false]
|
|
10281
10652
|
* Renders debug markers for constraint regions.
|
|
10282
|
-
* @param {"commit"|"cancel"|"manual"} [options.releasePositionEffect="commit"]
|
|
10653
|
+
* @param {"commit"|"cancel"|"cancel-animated"|"manual"} [options.releasePositionEffect="commit"]
|
|
10283
10654
|
* Controls what happens to the translated position on release.
|
|
10284
10655
|
* - `"commit"`: bakes the translate into inline styles so the element stays put (default).
|
|
10285
10656
|
* - `"cancel"`: discards the translate so the element snaps back to its original position.
|
|
10657
|
+
* - `"cancel-animated"`: same, travelling back to it over `cancelAnimationDuration`.
|
|
10286
10658
|
* - `"manual"`: does nothing — the caller is responsible for clearing or committing
|
|
10287
10659
|
* the transform via `dragStyleController`.
|
|
10660
|
+
* @param {number} [options.cancelAnimationDuration=200]
|
|
10661
|
+
* Duration (ms) of the way back for `"cancel-animated"`.
|
|
10662
|
+
* @param {string} [options.cancelAnimationEasing="ease-out"]
|
|
10663
|
+
* Easing of the way back for `"cancel-animated"`.
|
|
10288
10664
|
* @returns {object} Drag gesture controller with augmented `grab()` / `grabViaPointer()` methods.
|
|
10665
|
+
*
|
|
10666
|
+
* `gestureInfo` gains `cancelPosition()`, `commitPosition()` and
|
|
10667
|
+
* `cancelPositionAnimated({duration, easing})` — the last returns the `Animation`
|
|
10668
|
+
* playing the way back (`null` when the element was already home), so a caller
|
|
10669
|
+
* on `"manual"` can decide between thrown and put back, and still await the
|
|
10670
|
+
* landing.
|
|
10289
10671
|
*/
|
|
10290
10672
|
const createDragToMoveGestureController = ({
|
|
10291
10673
|
stickyFrontiers = true,
|
|
@@ -10296,6 +10678,8 @@ const createDragToMoveGestureController = ({
|
|
|
10296
10678
|
showConstraintFeedbackLine = false,
|
|
10297
10679
|
showDebugMarkers = false,
|
|
10298
10680
|
releasePositionEffect = "commit",
|
|
10681
|
+
cancelAnimationDuration = 200,
|
|
10682
|
+
cancelAnimationEasing = "ease-out",
|
|
10299
10683
|
...options
|
|
10300
10684
|
} = {}) => {
|
|
10301
10685
|
const initGrabToMoveElement = (
|
|
@@ -10321,15 +10705,39 @@ const createDragToMoveGestureController = ({
|
|
|
10321
10705
|
const cancelPosition = () => {
|
|
10322
10706
|
dragStyleController.clear(elementImpacted);
|
|
10323
10707
|
};
|
|
10708
|
+
// Reading the transform on either side of the clear is what lets this work
|
|
10709
|
+
// without knowing anything about the element: how it looked while held and
|
|
10710
|
+
// how it looks once let go are both just computed transforms, and the
|
|
10711
|
+
// animation has only to bridge the two.
|
|
10712
|
+
const cancelPositionAnimated = ({
|
|
10713
|
+
duration = cancelAnimationDuration,
|
|
10714
|
+
easing = cancelAnimationEasing,
|
|
10715
|
+
} = {}) => {
|
|
10716
|
+
const transformWhileHeld = getComputedStyle(elementImpacted).transform;
|
|
10717
|
+
cancelPosition();
|
|
10718
|
+
const transformAtRest = getComputedStyle(elementImpacted).transform;
|
|
10719
|
+
if (transformWhileHeld === transformAtRest) {
|
|
10720
|
+
return null;
|
|
10721
|
+
}
|
|
10722
|
+
// No fill: the element already sits at its resting transform, the
|
|
10723
|
+
// animation only replays the way back to it.
|
|
10724
|
+
return elementImpacted.animate(
|
|
10725
|
+
[{ transform: transformWhileHeld }, { transform: transformAtRest }],
|
|
10726
|
+
{ duration, easing },
|
|
10727
|
+
);
|
|
10728
|
+
};
|
|
10324
10729
|
const commitPosition = () => {
|
|
10325
10730
|
dragStyleController.commit(elementImpacted);
|
|
10326
10731
|
};
|
|
10327
10732
|
dragGesture.gestureInfo.cancelPosition = cancelPosition;
|
|
10733
|
+
dragGesture.gestureInfo.cancelPositionAnimated = cancelPositionAnimated;
|
|
10328
10734
|
dragGesture.gestureInfo.commitPosition = commitPosition;
|
|
10329
10735
|
|
|
10330
10736
|
dragGesture.addReleaseCallback(() => {
|
|
10331
10737
|
if (releasePositionEffect === "cancel") {
|
|
10332
10738
|
cancelPosition();
|
|
10739
|
+
} else if (releasePositionEffect === "cancel-animated") {
|
|
10740
|
+
cancelPositionAnimated();
|
|
10333
10741
|
} else if (releasePositionEffect === "commit") {
|
|
10334
10742
|
commitPosition();
|
|
10335
10743
|
}
|
|
@@ -10522,7 +10930,15 @@ const createDragToMoveGestureController = ({
|
|
|
10522
10930
|
// Build the transform to apply, preserving any transforms that were
|
|
10523
10931
|
// already on the element before the grab (e.g. rotate from another
|
|
10524
10932
|
// controller), and accumulating from the pre-grab translate baseline.
|
|
10525
|
-
|
|
10933
|
+
// The translate keys are seeded HERE, before the spread, and not merely
|
|
10934
|
+
// assigned below: a transform object is serialized in key order, and in a
|
|
10935
|
+
// transform list every function transforms the frame of the ones after it.
|
|
10936
|
+
// A translate written after a rotate or a scale therefore travels rotated
|
|
10937
|
+
// and scaled — the element drifts away from the pointer, proportionally to
|
|
10938
|
+
// the distance covered. Dragging moves things on screen, so its translate
|
|
10939
|
+
// has to come first, whatever else the element carries. The spread still
|
|
10940
|
+
// wins on the value when the element already had a translate of its own.
|
|
10941
|
+
const transform = { translateX: 0, translateY: 0, ...transformAtGrab };
|
|
10526
10942
|
if (direction.x) {
|
|
10527
10943
|
const leftTarget = positionedLeft;
|
|
10528
10944
|
const leftAtGrab = dragGesture.gestureInfo.leftAtGrab;
|
|
@@ -10910,9 +11326,9 @@ installImportMetaCssBuild(import.meta);const css$1 = /* css */`
|
|
|
10910
11326
|
|
|
10911
11327
|
/* WHO CAN START A DRAG, said in the cursor.
|
|
10912
11328
|
A handle drags on the spot, so it shows the hand. A source only drags once
|
|
10913
|
-
the
|
|
10914
|
-
the text inside it
|
|
10915
|
-
pointer), so an I-beam over it would promise something that does not
|
|
11329
|
+
the intent shows (a few pixels of travel, or a long press) — a plain click
|
|
11330
|
+
stays a click — but the text inside it cannot be selected (the gesture takes
|
|
11331
|
+
the pointer), so an I-beam over it would promise something that does not
|
|
10916
11332
|
happen: it reads as a plain surface instead. An opted-out area keeps both
|
|
10917
11333
|
its cursor and its selection, and never starts a drag (see the check in
|
|
10918
11334
|
startDragToReorder).
|
|
@@ -10986,7 +11402,9 @@ const dragCSSVars = ["--drop-hint-size", "--drop-hint-background-color", "--drop
|
|
|
10986
11402
|
* Starts a drag-to-reorder interaction on a list item.
|
|
10987
11403
|
*
|
|
10988
11404
|
* Handles the full reorder UX:
|
|
10989
|
-
* - Activates only
|
|
11405
|
+
* - Activates only once the intent is established — a short movement with a mouse, a long
|
|
11406
|
+
* press with a finger (see `dragAfterIntent`), so that neither a click nor a scroll
|
|
11407
|
+
* reorders anything by accident.
|
|
10990
11408
|
* - Clones the grabbed element and moves the clone while the original stays hidden in place
|
|
10991
11409
|
* (keeps the layout intact so other items don't shift during the drag).
|
|
10992
11410
|
* - CSS vars (`--drop-hint-size`, `--drop-hint-background-color`, etc.) are read from the
|
|
@@ -11009,30 +11427,48 @@ const dragCSSVars = ["--drop-hint-size", "--drop-hint-background-color", "--drop
|
|
|
11009
11427
|
* - Virtual lists render fewer DOM nodes than the total item count, so
|
|
11010
11428
|
* DOM-index-based counting would be wrong.
|
|
11011
11429
|
*
|
|
11430
|
+
* Any option not listed below is forwarded to `createDragToMoveGestureController`
|
|
11431
|
+
* (`areaConstraint`, `autoScrollAreaPadding`, `stickyFrontiers`…), except
|
|
11432
|
+
* `releasePositionEffect`, always `"manual"` here: what moves is the clone, and it
|
|
11433
|
+
* is removed on release, so there is no position to commit or cancel.
|
|
11434
|
+
*
|
|
11012
11435
|
* @param {PointerEvent} event
|
|
11013
|
-
* The `pointerdown` event
|
|
11014
|
-
* @param {Element} draggedElement
|
|
11015
|
-
* The list item element to drag. Typically `event.currentTarget`.
|
|
11436
|
+
* The `pointerdown` event that may become a reorder.
|
|
11016
11437
|
* @param {object} options
|
|
11438
|
+
* @param {Element} [options.draggedElement=event.currentTarget]
|
|
11439
|
+
* The list item to drag.
|
|
11440
|
+
* @param {Element} [options.containerElement=draggedElement.parentElement]
|
|
11441
|
+
* Element searched with `itemSelector` to find the items to drop between.
|
|
11017
11442
|
* @param {string} options.itemSelector
|
|
11018
|
-
* CSS selector that matches all list items inside
|
|
11443
|
+
* CSS selector that matches all list items inside `containerElement`.
|
|
11019
11444
|
* Used for drop-target detection and no-op filtering.
|
|
11020
11445
|
* @param {function} options.getItemId
|
|
11021
11446
|
* Returns the stable ID for a given DOM element.
|
|
11022
11447
|
* Signature: `getItemId(element) → id`.
|
|
11023
11448
|
* @param {function} options.onReorder
|
|
11024
11449
|
* Called when the user drops the item in a new position.
|
|
11025
|
-
* Signature: `onReorder(fromId, toId)`.
|
|
11450
|
+
* Signature: `onReorder(fromId, toId, syncCloneWithDropTarget)`.
|
|
11026
11451
|
* - `fromId`: stable ID of the dragged item.
|
|
11027
11452
|
* - `toId`: stable ID of the item to insert before, or `null` to append at the end.
|
|
11028
|
-
*
|
|
11029
|
-
*
|
|
11453
|
+
* - `syncCloneWithDropTarget`: call it synchronously inside a
|
|
11454
|
+
* `document.startViewTransition` callback, next to the DOM mutation, so the
|
|
11455
|
+
* clone is captured at its landing position.
|
|
11030
11456
|
* @param {object} [options.direction={ x: false, y: true }]
|
|
11031
11457
|
* Axes along which dragging is allowed. Passed to `createDragToMoveGestureController`.
|
|
11032
|
-
* @param {
|
|
11033
|
-
*
|
|
11034
|
-
*
|
|
11035
|
-
*
|
|
11458
|
+
* @param {number} [options.threshold=5]
|
|
11459
|
+
* Distance (px) a mouse must travel before the press becomes a drag.
|
|
11460
|
+
* @param {boolean|"if-touch"} [options.longPress="if-touch"]
|
|
11461
|
+
* Which pointers start the drag by holding still instead of by travelling.
|
|
11462
|
+
* @param {number} [options.longPressDelay=400]
|
|
11463
|
+
* How long (ms) such a pointer must stay down.
|
|
11464
|
+
* @param {number} [options.longPressSlop=8]
|
|
11465
|
+
* How far (px) it may drift during that wait before the press is abandoned.
|
|
11466
|
+
* @param {function} [options.onPressStart]
|
|
11467
|
+
* The pointer went down and the wait began (a cue that the press counts).
|
|
11468
|
+
* @param {function} [options.onPressCancel]
|
|
11469
|
+
* The pointer moved or lifted before the wait was over.
|
|
11470
|
+
* @param {function} [options.onPress]
|
|
11471
|
+
* The wait completed and the item is now held (haptics, scale…).
|
|
11036
11472
|
*/
|
|
11037
11473
|
const startDragToReorder = (event, {
|
|
11038
11474
|
draggedElement = event.currentTarget,
|
|
@@ -11044,6 +11480,13 @@ const startDragToReorder = (event, {
|
|
|
11044
11480
|
x: false,
|
|
11045
11481
|
y: true
|
|
11046
11482
|
},
|
|
11483
|
+
threshold,
|
|
11484
|
+
longPress,
|
|
11485
|
+
longPressDelay,
|
|
11486
|
+
longPressSlop,
|
|
11487
|
+
onPressStart,
|
|
11488
|
+
onPressCancel,
|
|
11489
|
+
onPress,
|
|
11047
11490
|
...options
|
|
11048
11491
|
}) => {
|
|
11049
11492
|
// An area that opted out of dragging (a text one wants to select, a control
|
|
@@ -11056,7 +11499,7 @@ const startDragToReorder = (event, {
|
|
|
11056
11499
|
return undefined;
|
|
11057
11500
|
}
|
|
11058
11501
|
event.preventDefault();
|
|
11059
|
-
return
|
|
11502
|
+
return dragAfterIntent(event, () => {
|
|
11060
11503
|
const cloneWrapper = createDragClone(draggedElement, event);
|
|
11061
11504
|
draggedElement.setAttribute("navi-drag-clone-source", "");
|
|
11062
11505
|
// Move drag related CSS vars from the element to the document
|
|
@@ -11179,6 +11622,14 @@ const startDragToReorder = (event, {
|
|
|
11179
11622
|
cloneWrapper.remove();
|
|
11180
11623
|
});
|
|
11181
11624
|
return dragGesture;
|
|
11625
|
+
}, {
|
|
11626
|
+
threshold,
|
|
11627
|
+
longPress,
|
|
11628
|
+
longPressDelay,
|
|
11629
|
+
longPressSlop,
|
|
11630
|
+
onPressStart,
|
|
11631
|
+
onPressCancel,
|
|
11632
|
+
onPress
|
|
11182
11633
|
});
|
|
11183
11634
|
};
|
|
11184
11635
|
|
|
@@ -11332,6 +11783,761 @@ const getResizeDirection = (element) => {
|
|
|
11332
11783
|
return { x, y };
|
|
11333
11784
|
};
|
|
11334
11785
|
|
|
11786
|
+
installImportMetaCssBuild(import.meta);/**
|
|
11787
|
+
* What a drag means when it TRAVELS: a whole screen pushed aside to bring in the
|
|
11788
|
+
* next one — slides inside one box, pages that are URLs.
|
|
11789
|
+
*
|
|
11790
|
+
* The pointer itself is not read here: reading a press, waiting for it to become
|
|
11791
|
+
* a gesture, capturing it, measuring how fast it goes, swallowing the click it
|
|
11792
|
+
* would have made is one gesture system for the whole codebase
|
|
11793
|
+
* (@jsenv/dom's drag_gesture + drag_after_intent), and this asks it for the
|
|
11794
|
+
* plain version — nothing carried, so no backdrop over the page, nothing made
|
|
11795
|
+
* inert, no focus taken: a screen slides and the page keeps its scrolling and
|
|
11796
|
+
* its keyboard.
|
|
11797
|
+
*
|
|
11798
|
+
* What IS here is everything that makes a travel a travel rather than a
|
|
11799
|
+
* carry — and it is policy, not plumbing:
|
|
11800
|
+
* - the axis is LOCKED by the first movement, instead of being constrained
|
|
11801
|
+
* ahead of time;
|
|
11802
|
+
* - a press becomes a gesture by distance for every pointer, finger included:
|
|
11803
|
+
* a swipe is a travel, and asking a finger to hold still first (the rule for
|
|
11804
|
+
* picking an object up) would mean waiting before being allowed to swipe;
|
|
11805
|
+
* - what travels walks ONE BOX, resists past its ends, and is measured from
|
|
11806
|
+
* where the finger is once it gets there — unless the caller has another box
|
|
11807
|
+
* to offer at that edge, and then the gesture walks on into it;
|
|
11808
|
+
* - letting go is a question with an answer: a third of a box, or a flick.
|
|
11809
|
+
*
|
|
11810
|
+
* What is NOT here is geometry: how big a box is, what lies one step that way,
|
|
11811
|
+
* what to paint while the finger moves. The caller knows those and nothing else
|
|
11812
|
+
* does — this reads the gesture and calls back.
|
|
11813
|
+
*
|
|
11814
|
+
* Who owns a gesture is decided in two places, and both are read here:
|
|
11815
|
+
* - what says so itself, with [data-no-drag-travel] or by being a field — a
|
|
11816
|
+
* component that reads the pointer marks itself, because the container it
|
|
11817
|
+
* ends up in cannot know what it is;
|
|
11818
|
+
* - a scroller between the pointer and the box with room left that way, which
|
|
11819
|
+
* keeps the gesture until it has none.
|
|
11820
|
+
*/
|
|
11821
|
+
|
|
11822
|
+
// While a pointer is on something that travels: said on the document, because
|
|
11823
|
+
// what has to be told is the document.
|
|
11824
|
+
const GESTURE_ATTRIBUTE = "data-drag-travel-gesture";
|
|
11825
|
+
|
|
11826
|
+
// …and while one is actually travelling something, which is a later moment and
|
|
11827
|
+
// takes more away (see the CSS).
|
|
11828
|
+
const WALKING_ATTRIBUTE = "data-drag-travel-walking";
|
|
11829
|
+
import.meta.css = [/* css */`
|
|
11830
|
+
:root[${GESTURE_ATTRIBUTE}] {
|
|
11831
|
+
/* The bounce the browser plays when a gesture reaches the end of a page —
|
|
11832
|
+
and the swipe that goes back in history with it. Both are the browser
|
|
11833
|
+
answering a gesture that is already answered, here, by what the finger is
|
|
11834
|
+
dragging: the page rocks under a travel that is doing its own moving, and
|
|
11835
|
+
one gesture is seen twice. From the press, because the browser starts
|
|
11836
|
+
answering from the press — waiting for the first pixel that travels would
|
|
11837
|
+
let it happen once, every time. Only while a finger is down, so a page
|
|
11838
|
+
that bounces the rest of the time goes on bouncing. */
|
|
11839
|
+
overscroll-behavior: none;
|
|
11840
|
+
}
|
|
11841
|
+
/* …and nothing inside a travelling box hands its leftovers to what is above
|
|
11842
|
+
it: a list that reaches its end passes what is left of the gesture up the
|
|
11843
|
+
chain, and the page moves behind a travel that is being dragged.
|
|
11844
|
+
|
|
11845
|
+
Written ONCE AND FOR ALL rather than while a finger is down, unlike
|
|
11846
|
+
everything else here: a browser decides what a gesture may do when the
|
|
11847
|
+
gesture BEGINS — at the touchstart, at the first wheel event — and a
|
|
11848
|
+
property written after that decision arrives too late for the gesture it
|
|
11849
|
+
was meant for. That is what "most of the time it does not move, sometimes
|
|
11850
|
+
it does" is made of.
|
|
11851
|
+
|
|
11852
|
+
On the axis the box travels on, and that one only: the other axis is the
|
|
11853
|
+
content's own scrolling and is left alone. Containing does not stop it from
|
|
11854
|
+
scrolling anyway — it stops it from spilling over.
|
|
11855
|
+
|
|
11856
|
+
!important because this is not a preference: a box that travels cannot let
|
|
11857
|
+
the page travel with it, and the rule has to win over whatever an
|
|
11858
|
+
application says about its own scrollers. */
|
|
11859
|
+
[data-drag-travel*="x"],
|
|
11860
|
+
[data-drag-travel*="x"] * {
|
|
11861
|
+
overscroll-behavior-x: contain !important;
|
|
11862
|
+
}
|
|
11863
|
+
[data-drag-travel*="y"],
|
|
11864
|
+
[data-drag-travel*="y"] * {
|
|
11865
|
+
overscroll-behavior-y: contain !important;
|
|
11866
|
+
}
|
|
11867
|
+
:root[${WALKING_ATTRIBUTE}] {
|
|
11868
|
+
/* A drag over text selects it on the way, and the blue trail says the
|
|
11869
|
+
gesture was understood as something else. Not from the press: a press on
|
|
11870
|
+
text IS how one selects it, and only a press that has become a travel has
|
|
11871
|
+
said it was about something else. */
|
|
11872
|
+
user-select: none;
|
|
11873
|
+
}
|
|
11874
|
+
`, "@jsenv/dom/src/interaction/drag/drag_to_travel.js"];
|
|
11875
|
+
|
|
11876
|
+
// How far a pointer goes before it is a travel rather than a click: below this
|
|
11877
|
+
// a press that wandered a pixel is still a press, and nothing budges.
|
|
11878
|
+
const DRAG_START_THRESHOLD = 10;
|
|
11879
|
+
// How much of a box has to be pulled for letting go to carry on rather than put
|
|
11880
|
+
// things back. Under half, because a gesture that has clearly begun is an
|
|
11881
|
+
// intention: asking for the box to be dragged all the way across turns a travel
|
|
11882
|
+
// into work.
|
|
11883
|
+
const DRAG_COMMIT_RATIO = 0.3;
|
|
11884
|
+
// A flick travels whatever the distance: the hand said "away" quickly, which is
|
|
11885
|
+
// the whole gesture — px/ms of pointer, and a few pixels to tell it from a tap
|
|
11886
|
+
// that shook.
|
|
11887
|
+
const DRAG_FLICK_VELOCITY = 0.4;
|
|
11888
|
+
const DRAG_FLICK_DISTANCE = 8;
|
|
11889
|
+
// Pulling towards nothing: what travels follows at a fraction of the finger, so
|
|
11890
|
+
// the gesture is answered (something moves) while saying there is nothing that
|
|
11891
|
+
// way. Let go and it comes back — a wall one can lean on, never walk through.
|
|
11892
|
+
const DRAG_RESISTANCE = 0.3;
|
|
11893
|
+
|
|
11894
|
+
// What a drag must not start on: something that reads the pointer itself. A
|
|
11895
|
+
// button or a link is not in the list — dragging from one travels, and the
|
|
11896
|
+
// click it would have made is swallowed on the way out.
|
|
11897
|
+
const DRAG_EXCLUDED_SELECTOR = ["input", "textarea", "select", '[contenteditable=""]', '[contenteditable="true"]', "[data-no-drag-travel]"].join(",");
|
|
11898
|
+
|
|
11899
|
+
/**
|
|
11900
|
+
* A scroller between the pointer and the box it is in, with room left the way
|
|
11901
|
+
* the gesture goes: it gets the gesture, and nothing travels — dragging a row
|
|
11902
|
+
* that scrolls sideways scrolls that row, and only a row with nowhere left to
|
|
11903
|
+
* go hands the travel over.
|
|
11904
|
+
*/
|
|
11905
|
+
const scrollRoomTowards = (fromElement, stopElement, axis, sign) => {
|
|
11906
|
+
let element = fromElement;
|
|
11907
|
+
while (element && element !== stopElement && element.nodeType === 1) {
|
|
11908
|
+
const size = axis === "x" ? element.clientWidth : element.clientHeight;
|
|
11909
|
+
const scrollSize = axis === "x" ? element.scrollWidth : element.scrollHeight;
|
|
11910
|
+
if (scrollSize > size + 1) {
|
|
11911
|
+
const {
|
|
11912
|
+
overflowX,
|
|
11913
|
+
overflowY
|
|
11914
|
+
} = getComputedStyle(element);
|
|
11915
|
+
const overflow = axis === "x" ? overflowX : overflowY;
|
|
11916
|
+
if (overflow === "auto" || overflow === "scroll") {
|
|
11917
|
+
const position = axis === "x" ? element.scrollLeft : element.scrollTop;
|
|
11918
|
+
// Dragging the content one way reveals what is on the other side of
|
|
11919
|
+
// it: to the right means going back up the scroll.
|
|
11920
|
+
const room = sign > 0 ? position : scrollSize - size - position;
|
|
11921
|
+
if (room > 1) {
|
|
11922
|
+
return true;
|
|
11923
|
+
}
|
|
11924
|
+
}
|
|
11925
|
+
}
|
|
11926
|
+
element = element.parentElement;
|
|
11927
|
+
}
|
|
11928
|
+
return false;
|
|
11929
|
+
};
|
|
11930
|
+
|
|
11931
|
+
// A gesture is over: does it carry on, or does everything go back? The distance
|
|
11932
|
+
// pulled says it, and the speed says it too — a short flick means "away" as
|
|
11933
|
+
// clearly as half a box does.
|
|
11934
|
+
const travelsAfter = ({
|
|
11935
|
+
pulled,
|
|
11936
|
+
slack,
|
|
11937
|
+
size,
|
|
11938
|
+
velocity,
|
|
11939
|
+
towardsSomething
|
|
11940
|
+
}) => {
|
|
11941
|
+
if (!towardsSomething) {
|
|
11942
|
+
return false;
|
|
11943
|
+
}
|
|
11944
|
+
// Caught in flight and let go of again without a word: what was on its way
|
|
11945
|
+
// carries on. Answered on the distance alone, a travel a hand merely touched
|
|
11946
|
+
// is undone BY the touch — it was stopped where it stood, and where it stood
|
|
11947
|
+
// is not far enough to count as an intention. Nobody asked it to stop; it was
|
|
11948
|
+
// asked to wait.
|
|
11949
|
+
if (slack && Math.abs(pulled - slack) < DRAG_START_THRESHOLD) {
|
|
11950
|
+
return true;
|
|
11951
|
+
}
|
|
11952
|
+
const sign = pulled > 0 ? 1 : -1;
|
|
11953
|
+
const goingFast = Math.abs(velocity) > DRAG_FLICK_VELOCITY;
|
|
11954
|
+
// A hand that is still moving says where it is going, and it says it about
|
|
11955
|
+
// BOTH answers. Going away from what it was bringing in is "put it back",
|
|
11956
|
+
// whatever the distance already covered — which is the whole of what one asks
|
|
11957
|
+
// for when catching something in flight and throwing it back the other way.
|
|
11958
|
+
// Without this the picture alone decides, and a screen caught at two thirds
|
|
11959
|
+
// and thrown back still arrives: the gesture was read as the place it was let
|
|
11960
|
+
// go of rather than as a movement.
|
|
11961
|
+
if (goingFast && Math.sign(velocity) !== sign) {
|
|
11962
|
+
return false;
|
|
11963
|
+
}
|
|
11964
|
+
// …and going towards it travels whatever the distance: the hand said "away"
|
|
11965
|
+
// quickly, which is the whole gesture.
|
|
11966
|
+
const flicked = goingFast && Math.abs(pulled) > DRAG_FLICK_DISTANCE;
|
|
11967
|
+
return flicked || Math.abs(pulled) > size * DRAG_COMMIT_RATIO;
|
|
11968
|
+
};
|
|
11969
|
+
|
|
11970
|
+
/**
|
|
11971
|
+
* Read a press, and tell the caller what the hand is doing with it.
|
|
11972
|
+
*
|
|
11973
|
+
* Called on pointerdown; returns a handle to stop the gesture, or null when the
|
|
11974
|
+
* press is not one this can be about (a right click, something that reads the
|
|
11975
|
+
* pointer itself).
|
|
11976
|
+
*
|
|
11977
|
+
* The gesture has no shape until the finger says which way it goes: `onStart`
|
|
11978
|
+
* is what turns a press into a travel, and it is asked at that moment rather
|
|
11979
|
+
* than when the finger landed, because whatever was moving then may have
|
|
11980
|
+
* arrived since.
|
|
11981
|
+
*
|
|
11982
|
+
* @param {PointerEvent} pointerDownEvent
|
|
11983
|
+
* @param {object} options
|
|
11984
|
+
* @param {Element} options.element - the box the gesture is about, and what the
|
|
11985
|
+
* pointer is captured on: it outlives whatever the caller does about the
|
|
11986
|
+
* travel, which the element under the finger may not.
|
|
11987
|
+
* @param {"x"|"y"|"xy"} [options.axes="xy"] - which ways this box can travel. A
|
|
11988
|
+
* finger leaning on any other axis is given up on at once, whole, so whatever
|
|
11989
|
+
* else wants it (a scroller, the page) gets it whole.
|
|
11990
|
+
* @param {false|"x"|"y"} [options.immediate=false] - the axis this press is
|
|
11991
|
+
* already on, for a press that landed on something moving: the gesture is
|
|
11992
|
+
* then read from its first pixel instead of waiting for an intent, and every
|
|
11993
|
+
* pixel since the grab is owed to the hand. The axis comes from the caller
|
|
11994
|
+
* rather than from the movement, because there is nothing to decide — what
|
|
11995
|
+
* was caught is travelling on one already.
|
|
11996
|
+
* @param {(detail: {axis: string, sign: number, target: Element, event: PointerEvent}) => false|{size: number, slack?: number, travelBack?: boolean, travelOn?: boolean}} options.onStart
|
|
11997
|
+
* - the finger has picked its axis. Answer `false` to give the gesture up, or
|
|
11998
|
+
* with the geometry it walks: `size` (one box along that axis), `slack` (how
|
|
11999
|
+
* far the box already sits from its resting place, for a travel grabbed
|
|
12000
|
+
* mid-flight) and whether there is anywhere to go each way — `travelBack`
|
|
12001
|
+
* towards the start of the axis, `travelOn` towards its end. A direction with
|
|
12002
|
+
* nothing there is not refused, it resists.
|
|
12003
|
+
* @param {(detail: {axis: string, pulled: number, size: number, progress: number, event: PointerEvent}) => void} options.onPull
|
|
12004
|
+
* - the finger has moved. `pulled` is in px from the resting place, `progress`
|
|
12005
|
+
* the same as a fraction of the box, signed the same way.
|
|
12006
|
+
* @param {(detail: {axis: string, sign: number, event: PointerEvent}) => false|{size: number, travelBack?: boolean, travelOn?: boolean}} [options.onEdge]
|
|
12007
|
+
* - the hand has reached an end of the box it holds and keeps going: `sign`
|
|
12008
|
+
* says which one — the far edge, a box walked whole, or its start, a box
|
|
12009
|
+
* walked back to where it began. Answer with the geometry of the box that
|
|
12010
|
+
* lies that way to hand the gesture over to it: the pixels past the end
|
|
12011
|
+
* become its first ones, so nothing is spent twice and the hand feels one
|
|
12012
|
+
* continuous movement. Answer `false` (or leave it out) for a wall — the
|
|
12013
|
+
* gesture stays on the box it has and leans on it.
|
|
12014
|
+
* @param {(detail: {axis: string, pulled: number, size: number, sign: number, travels: boolean, cancelled: boolean, event: PointerEvent}) => void} options.onEnd
|
|
12015
|
+
* - the finger is off. `travels` is the gesture's answer: carry on to what was
|
|
12016
|
+
* being pulled in, or put things back.
|
|
12017
|
+
* @param {() => void} [options.onGiveUp] - the press is over without ever
|
|
12018
|
+
* becoming a travel: it stayed still, leaned the wrong way, or `onStart`
|
|
12019
|
+
* refused it. Nothing was painted and nothing has to be put back — this is
|
|
12020
|
+
* only so the caller can forget the gesture it is holding.
|
|
12021
|
+
*/
|
|
12022
|
+
const startDragToTravel = (pointerDownEvent, {
|
|
12023
|
+
element,
|
|
12024
|
+
axes = "xy",
|
|
12025
|
+
immediate = false,
|
|
12026
|
+
onStart,
|
|
12027
|
+
onPull,
|
|
12028
|
+
onEnd,
|
|
12029
|
+
onEdge = () => false,
|
|
12030
|
+
onGiveUp = () => {}
|
|
12031
|
+
}) => {
|
|
12032
|
+
const target = pointerDownEvent.target;
|
|
12033
|
+
if (!target.closest || target.closest(DRAG_EXCLUDED_SELECTOR)) {
|
|
12034
|
+
return null;
|
|
12035
|
+
}
|
|
12036
|
+
|
|
12037
|
+
// The travel in hand: null until the finger has picked an axis and the caller
|
|
12038
|
+
// has accepted it.
|
|
12039
|
+
let travel = null;
|
|
12040
|
+
let dragGesture = null;
|
|
12041
|
+
let over = false;
|
|
12042
|
+
const finish = () => {
|
|
12043
|
+
if (over) {
|
|
12044
|
+
return;
|
|
12045
|
+
}
|
|
12046
|
+
over = true;
|
|
12047
|
+
document.documentElement.removeAttribute(GESTURE_ATTRIBUTE);
|
|
12048
|
+
document.documentElement.removeAttribute(WALKING_ATTRIBUTE);
|
|
12049
|
+
window.removeEventListener("pointerup", onPressOver);
|
|
12050
|
+
window.removeEventListener("pointercancel", onPressOver);
|
|
12051
|
+
};
|
|
12052
|
+
// A press that never became a travel: the intent never resolved, or the axis
|
|
12053
|
+
// it leaned on is not one this box walks. Nothing was painted and nothing has
|
|
12054
|
+
// to be put back — the caller is only told so it can forget the gesture.
|
|
12055
|
+
const onPressOver = pointerEvent => {
|
|
12056
|
+
if (pointerEvent.pointerId !== pointerDownEvent.pointerId || travel) {
|
|
12057
|
+
return;
|
|
12058
|
+
}
|
|
12059
|
+
finish();
|
|
12060
|
+
onGiveUp();
|
|
12061
|
+
};
|
|
12062
|
+
const giveUp = () => {
|
|
12063
|
+
finish();
|
|
12064
|
+
dragGesture?.release();
|
|
12065
|
+
onGiveUp();
|
|
12066
|
+
};
|
|
12067
|
+
|
|
12068
|
+
// Where the picture stands, from what the gesture reports: the distance the
|
|
12069
|
+
// pointer has covered along the axis, less the pixels spent deciding — what
|
|
12070
|
+
// travels starts moving from where the finger is at that moment rather than
|
|
12071
|
+
// jumping the threshold it just crossed.
|
|
12072
|
+
// How far the POINTER has come along an axis. The raw distance, not the
|
|
12073
|
+
// layout the gesture computes for something being carried: nothing is being
|
|
12074
|
+
// carried here, and a scroll happening meanwhile must not read as a finger
|
|
12075
|
+
// that moved.
|
|
12076
|
+
const coveredOn = (axis, gestureInfo) => axis === "x" ? gestureInfo.dragX - gestureInfo.grabX : gestureInfo.dragY - gestureInfo.grabY;
|
|
12077
|
+
const pullOf = gestureInfo => {
|
|
12078
|
+
const covered = coveredOn(travel.axis, gestureInfo);
|
|
12079
|
+
return travel.slack + (covered - travel.origin);
|
|
12080
|
+
};
|
|
12081
|
+
|
|
12082
|
+
// Another box under the same hand, at either end of the one it holds. The
|
|
12083
|
+
// distance already covered on that side becomes the new box's own, measured
|
|
12084
|
+
// from where the finger IS: nothing is spent twice, and the gesture is one
|
|
12085
|
+
// movement rather than a wall the hand had to let go of to cross.
|
|
12086
|
+
// Returns where the new box stands, or null when there is nothing that way.
|
|
12087
|
+
const relayTo = (sign, distance, gestureInfo) => {
|
|
12088
|
+
const next = onEdge({
|
|
12089
|
+
axis: travel.axis,
|
|
12090
|
+
sign,
|
|
12091
|
+
event: gestureInfo.dragEvent
|
|
12092
|
+
});
|
|
12093
|
+
if (!next || !next.size) {
|
|
12094
|
+
return null;
|
|
12095
|
+
}
|
|
12096
|
+
travel.size = next.size;
|
|
12097
|
+
travel.travelBack = Boolean(next.travelBack);
|
|
12098
|
+
travel.travelOn = Boolean(next.travelOn);
|
|
12099
|
+
travel.slack = 0;
|
|
12100
|
+
let pulled = distance;
|
|
12101
|
+
if (pulled > next.size) {
|
|
12102
|
+
pulled = next.size;
|
|
12103
|
+
} else if (pulled < -next.size) {
|
|
12104
|
+
pulled = -next.size;
|
|
12105
|
+
}
|
|
12106
|
+
travel.origin = coveredOn(travel.axis, gestureInfo) - pulled;
|
|
12107
|
+
return pulled;
|
|
12108
|
+
};
|
|
12109
|
+
const controller = createDragGestureController({
|
|
12110
|
+
// The threshold is left at its default and never crossed: what says this
|
|
12111
|
+
// press has become a gesture is the intent module below, which calls
|
|
12112
|
+
// start() itself. Zero here would mean "started from the grab", and a
|
|
12113
|
+
// gesture that starts on its own is never STARTED — the moment that
|
|
12114
|
+
// installs the click it must swallow and the touch it must refuse would
|
|
12115
|
+
// never come.
|
|
12116
|
+
// Nothing is being carried: the page keeps its focus, its scrolling and its
|
|
12117
|
+
// cursor while a screen slides under the finger. That is the whole
|
|
12118
|
+
// difference with a drag that moves an object, and it is one option.
|
|
12119
|
+
documentInteractions: "manual",
|
|
12120
|
+
onDragStart: () => {
|
|
12121
|
+
document.documentElement.setAttribute(GESTURE_ATTRIBUTE, "");
|
|
12122
|
+
},
|
|
12123
|
+
onDrag: gestureInfo => {
|
|
12124
|
+
// Releasing a gesture reports one last move, so giving one up would come
|
|
12125
|
+
// back through here and give it up again, forever.
|
|
12126
|
+
if (over) {
|
|
12127
|
+
return;
|
|
12128
|
+
}
|
|
12129
|
+
if (!travel) {
|
|
12130
|
+
let axis;
|
|
12131
|
+
if (immediate) {
|
|
12132
|
+
// The axis is not up for decision: what this press caught is already
|
|
12133
|
+
// travelling on one, and the caller said which. The first pixel of a
|
|
12134
|
+
// hand landing on something moving is a tremor as often as it is a
|
|
12135
|
+
// direction — read as a lean across the axis, it gives the gesture up
|
|
12136
|
+
// and lets go of what was caught, under a finger that has not asked
|
|
12137
|
+
// for anything yet.
|
|
12138
|
+
axis = immediate;
|
|
12139
|
+
} else {
|
|
12140
|
+
// ONE axis, decided by the first movement reported and never
|
|
12141
|
+
// revisited: a diagonal would ask for two travels at once and only
|
|
12142
|
+
// one thing can arrive.
|
|
12143
|
+
const reachX = Math.abs(coveredOn("x", gestureInfo));
|
|
12144
|
+
const reachY = Math.abs(coveredOn("y", gestureInfo));
|
|
12145
|
+
if (!reachX && !reachY) {
|
|
12146
|
+
return;
|
|
12147
|
+
}
|
|
12148
|
+
axis = reachX >= reachY ? "x" : "y";
|
|
12149
|
+
if (!axes.includes(axis)) {
|
|
12150
|
+
giveUp();
|
|
12151
|
+
return;
|
|
12152
|
+
}
|
|
12153
|
+
}
|
|
12154
|
+
const covered = coveredOn(axis, gestureInfo);
|
|
12155
|
+
if (!covered) {
|
|
12156
|
+
// Nothing said on that axis yet: a grab without a movement, or one
|
|
12157
|
+
// straight across it. There is no gesture in that and nothing to give
|
|
12158
|
+
// up on either — whatever the caller caught at the press stays
|
|
12159
|
+
// caught, and the next report will say.
|
|
12160
|
+
if (immediate) {
|
|
12161
|
+
return;
|
|
12162
|
+
}
|
|
12163
|
+
giveUp();
|
|
12164
|
+
return;
|
|
12165
|
+
}
|
|
12166
|
+
const sign = Math.sign(covered);
|
|
12167
|
+
const started = onStart({
|
|
12168
|
+
axis,
|
|
12169
|
+
sign,
|
|
12170
|
+
target,
|
|
12171
|
+
event: gestureInfo.dragEvent
|
|
12172
|
+
});
|
|
12173
|
+
if (!started || !started.size) {
|
|
12174
|
+
giveUp();
|
|
12175
|
+
return;
|
|
12176
|
+
}
|
|
12177
|
+
travel = {
|
|
12178
|
+
axis,
|
|
12179
|
+
size: started.size,
|
|
12180
|
+
travelBack: Boolean(started.travelBack),
|
|
12181
|
+
travelOn: Boolean(started.travelOn),
|
|
12182
|
+
slack: started.slack || 0,
|
|
12183
|
+
// The pixels spent deciding the axis are not pulled back — what
|
|
12184
|
+
// travels sets off from where the finger is at that moment rather
|
|
12185
|
+
// than jumping the threshold it just crossed. Except when the intent
|
|
12186
|
+
// was established before the press (see immediate): there was no
|
|
12187
|
+
// threshold to cross, so every pixel since the grab is the hand's and
|
|
12188
|
+
// is owed to it.
|
|
12189
|
+
origin: immediate ? 0 : covered,
|
|
12190
|
+
pulled: started.slack || 0
|
|
12191
|
+
};
|
|
12192
|
+
document.documentElement.setAttribute(WALKING_ATTRIBUTE, axis);
|
|
12193
|
+
}
|
|
12194
|
+
const {
|
|
12195
|
+
axis
|
|
12196
|
+
} = travel;
|
|
12197
|
+
let pulled = pullOf(gestureInfo);
|
|
12198
|
+
// Which side is being pulled in: dragging to the right brings in what is
|
|
12199
|
+
// on the left, which is what comes BEFORE.
|
|
12200
|
+
let towardsSomething = pulled > 0 ? travel.travelBack : travel.travelOn;
|
|
12201
|
+
// Past the start of the box in hand, and the caller has a box that way:
|
|
12202
|
+
// the hand is not leaning on a wall, it is walking into the next one
|
|
12203
|
+
// backwards. Asked before the resistance, so what it is handed is the
|
|
12204
|
+
// hand's own distance rather than a damped one.
|
|
12205
|
+
if (!towardsSomething && pulled) {
|
|
12206
|
+
const relayed = relayTo(pulled > 0 ? 1 : -1, pulled, gestureInfo);
|
|
12207
|
+
if (relayed !== null) {
|
|
12208
|
+
pulled = relayed;
|
|
12209
|
+
towardsSomething = true;
|
|
12210
|
+
}
|
|
12211
|
+
}
|
|
12212
|
+
let size = travel.size;
|
|
12213
|
+
if (!towardsSomething) {
|
|
12214
|
+
pulled *= DRAG_RESISTANCE;
|
|
12215
|
+
}
|
|
12216
|
+
if (pulled > size || pulled < -size) {
|
|
12217
|
+
const sign = pulled > 0 ? 1 : -1;
|
|
12218
|
+
// How far past the edge the hand has gone. Its own number, because it
|
|
12219
|
+
// is what the next box is owed if there is one.
|
|
12220
|
+
const overshoot = pulled - sign * size;
|
|
12221
|
+
pulled = sign * size;
|
|
12222
|
+
if (towardsSomething) {
|
|
12223
|
+
// A box walked whole, and the finger still going: the caller may have
|
|
12224
|
+
// another one to put under it. Then the gesture WALKS ON — the pixels
|
|
12225
|
+
// past the edge are its first ones, so the hand feels one movement
|
|
12226
|
+
// and not a wall it had to let go of to cross.
|
|
12227
|
+
const relayed = relayTo(sign, overshoot, gestureInfo);
|
|
12228
|
+
if (relayed === null) {
|
|
12229
|
+
// A box travels one box, and the hand can go further than that.
|
|
12230
|
+
// Those extra pixels are not owed back: the gesture is measured
|
|
12231
|
+
// from where the finger IS once it has reached the end, so turning
|
|
12232
|
+
// around moves the picture at once instead of first walking back
|
|
12233
|
+
// over the distance the hand went too far.
|
|
12234
|
+
travel.origin = coveredOn(axis, gestureInfo) - (pulled - travel.slack);
|
|
12235
|
+
} else {
|
|
12236
|
+
pulled = relayed;
|
|
12237
|
+
size = travel.size;
|
|
12238
|
+
}
|
|
12239
|
+
}
|
|
12240
|
+
}
|
|
12241
|
+
travel.pulled = pulled;
|
|
12242
|
+
onPull({
|
|
12243
|
+
axis,
|
|
12244
|
+
pulled,
|
|
12245
|
+
size,
|
|
12246
|
+
progress: pulled / size,
|
|
12247
|
+
event: gestureInfo.dragEvent
|
|
12248
|
+
});
|
|
12249
|
+
},
|
|
12250
|
+
onRelease: gestureInfo => {
|
|
12251
|
+
if (over || !travel) {
|
|
12252
|
+
return;
|
|
12253
|
+
}
|
|
12254
|
+
finish();
|
|
12255
|
+
const {
|
|
12256
|
+
axis,
|
|
12257
|
+
size,
|
|
12258
|
+
pulled,
|
|
12259
|
+
slack
|
|
12260
|
+
} = travel;
|
|
12261
|
+
const towardsSomething = pulled > 0 ? travel.travelBack : travel.travelOn;
|
|
12262
|
+
const velocity = axis === "x" ? gestureInfo.velocityX : gestureInfo.velocityY;
|
|
12263
|
+
// A gesture taken away rather than let go of (the browser scrolling
|
|
12264
|
+
// something else, a call coming in) said nothing: things go back.
|
|
12265
|
+
const releaseEvent = gestureInfo.releaseEvent || gestureInfo.dragEvent;
|
|
12266
|
+
const cancelled = releaseEvent?.type === "pointercancel";
|
|
12267
|
+
onEnd({
|
|
12268
|
+
axis,
|
|
12269
|
+
pulled,
|
|
12270
|
+
size,
|
|
12271
|
+
sign: pulled > 0 ? 1 : -1,
|
|
12272
|
+
travels: !cancelled && travelsAfter({
|
|
12273
|
+
pulled,
|
|
12274
|
+
slack,
|
|
12275
|
+
size,
|
|
12276
|
+
velocity,
|
|
12277
|
+
towardsSomething
|
|
12278
|
+
}),
|
|
12279
|
+
cancelled,
|
|
12280
|
+
event: releaseEvent
|
|
12281
|
+
});
|
|
12282
|
+
}
|
|
12283
|
+
});
|
|
12284
|
+
|
|
12285
|
+
// When a press becomes a gesture, and by which rule. A travel is a swipe, so
|
|
12286
|
+
// the rule is the distance for EVERY pointer: the long press a finger is
|
|
12287
|
+
// asked for elsewhere says "pick this up and carry it", and asking for it
|
|
12288
|
+
// here would mean holding still before being allowed to swipe.
|
|
12289
|
+
const grab = () => {
|
|
12290
|
+
dragGesture = controller.grabViaPointer(pointerDownEvent, {
|
|
12291
|
+
element,
|
|
12292
|
+
// The box, not what the finger landed on: the caller's answer to this
|
|
12293
|
+
// gesture may take that away (a page that travels navigates, and the
|
|
12294
|
+
// router unmounts the page being left), and a capture whose element
|
|
12295
|
+
// leaves the document is a capture the browser drops.
|
|
12296
|
+
pointerCaptureElement: element
|
|
12297
|
+
});
|
|
12298
|
+
return dragGesture;
|
|
12299
|
+
};
|
|
12300
|
+
if (immediate) {
|
|
12301
|
+
// Already in the gesture: what this press landed on was moving, and a hand
|
|
12302
|
+
// that reaches for something in motion has said what it wants by reaching.
|
|
12303
|
+
// Asking it to prove it over ten pixels is asking twice — and over those
|
|
12304
|
+
// pixels the thing it is holding answers to nobody.
|
|
12305
|
+
grab()?.start();
|
|
12306
|
+
} else {
|
|
12307
|
+
dragAfterIntent(pointerDownEvent, grab, {
|
|
12308
|
+
longPress: false,
|
|
12309
|
+
threshold: DRAG_START_THRESHOLD
|
|
12310
|
+
});
|
|
12311
|
+
}
|
|
12312
|
+
window.addEventListener("pointerup", onPressOver);
|
|
12313
|
+
window.addEventListener("pointercancel", onPressOver);
|
|
12314
|
+
return {
|
|
12315
|
+
stop: () => {
|
|
12316
|
+
finish();
|
|
12317
|
+
dragGesture?.release();
|
|
12318
|
+
}
|
|
12319
|
+
};
|
|
12320
|
+
};
|
|
12321
|
+
|
|
12322
|
+
// A wheel gesture has no beginning and no end of its own: it is a stream of
|
|
12323
|
+
// events that starts when the fingers move and stops some time after they are
|
|
12324
|
+
// gone — the tail of it is the momentum the system keeps sending. So the end is
|
|
12325
|
+
// read from silence, and long enough to survive a page that is busy: the frames
|
|
12326
|
+
// right after a travel sets off are the ones where the main thread has the most
|
|
12327
|
+
// to do, and a silence read there as "the hand is gone" would cut one gesture
|
|
12328
|
+
// into several.
|
|
12329
|
+
const WHEEL_GESTURE_END_DELAY = 150;
|
|
12330
|
+
// What each screen AFTER the first costs inside one gesture. Deliberately
|
|
12331
|
+
// steep: reconstructing "how much did that flick mean" from a stream nobody
|
|
12332
|
+
// agrees on is guesswork, and a guess that overshoots leaves someone three
|
|
12333
|
+
// screens from where they were with no idea how they got there. Under-shooting
|
|
12334
|
+
// costs one more push. So the door is open for a gesture that insists, and shut
|
|
12335
|
+
// the rest of the time.
|
|
12336
|
+
const WHEEL_NEXT_STEP_DELTA = 600;
|
|
12337
|
+
// A stream that keeps getting weaker is momentum, not a hand: the system goes
|
|
12338
|
+
// on sending long after the fingers are gone. Counted, one flick becomes five
|
|
12339
|
+
// slides. Two events in a row are asked for rather than one, because a hand
|
|
12340
|
+
// wavers and momentum does not.
|
|
12341
|
+
const WHEEL_FADE_RUN = 2;
|
|
12342
|
+
|
|
12343
|
+
/**
|
|
12344
|
+
* A travel asked for with a wheel, and it asks for a WHOLE ONE.
|
|
12345
|
+
*
|
|
12346
|
+
* Two fingers swiping sideways on a trackpad, a mouse pushed sideways: the
|
|
12347
|
+
* browser sends `wheel` events and, left alone, answers them itself by
|
|
12348
|
+
* scrolling the page, bouncing it, or going back in history. Answering them
|
|
12349
|
+
* here is what stops that — a gesture is either ours or the browser's, and half
|
|
12350
|
+
* of each is what makes a page rock under a travel that is already moving.
|
|
12351
|
+
*
|
|
12352
|
+
* Read as STEPS and not as a distance, which is where this parts company with a
|
|
12353
|
+
* press: a hand on the box holds a screen and says where to put it, so it is
|
|
12354
|
+
* owed every pixel; a wheel points at the next screen and says "that one". What
|
|
12355
|
+
* travels is a row of slides, not a long strip one stops in the middle of, so
|
|
12356
|
+
* one push moves one slide — and the travel that follows plays at its own pace,
|
|
12357
|
+
* exactly as it would from a tab pressed or an arrow key.
|
|
12358
|
+
*
|
|
12359
|
+
* A gesture therefore moves ONE screen the moment it begins, on its first event
|
|
12360
|
+
* and whatever that event is worth: a hand that moved and saw nothing happen
|
|
12361
|
+
* does not wait, it pushes harder. Everything a threshold there would have
|
|
12362
|
+
* bought is bought instead by what the SECOND screen costs, which is a lot —
|
|
12363
|
+
* "how much did that flick mean" cannot be reconstructed from a stream nobody
|
|
12364
|
+
* agrees on, and a guess that overshoots leaves someone three screens away with
|
|
12365
|
+
* no idea how they got there. Under-shooting costs one more push, so that is
|
|
12366
|
+
* the side to be wrong on.
|
|
12367
|
+
*
|
|
12368
|
+
* The rest of the stream is mostly momentum, still arriving with the fingers
|
|
12369
|
+
* gone, and it must not be counted. What gives it away is that momentum only
|
|
12370
|
+
* ever WEAKENS: a stream that keeps shrinking is a push already answered, and a
|
|
12371
|
+
* number that grows again is a hand asking for more.
|
|
12372
|
+
*
|
|
12373
|
+
* @param {Element} element
|
|
12374
|
+
* @param {object} options
|
|
12375
|
+
* @param {"x"|"y"|"xy"} [options.axes="xy"] - which ways this box can travel.
|
|
12376
|
+
* The other one is the content's own scrolling and is left alone.
|
|
12377
|
+
* @param {(detail: {axis: string, sign: number, event: WheelEvent}) => void} options.onStep
|
|
12378
|
+
* - one push, one screen. `sign` is positive towards the start of the axis,
|
|
12379
|
+
* which brings in what comes BEFORE — a wheel says how far the CONTENT
|
|
12380
|
+
* scrolls, and pushing content to the right reveals its left.
|
|
12381
|
+
* @returns {() => void} stop listening.
|
|
12382
|
+
*/
|
|
12383
|
+
const watchWheelTravel = (element, {
|
|
12384
|
+
axes = "xy",
|
|
12385
|
+
onStep
|
|
12386
|
+
}) => {
|
|
12387
|
+
let gesture = null;
|
|
12388
|
+
let endTimeout = null;
|
|
12389
|
+
const forgetGesture = () => {
|
|
12390
|
+
endTimeout = null;
|
|
12391
|
+
gesture = null;
|
|
12392
|
+
document.documentElement.removeAttribute(GESTURE_ATTRIBUTE);
|
|
12393
|
+
document.documentElement.removeAttribute(WALKING_ATTRIBUTE);
|
|
12394
|
+
};
|
|
12395
|
+
|
|
12396
|
+
// Where the hand thinks it is pushing. Not "what the event landed on":
|
|
12397
|
+
// while a view transition is playing, the browser delivers the wheel to the
|
|
12398
|
+
// document root rather than to the box under the pointer, whatever the
|
|
12399
|
+
// pseudo-elements are told about pointer-events. Heard on the box alone, a
|
|
12400
|
+
// gesture that sets a travel off loses every event after the first — and the
|
|
12401
|
+
// page scrolls behind the travel with everything that was not taken.
|
|
12402
|
+
const isOverElement = wheelEvent => {
|
|
12403
|
+
const {
|
|
12404
|
+
target
|
|
12405
|
+
} = wheelEvent;
|
|
12406
|
+
if (element.contains(target)) {
|
|
12407
|
+
return true;
|
|
12408
|
+
}
|
|
12409
|
+
// Something the box is INSIDE, which is what a wheel lands on while a view
|
|
12410
|
+
// transition has taken the box's rendering away: the hit falls through to
|
|
12411
|
+
// the nearest ancestor still being painted. That is the only case worth
|
|
12412
|
+
// measuring for, and asking it this way round costs a walk up the tree
|
|
12413
|
+
// rather than a layout read — a page can hold many travelling boxes, and
|
|
12414
|
+
// every one of them would otherwise measure itself on every wheel event
|
|
12415
|
+
// anywhere.
|
|
12416
|
+
if (!target.contains(element)) {
|
|
12417
|
+
return false;
|
|
12418
|
+
}
|
|
12419
|
+
const {
|
|
12420
|
+
left,
|
|
12421
|
+
right,
|
|
12422
|
+
top,
|
|
12423
|
+
bottom
|
|
12424
|
+
} = element.getBoundingClientRect();
|
|
12425
|
+
const {
|
|
12426
|
+
clientX,
|
|
12427
|
+
clientY
|
|
12428
|
+
} = wheelEvent;
|
|
12429
|
+
return clientX >= left && clientX <= right && clientY >= top && clientY <= bottom;
|
|
12430
|
+
};
|
|
12431
|
+
const onWheel = wheelEvent => {
|
|
12432
|
+
if (!isOverElement(wheelEvent)) {
|
|
12433
|
+
return;
|
|
12434
|
+
}
|
|
12435
|
+
const axis = Math.abs(wheelEvent.deltaX) > Math.abs(wheelEvent.deltaY) ? "x" : "y";
|
|
12436
|
+
const delta = axis === "x" ? wheelEvent.deltaX : wheelEvent.deltaY;
|
|
12437
|
+
if (!delta) {
|
|
12438
|
+
return;
|
|
12439
|
+
}
|
|
12440
|
+
// Which way the screens go, said backwards: a wheel says how far the
|
|
12441
|
+
// CONTENT scrolls, and pushing content to the left brings in what is on the
|
|
12442
|
+
// right.
|
|
12443
|
+
const sign = delta > 0 ? -1 : 1;
|
|
12444
|
+
if (!gesture) {
|
|
12445
|
+
if (!axes.includes(axis)) {
|
|
12446
|
+
// The other axis: the content's own scrolling, left whole to whatever
|
|
12447
|
+
// wants it.
|
|
12448
|
+
return;
|
|
12449
|
+
}
|
|
12450
|
+
// Who owns it, asked once for the gesture rather than for every event of
|
|
12451
|
+
// it — the same two claims a press is read against (see the top of this
|
|
12452
|
+
// file), and both are answered by giving the gesture up whole: nothing is
|
|
12453
|
+
// prevented and the browser scrolls as it would have.
|
|
12454
|
+
const {
|
|
12455
|
+
target
|
|
12456
|
+
} = wheelEvent;
|
|
12457
|
+
if (target.closest && target.closest(DRAG_EXCLUDED_SELECTOR) || scrollRoomTowards(target, element, axis, sign)) {
|
|
12458
|
+
return;
|
|
12459
|
+
}
|
|
12460
|
+
gesture = {
|
|
12461
|
+
axis,
|
|
12462
|
+
sign,
|
|
12463
|
+
pushed: 0,
|
|
12464
|
+
lastMagnitude: 0,
|
|
12465
|
+
fadeRun: 0,
|
|
12466
|
+
stepped: false
|
|
12467
|
+
};
|
|
12468
|
+
document.documentElement.setAttribute(GESTURE_ATTRIBUTE, "");
|
|
12469
|
+
document.documentElement.setAttribute(WALKING_ATTRIBUTE, axis);
|
|
12470
|
+
}
|
|
12471
|
+
// Ours from here, on both axes: what the browser would do with the leftover
|
|
12472
|
+
// — scroll the page behind the box, bounce it, go back in history — is one
|
|
12473
|
+
// gesture answered twice.
|
|
12474
|
+
wheelEvent.preventDefault();
|
|
12475
|
+
clearTimeout(endTimeout);
|
|
12476
|
+
endTimeout = setTimeout(forgetGesture, WHEEL_GESTURE_END_DELAY);
|
|
12477
|
+
if (axis !== gesture.axis) {
|
|
12478
|
+
// The other axis mid-gesture: a hand is never perfectly straight, and the
|
|
12479
|
+
// axis was decided when the gesture set off.
|
|
12480
|
+
return;
|
|
12481
|
+
}
|
|
12482
|
+
if (sign !== gesture.sign) {
|
|
12483
|
+
// Turned around: what was adding up was going the other way.
|
|
12484
|
+
gesture.sign = sign;
|
|
12485
|
+
gesture.pushed = 0;
|
|
12486
|
+
gesture.lastMagnitude = 0;
|
|
12487
|
+
gesture.fadeRun = 0;
|
|
12488
|
+
gesture.stepped = false;
|
|
12489
|
+
}
|
|
12490
|
+
if (!gesture.stepped) {
|
|
12491
|
+
// The first event of a gesture moves a screen, whatever it is worth —
|
|
12492
|
+
// a pixel is a hand that moved, and a hand that moved and saw nothing
|
|
12493
|
+
// happen pushes harder rather than waiting. Everything a threshold could
|
|
12494
|
+
// buy here is bought by what a screen AFTER this one costs.
|
|
12495
|
+
gesture.stepped = true;
|
|
12496
|
+
onStep({
|
|
12497
|
+
axis: gesture.axis,
|
|
12498
|
+
sign: gesture.sign,
|
|
12499
|
+
event: wheelEvent
|
|
12500
|
+
});
|
|
12501
|
+
return;
|
|
12502
|
+
}
|
|
12503
|
+
const magnitude = Math.abs(delta);
|
|
12504
|
+
if (magnitude < gesture.lastMagnitude) {
|
|
12505
|
+
gesture.fadeRun += 1;
|
|
12506
|
+
} else if (magnitude > gesture.lastMagnitude) {
|
|
12507
|
+
// Back up again — a hand asking for more. Momentum never does this.
|
|
12508
|
+
gesture.fadeRun = 0;
|
|
12509
|
+
}
|
|
12510
|
+
gesture.lastMagnitude = magnitude;
|
|
12511
|
+
if (gesture.fadeRun >= WHEEL_FADE_RUN) {
|
|
12512
|
+
return;
|
|
12513
|
+
}
|
|
12514
|
+
gesture.pushed += magnitude;
|
|
12515
|
+
if (gesture.pushed < WHEEL_NEXT_STEP_DELTA) {
|
|
12516
|
+
return;
|
|
12517
|
+
}
|
|
12518
|
+
gesture.pushed = 0;
|
|
12519
|
+
onStep({
|
|
12520
|
+
axis: gesture.axis,
|
|
12521
|
+
sign: gesture.sign,
|
|
12522
|
+
event: wheelEvent
|
|
12523
|
+
});
|
|
12524
|
+
};
|
|
12525
|
+
document.addEventListener("wheel", onWheel, {
|
|
12526
|
+
passive: false,
|
|
12527
|
+
capture: true
|
|
12528
|
+
});
|
|
12529
|
+
return () => {
|
|
12530
|
+
document.removeEventListener("wheel", onWheel, {
|
|
12531
|
+
capture: true
|
|
12532
|
+
});
|
|
12533
|
+
clearTimeout(endTimeout);
|
|
12534
|
+
endTimeout = null;
|
|
12535
|
+
gesture = null;
|
|
12536
|
+
document.documentElement.removeAttribute(GESTURE_ATTRIBUTE);
|
|
12537
|
+
document.documentElement.removeAttribute(WALKING_ATTRIBUTE);
|
|
12538
|
+
};
|
|
12539
|
+
};
|
|
12540
|
+
|
|
11335
12541
|
// Shared by navi's own use_displayed_layout_effect.js (rich "navi_displayed"
|
|
11336
12542
|
// CustomEvent, open transitions only) and visible_rect.js (needs both
|
|
11337
12543
|
// directions: hide when a container closes, recheck when it reopens) — the
|
|
@@ -16869,4 +18075,4 @@ const useResizeStatus = (elementRef, { as = "number" } = {}) => {
|
|
|
16869
18075
|
};
|
|
16870
18076
|
};
|
|
16871
18077
|
|
|
16872
|
-
export { EASING, ELEMENT_SIZE_CHANGE, activeElementSignal, addActiveElementEffect, addAttributeEffect, allowWheelThrough, appendStyles, applyNewPosition, canScroll, captureScrollState, chainEvent, closestOpenableAncestor, contrastColor, createBackgroundColorTransition, createBackgroundTransition, createBorderRadiusTransition, createBorderTransition, createDragGestureController, createDragToMoveGestureController, createEventGroupLogger, createGroupTransitionController, createHeightTransition, createIterableWeakSet, createOpacityTransition, createPubSub, createStyleController, createTimelineTransition, createTransition, createTranslateXTransition, createValueEffect, createWidthTransition, cubicBezier, dispatchCustomEvent, dispatchInternalCustomEvent, dispatchPublicCustomEvent,
|
|
18078
|
+
export { EASING, ELEMENT_SIZE_CHANGE, activeElementSignal, addActiveElementEffect, addAttributeEffect, allowWheelThrough, appendStyles, applyNewPosition, canScroll, captureScrollState, chainEvent, 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, resolveCSSColor, resolveCSSSize, resolveColorLuminance, resolveOklchLightness, scrollIntoViewScoped, scrollIntoViewWithStickyAwareness, scrollRoomTowards, setAttribute, setAttributes, setStyles, snapToPixel, startDragToReorder, startDragToResizeGesture, startDragToTravel, stickyAsRelativeCoords, stringifyStyle, subscribeVisualViewportResizeSettled, subscribeWindowResizeSettled, trapFocusInside, trapScrollInside, useActiveElement, useAvailableHeight, useAvailableWidth, useMaxHeight, useMaxWidth, useResizeStatus, visibleRectEffect, watchWheelTravel };
|