@jsenv/dom 0.10.2 → 0.10.3
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 +200 -109
- package/package.json +1 -1
package/dist/jsenv_dom.js
CHANGED
|
@@ -1908,6 +1908,8 @@ const pxPropertySet = new Set([
|
|
|
1908
1908
|
"paddingRight",
|
|
1909
1909
|
"paddingBottom",
|
|
1910
1910
|
"paddingLeft",
|
|
1911
|
+
"outlineWidth",
|
|
1912
|
+
"outlineOffset",
|
|
1911
1913
|
"borderWidth",
|
|
1912
1914
|
"borderTopWidth",
|
|
1913
1915
|
"borderRightWidth",
|
|
@@ -6131,10 +6133,10 @@ const trapScrollInside = (element) => {
|
|
|
6131
6133
|
}
|
|
6132
6134
|
const [scrollbarWidth, scrollbarHeight] = measureScrollbar(el);
|
|
6133
6135
|
const paddingRight = parseInt(getStyle(el, "padding-right"), 0);
|
|
6134
|
-
const
|
|
6136
|
+
const paddingBottom = parseInt(getStyle(el, "padding-bottom"), 0);
|
|
6135
6137
|
const removeScrollLockStyles = setStyles(el, {
|
|
6136
6138
|
"padding-right": `${paddingRight + scrollbarWidth}px`,
|
|
6137
|
-
"padding-
|
|
6139
|
+
"padding-bottom": `${paddingBottom + scrollbarHeight}px`,
|
|
6138
6140
|
"overflow": "hidden",
|
|
6139
6141
|
});
|
|
6140
6142
|
cleanupCallbackSet.add(removeScrollLockStyles);
|
|
@@ -9575,6 +9577,10 @@ const stickyAsRelativeCoords = (
|
|
|
9575
9577
|
return [leftPosition, topPosition];
|
|
9576
9578
|
};
|
|
9577
9579
|
|
|
9580
|
+
// Minimum fraction of element width/height that must be visible on the preferred side
|
|
9581
|
+
// before flipping to the opposite side. Prevents flickering near the flip threshold.
|
|
9582
|
+
const MIN_CONTENT_VISIBILITY_RATIO = 0.6;
|
|
9583
|
+
|
|
9578
9584
|
/**
|
|
9579
9585
|
* Tracks how much of an element is visible within its scrollable parent and within the
|
|
9580
9586
|
* document viewport. Calls update() on initialization and whenever visibility changes
|
|
@@ -9876,47 +9882,52 @@ const visibleRectEffect = (element, update) => {
|
|
|
9876
9882
|
};
|
|
9877
9883
|
|
|
9878
9884
|
/**
|
|
9879
|
-
* Places element
|
|
9885
|
+
* Places element relative to anchor with independent control of horizontal and vertical axes.
|
|
9880
9886
|
*
|
|
9881
|
-
*
|
|
9882
|
-
*
|
|
9883
|
-
*
|
|
9884
|
-
*
|
|
9885
|
-
*
|
|
9886
|
-
*
|
|
9887
|
-
*
|
|
9887
|
+
* Horizontal axis — positionX / positionXFixed (left → right):
|
|
9888
|
+
* "to-the-left" element.right = anchor.left (sits entirely to the left of anchor)
|
|
9889
|
+
* "left-aligned" element.left = anchor.left (left edges aligned)
|
|
9890
|
+
* "center" element centered horizontally over anchor (default)
|
|
9891
|
+
* "right-aligned" element.right = anchor.right (right edges aligned)
|
|
9892
|
+
* "to-the-right" element.left = anchor.right (sits entirely to the right of anchor)
|
|
9893
|
+
*
|
|
9894
|
+
* Vertical axis — positionY / positionYFixed (top → bottom):
|
|
9895
|
+
* "above" element.bottom = anchor.top (sits above, no overlap)
|
|
9896
|
+
* "above-overlap" element.bottom = anchor.bottom (sits above, overlapping anchor)
|
|
9897
|
+
* "center" element centered vertically over anchor
|
|
9898
|
+
* "below-overlap" element.top = anchor.top (sits below, overlapping anchor)
|
|
9899
|
+
* "below" element.top = anchor.bottom (sits below, no overlap) (default)
|
|
9888
9900
|
*
|
|
9889
|
-
*
|
|
9890
|
-
*
|
|
9891
|
-
*
|
|
9892
|
-
*
|
|
9893
|
-
*
|
|
9894
|
-
*
|
|
9895
|
-
*
|
|
9896
|
-
*
|
|
9897
|
-
*
|
|
9898
|
-
* - "center" → element centered on anchor (overlapping)
|
|
9901
|
+
* positionX / positionY attempt the requested placement and automatically flip to the
|
|
9902
|
+
* logical opposite when the element does not fit in the viewport:
|
|
9903
|
+
* above ↔ below, above-overlap ↔ below-overlap
|
|
9904
|
+
*
|
|
9905
|
+
* positionXFixed / positionYFixed skip the fit check entirely.
|
|
9906
|
+
*
|
|
9907
|
+
* The resolved X and Y are persisted as data-position-x-current / data-position-y-current
|
|
9908
|
+
* on the element so subsequent calls start from the last resolved position (avoids
|
|
9909
|
+
* flickering when the element is near the flip threshold). Fixed axes are not persisted.
|
|
9899
9910
|
*
|
|
9900
9911
|
* @param {HTMLElement} element - The element to position (must be document-relative)
|
|
9901
9912
|
* @param {HTMLElement} anchor - The anchor element to position against
|
|
9902
9913
|
* @param {object} [options]
|
|
9903
|
-
* @param {string} [options.
|
|
9904
|
-
*
|
|
9905
|
-
*
|
|
9906
|
-
*
|
|
9907
|
-
* the last resolved position is persisted as data-position-current to avoid flickering.
|
|
9908
|
-
* @param {string} [options.position] - Force a specific position, skipping the fit-check.
|
|
9914
|
+
* @param {string} [options.positionX="center"] - Preferred X placement, with viewport fallback.
|
|
9915
|
+
* @param {string} [options.positionY="below"] - Preferred Y placement, with viewport fallback.
|
|
9916
|
+
* @param {string} [options.positionXFixed] - Force X placement, skipping the fit-check.
|
|
9917
|
+
* @param {string} [options.positionYFixed] - Force Y placement, skipping the fit-check.
|
|
9909
9918
|
* @param {number} [options.alignToViewportEdgeWhenAnchorNearEdge=0] - Snap to viewport left
|
|
9910
9919
|
* edge when anchor is within this many px of the left edge and element is wider than anchor.
|
|
9911
9920
|
* @param {number} [options.minLeft=0] - Minimum left coordinate (document-relative).
|
|
9912
|
-
* @returns {{
|
|
9921
|
+
* @returns {{ positionX, positionY, left, top, width, height, anchorLeft, anchorTop, anchorRight, anchorBottom, spaceLeft, spaceRight, spaceAbove, spaceBelow }}
|
|
9913
9922
|
*/
|
|
9914
9923
|
const pickPositionRelativeTo = (
|
|
9915
9924
|
element,
|
|
9916
9925
|
anchor,
|
|
9917
9926
|
{
|
|
9918
|
-
|
|
9919
|
-
|
|
9927
|
+
positionX = "center",
|
|
9928
|
+
positionY = "below",
|
|
9929
|
+
positionXFixed,
|
|
9930
|
+
positionYFixed,
|
|
9920
9931
|
alignToViewportEdgeWhenAnchorNearEdge = 0,
|
|
9921
9932
|
minLeft = 0,
|
|
9922
9933
|
} = {},
|
|
@@ -9944,61 +9955,151 @@ const pickPositionRelativeTo = (
|
|
|
9944
9955
|
const anchorWidth = anchorRight - anchorLeft;
|
|
9945
9956
|
const anchorHeight = anchorBottom - anchorTop;
|
|
9946
9957
|
|
|
9947
|
-
// Determine the active position: position wins, then data-position-current (last resolved),
|
|
9948
|
-
// then data-position-try attribute (user preference), then positionTry param
|
|
9949
|
-
let activePosition;
|
|
9950
|
-
if (position) {
|
|
9951
|
-
activePosition = position;
|
|
9952
|
-
} else {
|
|
9953
|
-
const positionCurrentFromAttribute = element.getAttribute(
|
|
9954
|
-
"data-position-current",
|
|
9955
|
-
);
|
|
9956
|
-
const positionTryFromAttribute = element.getAttribute("data-position-try");
|
|
9957
|
-
activePosition =
|
|
9958
|
-
positionCurrentFromAttribute || positionTryFromAttribute || positionTry;
|
|
9959
|
-
}
|
|
9960
|
-
|
|
9961
9958
|
const spaceAbove = anchorTop;
|
|
9962
9959
|
const spaceBelow = viewportHeight - anchorBottom;
|
|
9960
|
+
const spaceLeft = anchorLeft;
|
|
9961
|
+
const spaceRight = viewportWidth - anchorRight;
|
|
9962
|
+
|
|
9963
|
+
// Resolve active X and Y, and whether each is fixed (no flip fallback)
|
|
9964
|
+
let activeX;
|
|
9965
|
+
let activeY;
|
|
9966
|
+
const xIsFixed = Boolean(positionXFixed);
|
|
9967
|
+
const yIsFixed = Boolean(positionYFixed);
|
|
9968
|
+
const hasStoredY = Boolean(element.getAttribute("data-position-y-current"));
|
|
9969
|
+
const hasStoredX = Boolean(element.getAttribute("data-position-x-current"));
|
|
9970
|
+
if (xIsFixed) {
|
|
9971
|
+
activeX = positionXFixed;
|
|
9972
|
+
} else {
|
|
9973
|
+
const storedX = element.getAttribute("data-position-x-current");
|
|
9974
|
+
activeX = storedX ?? positionX;
|
|
9975
|
+
}
|
|
9976
|
+
if (yIsFixed) {
|
|
9977
|
+
activeY = positionYFixed;
|
|
9978
|
+
} else {
|
|
9979
|
+
const storedY = element.getAttribute("data-position-y-current");
|
|
9980
|
+
activeY = storedY ?? positionY;
|
|
9981
|
+
}
|
|
9963
9982
|
|
|
9964
|
-
// Resolve
|
|
9965
|
-
|
|
9966
|
-
|
|
9967
|
-
|
|
9968
|
-
|
|
9969
|
-
|
|
9970
|
-
|
|
9971
|
-
|
|
9972
|
-
|
|
9973
|
-
|
|
9974
|
-
|
|
9975
|
-
|
|
9976
|
-
|
|
9977
|
-
|
|
9978
|
-
|
|
9979
|
-
|
|
9983
|
+
// Resolve final Y
|
|
9984
|
+
let finalY;
|
|
9985
|
+
{
|
|
9986
|
+
const oppositeY = {
|
|
9987
|
+
"above": "below",
|
|
9988
|
+
"below": "above",
|
|
9989
|
+
"above-overlap": "below-overlap",
|
|
9990
|
+
"below-overlap": "above-overlap",
|
|
9991
|
+
};
|
|
9992
|
+
// Compute effective space for a given Y value
|
|
9993
|
+
const spaceFor = (y) => {
|
|
9994
|
+
if (y === "above") {
|
|
9995
|
+
return spaceAbove;
|
|
9996
|
+
}
|
|
9997
|
+
if (y === "above-overlap") {
|
|
9998
|
+
return spaceAbove + anchorHeight;
|
|
9999
|
+
}
|
|
10000
|
+
if (y === "below") {
|
|
10001
|
+
return spaceBelow;
|
|
10002
|
+
}
|
|
10003
|
+
if (y === "below-overlap") {
|
|
10004
|
+
return spaceBelow + anchorHeight;
|
|
10005
|
+
}
|
|
10006
|
+
return Infinity; // center
|
|
10007
|
+
};
|
|
10008
|
+
if (yIsFixed || activeY === "center") {
|
|
10009
|
+
finalY = activeY;
|
|
10010
|
+
} else if (!hasStoredY) {
|
|
10011
|
+
// Never positioned before — pick the best side from scratch.
|
|
10012
|
+
const preferred = positionY;
|
|
10013
|
+
const opposite = oppositeY[preferred];
|
|
10014
|
+
const preferredFits = spaceFor(preferred) >= elementHeight;
|
|
10015
|
+
const oppositeFits = spaceFor(opposite) >= elementHeight;
|
|
10016
|
+
if (preferredFits) {
|
|
10017
|
+
// Preferred fits completely — use it (even if opposite also fits)
|
|
10018
|
+
finalY = preferred;
|
|
10019
|
+
} else if (oppositeFits) {
|
|
10020
|
+
// Only opposite fits completely — flip
|
|
10021
|
+
finalY = opposite;
|
|
10022
|
+
} else {
|
|
10023
|
+
// Neither fits completely — use whichever meets the minimum ratio
|
|
10024
|
+
const preferredMeetsRatio =
|
|
10025
|
+
spaceFor(preferred) / elementHeight >= MIN_CONTENT_VISIBILITY_RATIO;
|
|
10026
|
+
finalY = preferredMeetsRatio ? preferred : opposite;
|
|
10027
|
+
}
|
|
9980
10028
|
} else {
|
|
9981
|
-
|
|
10029
|
+
// Previously positioned — stay as long as current side meets minimum ratio
|
|
10030
|
+
const currentFitsEnough =
|
|
10031
|
+
spaceFor(activeY) / elementHeight >= MIN_CONTENT_VISIBILITY_RATIO;
|
|
10032
|
+
if (currentFitsEnough) {
|
|
10033
|
+
finalY = activeY;
|
|
10034
|
+
} else {
|
|
10035
|
+
finalY = oppositeY[activeY];
|
|
10036
|
+
}
|
|
9982
10037
|
}
|
|
9983
|
-
}
|
|
9984
|
-
|
|
9985
|
-
|
|
9986
|
-
|
|
9987
|
-
|
|
10038
|
+
}
|
|
10039
|
+
|
|
10040
|
+
// Resolve final X
|
|
10041
|
+
let finalX;
|
|
10042
|
+
{
|
|
10043
|
+
const oppositeX = {
|
|
10044
|
+
"to-the-left": "to-the-right",
|
|
10045
|
+
"to-the-right": "to-the-left",
|
|
10046
|
+
"left-aligned": "right-aligned",
|
|
10047
|
+
"right-aligned": "left-aligned",
|
|
10048
|
+
};
|
|
10049
|
+
// Compute effective space for a given X value
|
|
10050
|
+
const spaceFor = (x) => {
|
|
10051
|
+
if (x === "to-the-left") {
|
|
10052
|
+
return spaceLeft;
|
|
10053
|
+
}
|
|
10054
|
+
if (x === "left-aligned") {
|
|
10055
|
+
return viewportWidth - anchorLeft;
|
|
10056
|
+
}
|
|
10057
|
+
if (x === "right-aligned") {
|
|
10058
|
+
return anchorRight;
|
|
10059
|
+
}
|
|
10060
|
+
if (x === "to-the-right") {
|
|
10061
|
+
return spaceRight;
|
|
10062
|
+
}
|
|
10063
|
+
return Infinity; // center
|
|
10064
|
+
};
|
|
10065
|
+
if (xIsFixed || activeX === "center") {
|
|
10066
|
+
finalX = activeX;
|
|
10067
|
+
} else if (!hasStoredX) {
|
|
10068
|
+
// Never positioned before — pick the best side from scratch.
|
|
10069
|
+
const preferred = positionX;
|
|
10070
|
+
const opposite = oppositeX[preferred];
|
|
10071
|
+
const preferredFits = spaceFor(preferred) >= elementWidth;
|
|
10072
|
+
const oppositeFits = spaceFor(opposite) >= elementWidth;
|
|
10073
|
+
if (preferredFits) {
|
|
10074
|
+
finalX = preferred;
|
|
10075
|
+
} else if (oppositeFits) {
|
|
10076
|
+
finalX = opposite;
|
|
10077
|
+
} else {
|
|
10078
|
+
const preferredMeetsRatio =
|
|
10079
|
+
spaceFor(preferred) / elementWidth >= MIN_CONTENT_VISIBILITY_RATIO;
|
|
10080
|
+
finalX = preferredMeetsRatio ? preferred : opposite;
|
|
10081
|
+
}
|
|
9988
10082
|
} else {
|
|
9989
|
-
|
|
10083
|
+
// Previously positioned — stay as long as current side meets minimum ratio
|
|
10084
|
+
const currentFitsEnough =
|
|
10085
|
+
spaceFor(activeX) / elementWidth >= MIN_CONTENT_VISIBILITY_RATIO;
|
|
10086
|
+
if (currentFitsEnough) {
|
|
10087
|
+
finalX = activeX;
|
|
10088
|
+
} else {
|
|
10089
|
+
finalX = oppositeX[activeX];
|
|
10090
|
+
}
|
|
9990
10091
|
}
|
|
9991
10092
|
}
|
|
9992
10093
|
|
|
9993
10094
|
// Calculate horizontal position (viewport-relative)
|
|
9994
10095
|
let elementPositionLeft;
|
|
9995
10096
|
{
|
|
9996
|
-
if (
|
|
10097
|
+
if (finalX === "to-the-left") {
|
|
9997
10098
|
elementPositionLeft = anchorLeft - elementWidth;
|
|
9998
|
-
} else if (
|
|
9999
|
-
elementPositionLeft =
|
|
10000
|
-
} else {
|
|
10001
|
-
//
|
|
10099
|
+
} else if (finalX === "left-aligned") {
|
|
10100
|
+
elementPositionLeft = anchorLeft;
|
|
10101
|
+
} else if (finalX === "center") {
|
|
10102
|
+
// Complex logic handles wide anchors and viewport-edge snapping
|
|
10002
10103
|
const anchorIsWiderThanViewport = anchorWidth > viewportWidth;
|
|
10003
10104
|
if (anchorIsWiderThanViewport) {
|
|
10004
10105
|
const anchorLeftIsVisible = anchorLeft >= 0;
|
|
@@ -10027,6 +10128,11 @@ const pickPositionRelativeTo = (
|
|
|
10027
10128
|
}
|
|
10028
10129
|
}
|
|
10029
10130
|
}
|
|
10131
|
+
} else if (finalX === "right-aligned") {
|
|
10132
|
+
elementPositionLeft = anchorRight - elementWidth;
|
|
10133
|
+
} else {
|
|
10134
|
+
// "to-the-right"
|
|
10135
|
+
elementPositionLeft = anchorRight;
|
|
10030
10136
|
}
|
|
10031
10137
|
// Constrain horizontal position to viewport boundaries
|
|
10032
10138
|
if (elementPositionLeft < 0) {
|
|
@@ -10039,40 +10145,33 @@ const pickPositionRelativeTo = (
|
|
|
10039
10145
|
// Calculate vertical position (viewport-relative)
|
|
10040
10146
|
let elementPositionTop;
|
|
10041
10147
|
{
|
|
10042
|
-
if (
|
|
10148
|
+
if (finalY === "above") {
|
|
10149
|
+
const idealTop = anchorTop - elementHeight;
|
|
10150
|
+
elementPositionTop = idealTop < 0 ? 0 : idealTop;
|
|
10151
|
+
} else if (finalY === "above-overlap") {
|
|
10152
|
+
const idealTop = anchorBottom - elementHeight;
|
|
10153
|
+
elementPositionTop = idealTop < 0 ? 0 : idealTop;
|
|
10154
|
+
} else if (finalY === "center") {
|
|
10043
10155
|
elementPositionTop = anchorTop + anchorHeight / 2 - elementHeight / 2;
|
|
10044
|
-
} else if (
|
|
10045
|
-
const idealTop =
|
|
10156
|
+
} else if (finalY === "below-overlap") {
|
|
10157
|
+
const idealTop = anchorTop;
|
|
10046
10158
|
elementPositionTop =
|
|
10047
10159
|
idealTop % 1 === 0 ? idealTop : Math.floor(idealTop) + 1;
|
|
10048
10160
|
} else {
|
|
10049
|
-
// "
|
|
10050
|
-
const idealTop =
|
|
10051
|
-
elementPositionTop =
|
|
10161
|
+
// "below"
|
|
10162
|
+
const idealTop = anchorBottom;
|
|
10163
|
+
elementPositionTop =
|
|
10164
|
+
idealTop % 1 === 0 ? idealTop : Math.floor(idealTop) + 1;
|
|
10052
10165
|
}
|
|
10053
10166
|
}
|
|
10054
10167
|
|
|
10055
|
-
|
|
10056
|
-
|
|
10057
|
-
|
|
10058
|
-
|
|
10059
|
-
if (vertPart && horzPart) {
|
|
10060
|
-
finalPosition = `${vertPart}-${horzPart}`;
|
|
10061
|
-
} else if (vertPart) {
|
|
10062
|
-
finalPosition = vertPart;
|
|
10063
|
-
} else if (horzPart) {
|
|
10064
|
-
finalPosition = horzPart;
|
|
10065
|
-
} else {
|
|
10066
|
-
finalPosition = "center";
|
|
10067
|
-
}
|
|
10168
|
+
// Persist resolved X/Y so subsequent calls start from here (avoids flickering).
|
|
10169
|
+
// Fixed axes are not persisted.
|
|
10170
|
+
if (!xIsFixed) {
|
|
10171
|
+
element.setAttribute("data-position-x-current", finalX);
|
|
10068
10172
|
}
|
|
10069
|
-
|
|
10070
|
-
|
|
10071
|
-
// (avoids flickering between positions when the element is near the threshold).
|
|
10072
|
-
// position is not persisted — it is always explicit.
|
|
10073
|
-
|
|
10074
|
-
if (!position) {
|
|
10075
|
-
element.setAttribute("data-position-current", finalPosition);
|
|
10173
|
+
if (!yIsFixed) {
|
|
10174
|
+
element.setAttribute("data-position-y-current", finalY);
|
|
10076
10175
|
}
|
|
10077
10176
|
|
|
10078
10177
|
// Get document scroll for final coordinate conversion
|
|
@@ -10085,7 +10184,8 @@ const pickPositionRelativeTo = (
|
|
|
10085
10184
|
const anchorDocumentBottom = anchorBottom + scrollTop;
|
|
10086
10185
|
|
|
10087
10186
|
return {
|
|
10088
|
-
|
|
10187
|
+
positionX: finalX,
|
|
10188
|
+
positionY: finalY,
|
|
10089
10189
|
left: elementDocumentLeft,
|
|
10090
10190
|
top: elementDocumentTop,
|
|
10091
10191
|
width: elementWidth,
|
|
@@ -10094,21 +10194,12 @@ const pickPositionRelativeTo = (
|
|
|
10094
10194
|
anchorTop: anchorDocumentTop,
|
|
10095
10195
|
anchorRight: anchorDocumentRight,
|
|
10096
10196
|
anchorBottom: anchorDocumentBottom,
|
|
10197
|
+
spaceLeft,
|
|
10198
|
+
spaceRight,
|
|
10097
10199
|
spaceAbove,
|
|
10098
10200
|
spaceBelow,
|
|
10099
10201
|
};
|
|
10100
10202
|
};
|
|
10101
|
-
// Decompose position flags
|
|
10102
|
-
const decomposePosition = (pos) => {
|
|
10103
|
-
return {
|
|
10104
|
-
isTop: pos === "top" || pos === "top-left" || pos === "top-right",
|
|
10105
|
-
isBottom:
|
|
10106
|
-
pos === "bottom" || pos === "bottom-left" || pos === "bottom-right",
|
|
10107
|
-
isLeft: pos === "left" || pos === "top-left" || pos === "bottom-left",
|
|
10108
|
-
isRight: pos === "right" || pos === "top-right" || pos === "bottom-right",
|
|
10109
|
-
isCenter: pos === "center",
|
|
10110
|
-
};
|
|
10111
|
-
};
|
|
10112
10203
|
|
|
10113
10204
|
const [publishDebugger, subscribeDebugger] = createPubSub();
|
|
10114
10205
|
|