@jsenv/dom 0.17.31 → 0.17.33
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 +119 -27
- package/package.json +1 -1
package/dist/jsenv_dom.js
CHANGED
|
@@ -15008,17 +15008,69 @@ window.addEventListener("resize", (event) => {
|
|
|
15008
15008
|
const getVisibleViewportRect = () => {
|
|
15009
15009
|
const visualViewport = window.visualViewport;
|
|
15010
15010
|
const documentElement = document.documentElement;
|
|
15011
|
-
|
|
15012
|
-
|
|
15013
|
-
|
|
15011
|
+
if (!visualViewport) {
|
|
15012
|
+
return {
|
|
15013
|
+
left: 0,
|
|
15014
|
+
top: 0,
|
|
15015
|
+
width: documentElement.clientWidth,
|
|
15016
|
+
height: Math.max(
|
|
15017
|
+
0,
|
|
15018
|
+
documentElement.clientHeight - getVirtualKeyboardOverlayHeight(),
|
|
15019
|
+
),
|
|
15020
|
+
};
|
|
15021
|
+
}
|
|
15022
|
+
let { offsetLeft, offsetTop, width, height } = visualViewport;
|
|
15023
|
+
// The visual viewport only drifts from the layout viewport while the page is
|
|
15024
|
+
// pinch-zoomed or an on-screen keyboard is up (Safari/Firefox shrink and
|
|
15025
|
+
// pan it to keep the field in view). Neither can be the case at scale 1
|
|
15026
|
+
// with nothing editable focused — an on-screen keyboard is dismissed by
|
|
15027
|
+
// blurring the field — so an offset reported then is a stale one. iOS 26.0
|
|
15028
|
+
// keeps the last keyboard offset and height after the keyboard went away
|
|
15029
|
+
// (https://developer.apple.com/forums/thread/800125): trusted as-is, that
|
|
15030
|
+
// offset sends a bottom-docked sheet past the bottom edge, out of sight.
|
|
15031
|
+
if (
|
|
15032
|
+
visualViewport.scale === 1 &&
|
|
15033
|
+
!isEditableElement(document.activeElement)
|
|
15034
|
+
) {
|
|
15035
|
+
offsetLeft = 0;
|
|
15036
|
+
offsetTop = 0;
|
|
15037
|
+
if (window.innerHeight > height) {
|
|
15038
|
+
height = window.innerHeight;
|
|
15039
|
+
}
|
|
15040
|
+
}
|
|
15014
15041
|
return {
|
|
15015
|
-
left:
|
|
15016
|
-
top:
|
|
15017
|
-
width
|
|
15042
|
+
left: offsetLeft,
|
|
15043
|
+
top: offsetTop,
|
|
15044
|
+
width,
|
|
15018
15045
|
height: Math.max(0, height - getVirtualKeyboardOverlayHeight()),
|
|
15019
15046
|
};
|
|
15020
15047
|
};
|
|
15021
15048
|
|
|
15049
|
+
const isEditableElement = (element) => {
|
|
15050
|
+
if (!element) {
|
|
15051
|
+
return false;
|
|
15052
|
+
}
|
|
15053
|
+
if (element.tagName === "TEXTAREA" || element.isContentEditable) {
|
|
15054
|
+
return true;
|
|
15055
|
+
}
|
|
15056
|
+
if (element.tagName === "INPUT") {
|
|
15057
|
+
return !NON_TYPING_INPUT_TYPES.has(element.type);
|
|
15058
|
+
}
|
|
15059
|
+
return false;
|
|
15060
|
+
};
|
|
15061
|
+
const NON_TYPING_INPUT_TYPES = new Set([
|
|
15062
|
+
"button",
|
|
15063
|
+
"checkbox",
|
|
15064
|
+
"color",
|
|
15065
|
+
"file",
|
|
15066
|
+
"hidden",
|
|
15067
|
+
"image",
|
|
15068
|
+
"radio",
|
|
15069
|
+
"range",
|
|
15070
|
+
"reset",
|
|
15071
|
+
"submit",
|
|
15072
|
+
]);
|
|
15073
|
+
|
|
15022
15074
|
// Minimum fraction of element width/height that must be visible on the preferred side
|
|
15023
15075
|
// before flipping to the opposite side. Prevents flickering near the flip threshold.
|
|
15024
15076
|
const MIN_CONTENT_VISIBILITY_RATIO = 0.6;
|
|
@@ -15634,7 +15686,7 @@ const visibleRectEffect = (
|
|
|
15634
15686
|
onNaviPositionChange,
|
|
15635
15687
|
);
|
|
15636
15688
|
// Dispatched by applyNewPosition's own notifyPositionTransition
|
|
15637
|
-
// around this ancestor's own
|
|
15689
|
+
// around this ancestor's own placement animation (distinct from
|
|
15638
15690
|
// navi_position_change, fired once with the final target, not per
|
|
15639
15691
|
// frame). The anchor this element is positioned against may live
|
|
15640
15692
|
// inside that ancestor and be moving right now — rather than hiding
|
|
@@ -15956,7 +16008,10 @@ const toContainerAlignedPosition = (value) => {
|
|
|
15956
16008
|
* axis never reads the attribute back itself (`positionAreaFixed` always wins).
|
|
15957
16009
|
*
|
|
15958
16010
|
* @param {HTMLElement} element - The element to position (position: absolute or
|
|
15959
|
-
* fixed — detected from its own computed style, see the scroll offset comment below)
|
|
16011
|
+
* fixed — detected from its own computed style, see the scroll offset comment below),
|
|
16012
|
+
* laid out at its containing block's own origin: the returned `left`/`top` are meant
|
|
16013
|
+
* to be applied as a translate from there, see `applyNewPosition` for the whole
|
|
16014
|
+
* contract and for why the placement may not go through `left`/`top` themselves
|
|
15960
16015
|
* @param {HTMLElement} [anchor] - The anchor element to position against. Omit (or pass
|
|
15961
16016
|
* `null`/`undefined`) when there's no real anchor to dock `element` against a *container*
|
|
15962
16017
|
* instead — see `container` below; in that mode, "top"/"bottom"/"left"/"right" are
|
|
@@ -16154,12 +16209,12 @@ const pickPositionRelativeTo = (
|
|
|
16154
16209
|
const clampLeftBound = availableLeft;
|
|
16155
16210
|
const clampRightBound = availableRight;
|
|
16156
16211
|
// offsetWidth/offsetHeight (layout box), not getBoundingClientRect() (the
|
|
16157
|
-
// painted/transformed box): the element being positioned
|
|
16158
|
-
//
|
|
16159
|
-
//
|
|
16160
|
-
// instant
|
|
16161
|
-
//
|
|
16162
|
-
//
|
|
16212
|
+
// painted/transformed box): the element being positioned is moved by
|
|
16213
|
+
// transforms — its own placement translate (see applyNewPosition), plus
|
|
16214
|
+
// whatever `scale` an entrance animation happens to be playing at that
|
|
16215
|
+
// instant (a popover using animation="scaling"/"expand-*") — so
|
|
16216
|
+
// getBoundingClientRect() answers where it is painted, at its *shrunk*
|
|
16217
|
+
// mid-animation size, instead of the box being measured here.
|
|
16163
16218
|
const elementWidth = element.offsetWidth;
|
|
16164
16219
|
const elementHeight = element.offsetHeight;
|
|
16165
16220
|
const anchorWidth = anchorRight - anchorLeft;
|
|
@@ -16668,11 +16723,32 @@ const notifyPositionTransition = (element, animation) => {
|
|
|
16668
16723
|
};
|
|
16669
16724
|
|
|
16670
16725
|
/**
|
|
16671
|
-
* Applies a `pickPositionRelativeTo` result to `element
|
|
16672
|
-
*
|
|
16673
|
-
*
|
|
16674
|
-
*
|
|
16675
|
-
*
|
|
16726
|
+
* Applies a `pickPositionRelativeTo` result to `element`, as a `translate`.
|
|
16727
|
+
*
|
|
16728
|
+
* The two halves of that contract, which a caller must honor:
|
|
16729
|
+
*
|
|
16730
|
+
* 1. `element` stays laid out at its containing block's own origin — its CSS
|
|
16731
|
+
* must say `left: 0; top: 0` (and leave right/bottom `auto`). The computed
|
|
16732
|
+
* `left`/`top` are then applied as a translate from there, which lands the
|
|
16733
|
+
* box in exactly the same place `left: Npx` would have. The reason to go
|
|
16734
|
+
* through a transform at all is the *measuring*: an out-of-flow box with a
|
|
16735
|
+
* shrink-to-fit width can never be wider than "containing block width -
|
|
16736
|
+
* left", so a box placed with `left` reports a width that depends on where
|
|
16737
|
+
* it currently stands — and `pickPositionRelativeTo` decides which side to
|
|
16738
|
+
* place it on from that very width. Feeding a placement back into the
|
|
16739
|
+
* decision that produced it makes the element alternate sides forever, one
|
|
16740
|
+
* flip per reposition. Laid out at the origin, the measured width only ever
|
|
16741
|
+
* depends on the content and on the size caps below.
|
|
16742
|
+
* 2. The `translate` property belongs to this function alone. An animation on
|
|
16743
|
+
* the same element uses `scale` (which composes: individual transform
|
|
16744
|
+
* properties apply translate, then rotate, then scale, then `transform`, so
|
|
16745
|
+
* a scale never rescales the placement) or `transform` — see popup_css.js,
|
|
16746
|
+
* whose slide entrances translate through `transform` for that reason.
|
|
16747
|
+
*
|
|
16748
|
+
* The translate is set instantly (a scroll-triggered reposition should never
|
|
16749
|
+
* lag its target); when `shouldTransition` is set (a resize-triggered
|
|
16750
|
+
* reposition), the visual move is played out via `element.animate()` instead —
|
|
16751
|
+
* kept independent of Popover/Dialog/Callout's own opacity/scale/display CSS
|
|
16676
16752
|
* transition on the same element, so neither can clobber the other (see
|
|
16677
16753
|
* notifyPositionTransition's own doc for why a dedicated Animation over a
|
|
16678
16754
|
* CSS one). Duration comes from `--popup-position-transition-duration`
|
|
@@ -16744,18 +16820,19 @@ const applyNewPosition = (
|
|
|
16744
16820
|
// A single implicit keyframe turned out not to work here: the WAAPI
|
|
16745
16821
|
// "neutral" start keyframe isn't frozen at `animate()` call time, it's
|
|
16746
16822
|
// resolved from the underlying value when the animation is first
|
|
16747
|
-
// *sampled* (the next frame) — by then `element.style.
|
|
16823
|
+
// *sampled* (the next frame) — by then `element.style.translate` below
|
|
16748
16824
|
// has already been overwritten with the new target, so start === end and
|
|
16749
16825
|
// nothing visibly moves (observed as the dialog just jumping). Reading
|
|
16750
16826
|
// the previous value ourselves, before overwriting it, and passing both
|
|
16751
16827
|
// keyframes explicitly sidesteps that entirely.
|
|
16752
|
-
const
|
|
16753
|
-
const
|
|
16828
|
+
const previousTranslate = parseTranslate(element.style.translate);
|
|
16829
|
+
const previousLeft = previousTranslate ? previousTranslate.x : left;
|
|
16830
|
+
const previousTop = previousTranslate ? previousTranslate.y : top;
|
|
16754
16831
|
if (shouldTransition) {
|
|
16755
16832
|
const animation = element.animate(
|
|
16756
16833
|
[
|
|
16757
|
-
{
|
|
16758
|
-
{
|
|
16834
|
+
{ translate: `${previousLeft}px ${previousTop}px` },
|
|
16835
|
+
{ translate: `${left}px ${top}px` },
|
|
16759
16836
|
],
|
|
16760
16837
|
{
|
|
16761
16838
|
duration: parseTransitionDurationMs(
|
|
@@ -16768,16 +16845,31 @@ const applyNewPosition = (
|
|
|
16768
16845
|
);
|
|
16769
16846
|
notifyPositionTransition(element, animation);
|
|
16770
16847
|
}
|
|
16771
|
-
// The specified
|
|
16848
|
+
// The specified translate is set to its final target right away,
|
|
16772
16849
|
// regardless of `shouldTransition` — the animation above only plays the
|
|
16773
16850
|
// visual move from the old position, it never becomes the actual
|
|
16774
16851
|
// specified style (see notifyPositionTransition's own commitStyles for
|
|
16775
16852
|
// why that matters once it ends).
|
|
16776
|
-
element.style.
|
|
16777
|
-
element.style.top = `${top}px`;
|
|
16853
|
+
element.style.translate = `${left}px ${top}px`;
|
|
16778
16854
|
dispatchCustomEvent(element, "navi_position_change");
|
|
16779
16855
|
};
|
|
16780
16856
|
|
|
16857
|
+
// "42px 100px" as { x, y }. Anything else — unset, "none", a single-value
|
|
16858
|
+
// shorthand — reads as "never placed yet" (null), so a first placement has no
|
|
16859
|
+
// stale point to be animated from.
|
|
16860
|
+
const parseTranslate = (translate) => {
|
|
16861
|
+
if (!translate) {
|
|
16862
|
+
return null;
|
|
16863
|
+
}
|
|
16864
|
+
const [x, y] = translate.split(" ");
|
|
16865
|
+
const xNumber = parseFloat(x);
|
|
16866
|
+
const yNumber = parseFloat(y);
|
|
16867
|
+
if (Number.isNaN(xNumber) || Number.isNaN(yNumber)) {
|
|
16868
|
+
return null;
|
|
16869
|
+
}
|
|
16870
|
+
return { x: xNumber, y: yNumber };
|
|
16871
|
+
};
|
|
16872
|
+
|
|
16781
16873
|
const [publishDebugger, subscribeDebugger] = createPubSub();
|
|
16782
16874
|
|
|
16783
16875
|
const notifyDebuggerStart = () => {
|