@jsenv/dom 0.9.1 → 0.9.2
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 +139 -59
- package/package.json +2 -2
package/dist/jsenv_dom.js
CHANGED
|
@@ -385,7 +385,8 @@ const areSameRGBA = (first, second) => {
|
|
|
385
385
|
};
|
|
386
386
|
const resolveCSSColor = (color, element) => {
|
|
387
387
|
const rgba = parseCSSColor(color, element);
|
|
388
|
-
|
|
388
|
+
const colorString = stringifyCSSColor(rgba);
|
|
389
|
+
return colorString;
|
|
389
390
|
};
|
|
390
391
|
|
|
391
392
|
/**
|
|
@@ -440,12 +441,27 @@ const parseCSSColor = (color, element) => {
|
|
|
440
441
|
return color;
|
|
441
442
|
}
|
|
442
443
|
|
|
444
|
+
// oklab(L a b) and oklab(L a b / alpha)
|
|
445
|
+
if (color.startsWith("oklab(")) {
|
|
446
|
+
const oklabMatch = color.match(
|
|
447
|
+
/^oklab\(\s*([\d.]+)\s+(-?[\d.]+)\s+(-?[\d.]+)(?:\s*\/\s*([\d.]+))?\s*\)$/,
|
|
448
|
+
);
|
|
449
|
+
if (oklabMatch) {
|
|
450
|
+
const L = parseFloat(oklabMatch[1]);
|
|
451
|
+
const a = parseFloat(oklabMatch[2]);
|
|
452
|
+
const b = parseFloat(oklabMatch[3]);
|
|
453
|
+
const alpha = oklabMatch[4] !== undefined ? parseFloat(oklabMatch[4]) : 1;
|
|
454
|
+
const [r, g, bChannel] = oklabToRgb(L, a, b);
|
|
455
|
+
return [r, g, bChannel, alpha];
|
|
456
|
+
}
|
|
457
|
+
return color;
|
|
458
|
+
}
|
|
459
|
+
|
|
443
460
|
// Pass through CSS color functions we don't handle
|
|
444
461
|
if (
|
|
445
462
|
color.startsWith("lch(") ||
|
|
446
463
|
color.startsWith("oklch(") ||
|
|
447
464
|
color.startsWith("lab(") ||
|
|
448
|
-
color.startsWith("oklab(") ||
|
|
449
465
|
color.startsWith("hwb(") ||
|
|
450
466
|
color.includes("color-contrast(")
|
|
451
467
|
) {
|
|
@@ -505,6 +521,27 @@ const parseCSSColor = (color, element) => {
|
|
|
505
521
|
const rgba = convertColorToRgba(resolvedColor);
|
|
506
522
|
return rgba;
|
|
507
523
|
};
|
|
524
|
+
const oklabToRgb = (L, a, b) => {
|
|
525
|
+
const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
|
|
526
|
+
const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
|
|
527
|
+
const s_ = L - 0.0894841775 * a - 1.291485548 * b;
|
|
528
|
+
const l = l_ * l_ * l_;
|
|
529
|
+
const m = m_ * m_ * m_;
|
|
530
|
+
const s = s_ * s_ * s_;
|
|
531
|
+
const rLinear = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s;
|
|
532
|
+
const gLinear = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s;
|
|
533
|
+
const bLinear = -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s;
|
|
534
|
+
const toSrgb = (linear) => {
|
|
535
|
+
const clamped = linear < 0 ? 0 : linear > 1 ? 1 : linear;
|
|
536
|
+
const srgb =
|
|
537
|
+
clamped <= 0.0031308
|
|
538
|
+
? 12.92 * clamped
|
|
539
|
+
: 1.055 * Math.pow(clamped, 1 / 2.4) - 0.055;
|
|
540
|
+
return Math.round(srgb * 255);
|
|
541
|
+
};
|
|
542
|
+
return [toSrgb(rLinear), toSrgb(gLinear), toSrgb(bLinear)];
|
|
543
|
+
};
|
|
544
|
+
|
|
508
545
|
/**
|
|
509
546
|
* Converts HSL color to RGB
|
|
510
547
|
* @param {number} h - Hue (0-360)
|
|
@@ -637,12 +674,10 @@ const stringifyCSSColor = (value) => {
|
|
|
637
674
|
}
|
|
638
675
|
const rgba = value;
|
|
639
676
|
const [r, g, b, a = 1] = rgba;
|
|
640
|
-
|
|
641
677
|
// Validate RGB values
|
|
642
678
|
if (r < 0 || r > 255 || g < 0 || g > 255 || b < 0 || b > 255) {
|
|
643
679
|
return null;
|
|
644
680
|
}
|
|
645
|
-
|
|
646
681
|
// Validate alpha value
|
|
647
682
|
if (a < 0 || a > 1) {
|
|
648
683
|
return null;
|
|
@@ -660,10 +695,7 @@ const stringifyCSSColor = (value) => {
|
|
|
660
695
|
return name;
|
|
661
696
|
}
|
|
662
697
|
}
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
// Use rgb() for opaque colors, rgba() for transparent
|
|
666
|
-
if (a === 1) {
|
|
698
|
+
// Use rgb() for opaque colors, rgba() for transparent
|
|
667
699
|
return `rgb(${rInt}, ${gInt}, ${bInt})`;
|
|
668
700
|
}
|
|
669
701
|
if (a === 0 && rInt === 0 && gInt === 0 && bInt === 0) {
|
|
@@ -4034,12 +4066,6 @@ const performArrowNavigation = (
|
|
|
4034
4066
|
}
|
|
4035
4067
|
|
|
4036
4068
|
const onTargetToFocus = (targetToFocus) => {
|
|
4037
|
-
console.debug(
|
|
4038
|
-
`Arrow navigation: ${event.key} from`,
|
|
4039
|
-
activeElement,
|
|
4040
|
-
"to",
|
|
4041
|
-
targetToFocus,
|
|
4042
|
-
);
|
|
4043
4069
|
event.preventDefault();
|
|
4044
4070
|
markFocusNav(event);
|
|
4045
4071
|
targetToFocus.focus();
|
|
@@ -7041,6 +7067,8 @@ installImportMetaCss(import.meta);const setupConstraintFeedbackLine = () => {
|
|
|
7041
7067
|
const { grabEvent, dragEvent } = gestureInfo;
|
|
7042
7068
|
if (
|
|
7043
7069
|
grabEvent.type === "programmatic" ||
|
|
7070
|
+
// dragEvent can be null when only mousedown without yet any mousemove
|
|
7071
|
+
!dragEvent ||
|
|
7044
7072
|
dragEvent.type === "programmatic"
|
|
7045
7073
|
) {
|
|
7046
7074
|
// programmatic drag
|
|
@@ -8913,31 +8941,48 @@ const initPositionSticky = (element) => {
|
|
|
8913
8941
|
const computedStyle = getComputedStyle(element);
|
|
8914
8942
|
const topCssValue = computedStyle.top;
|
|
8915
8943
|
const top = parseFloat(topCssValue);
|
|
8916
|
-
|
|
8917
|
-
|
|
8944
|
+
const leftCssValue = computedStyle.left;
|
|
8945
|
+
const left = parseFloat(leftCssValue);
|
|
8946
|
+
const hasTop = !isNaN(top);
|
|
8947
|
+
const hasLeft = !isNaN(left);
|
|
8948
|
+
if (!hasTop && !hasLeft) {
|
|
8949
|
+
return () => {}; // Early return if no valid top or left value
|
|
8918
8950
|
}
|
|
8919
8951
|
|
|
8920
8952
|
// Skip polyfill if native position:sticky would work (no overflow:auto/hidden parents)
|
|
8921
8953
|
const scrollContainerSet = getScrollContainerSet(element);
|
|
8922
|
-
|
|
8923
|
-
|
|
8924
|
-
|
|
8925
|
-
|
|
8926
|
-
|
|
8927
|
-
|
|
8928
|
-
|
|
8929
|
-
|
|
8930
|
-
}
|
|
8931
|
-
const overflowY = scrollContainerComputedStyle.overflowY;
|
|
8932
|
-
if (overflowY === "auto" || overflowY === "hidden") {
|
|
8933
|
-
hasOverflowHiddenOrAuto = true;
|
|
8934
|
-
break;
|
|
8935
|
-
}
|
|
8954
|
+
// Determine per-axis whether an intermediate container blocks native sticky.
|
|
8955
|
+
// Native sticky fails only when there is a scroll container between the element
|
|
8956
|
+
// and the document with overflow set on that axis.
|
|
8957
|
+
let xScrollContainer = null; // first intermediate container blocking horizontal sticky
|
|
8958
|
+
let yScrollContainer = null; // first intermediate container blocking vertical sticky
|
|
8959
|
+
for (const scrollContainer of scrollContainerSet) {
|
|
8960
|
+
if (scrollContainer === document.documentElement) {
|
|
8961
|
+
break;
|
|
8936
8962
|
}
|
|
8937
|
-
|
|
8938
|
-
|
|
8963
|
+
const style = getComputedStyle(scrollContainer);
|
|
8964
|
+
if (
|
|
8965
|
+
xScrollContainer === null &&
|
|
8966
|
+
(style.overflowX === "auto" ||
|
|
8967
|
+
style.overflowX === "hidden" ||
|
|
8968
|
+
style.overflowX === "scroll")
|
|
8969
|
+
) {
|
|
8970
|
+
xScrollContainer = scrollContainer;
|
|
8971
|
+
}
|
|
8972
|
+
if (
|
|
8973
|
+
yScrollContainer === null &&
|
|
8974
|
+
(style.overflowY === "auto" ||
|
|
8975
|
+
style.overflowY === "hidden" ||
|
|
8976
|
+
style.overflowY === "scroll")
|
|
8977
|
+
) {
|
|
8978
|
+
yScrollContainer = scrollContainer;
|
|
8939
8979
|
}
|
|
8940
8980
|
}
|
|
8981
|
+
const needsPolyfillX = hasLeft && xScrollContainer !== null;
|
|
8982
|
+
const needsPolyfillY = hasTop && yScrollContainer !== null;
|
|
8983
|
+
if (!needsPolyfillX && !needsPolyfillY) {
|
|
8984
|
+
return () => {}; // Native sticky will work fine on both axes
|
|
8985
|
+
}
|
|
8941
8986
|
|
|
8942
8987
|
const cleanupCallbackSet = new Set();
|
|
8943
8988
|
const cleanup = () => {
|
|
@@ -8983,39 +9028,72 @@ const initPositionSticky = (element) => {
|
|
|
8983
9028
|
const placeholderRect = placeholder.getBoundingClientRect();
|
|
8984
9029
|
const parentRect = parentElement.getBoundingClientRect();
|
|
8985
9030
|
|
|
8986
|
-
//
|
|
8987
|
-
|
|
8988
|
-
|
|
8989
|
-
|
|
8990
|
-
//
|
|
9031
|
+
// The CSS `top`/`left` values are offsets from the scroll container's edge.
|
|
9032
|
+
// getBoundingClientRect() always returns viewport coordinates (already accounting
|
|
9033
|
+
// for scroll position of all ancestors), so to convert the CSS offset to a
|
|
9034
|
+
// viewport threshold we add the scroll container's own viewport position.
|
|
9035
|
+
//
|
|
9036
|
+
// Example: main starts at viewport x=250, left=0 → leftThreshold=250.
|
|
9037
|
+
// After scrolling main 670px: placeholderRect.left = 250-670 = -420.
|
|
9038
|
+
// -420 <= 250 → stuck → element.style.left = 250px (main's left edge). ✓
|
|
9039
|
+
//
|
|
9040
|
+
// If no intermediate scroll container exists, use 0 (document/viewport edge).
|
|
9041
|
+
const yContainerRect = yScrollContainer
|
|
9042
|
+
? yScrollContainer.getBoundingClientRect()
|
|
9043
|
+
: { top: 0 };
|
|
9044
|
+
const xContainerRect = xScrollContainer
|
|
9045
|
+
? xScrollContainer.getBoundingClientRect()
|
|
9046
|
+
: { left: 0 };
|
|
9047
|
+
const topThreshold = yContainerRect.top + top;
|
|
9048
|
+
const leftThreshold = xContainerRect.left + left;
|
|
9049
|
+
|
|
9050
|
+
// ── Vertical (top) ──────────────────────────────────────────────────────
|
|
8991
9051
|
let topPosition;
|
|
8992
|
-
let
|
|
8993
|
-
|
|
8994
|
-
|
|
8995
|
-
|
|
8996
|
-
|
|
8997
|
-
|
|
8998
|
-
|
|
8999
|
-
|
|
9000
|
-
|
|
9001
|
-
|
|
9002
|
-
|
|
9003
|
-
|
|
9004
|
-
|
|
9005
|
-
// Adjust to stay within parent
|
|
9006
|
-
topPosition = parentBottom - height;
|
|
9052
|
+
let isStuckVertically = false;
|
|
9053
|
+
if (hasTop) {
|
|
9054
|
+
if (placeholderRect.top <= topThreshold) {
|
|
9055
|
+
topPosition = topThreshold;
|
|
9056
|
+
isStuckVertically = true;
|
|
9057
|
+
// Don't go beyond parent's bottom boundary
|
|
9058
|
+
const parentBottom = parentRect.bottom;
|
|
9059
|
+
const elementBottom = topThreshold + height;
|
|
9060
|
+
if (elementBottom > parentBottom) {
|
|
9061
|
+
topPosition = parentBottom - height;
|
|
9062
|
+
}
|
|
9063
|
+
} else {
|
|
9064
|
+
topPosition = placeholderRect.top;
|
|
9007
9065
|
}
|
|
9008
9066
|
} else {
|
|
9009
|
-
// Element should be at its natural position in the flow
|
|
9010
9067
|
topPosition = placeholderRect.top;
|
|
9011
9068
|
}
|
|
9012
9069
|
|
|
9070
|
+
// ── Horizontal (left) ───────────────────────────────────────────────────
|
|
9071
|
+
let leftPosition;
|
|
9072
|
+
let isStuckHorizontally = false;
|
|
9073
|
+
if (hasLeft) {
|
|
9074
|
+
if (placeholderRect.left <= leftThreshold) {
|
|
9075
|
+
leftPosition = leftThreshold;
|
|
9076
|
+
isStuckHorizontally = true;
|
|
9077
|
+
// Don't go beyond parent's right boundary
|
|
9078
|
+
const parentRight = parentRect.right;
|
|
9079
|
+
const elementRight = leftThreshold + width;
|
|
9080
|
+
if (elementRight > parentRight) {
|
|
9081
|
+
leftPosition = parentRight - width;
|
|
9082
|
+
}
|
|
9083
|
+
} else {
|
|
9084
|
+
leftPosition = placeholderRect.left;
|
|
9085
|
+
}
|
|
9086
|
+
} else {
|
|
9087
|
+
leftPosition = placeholderRect.left;
|
|
9088
|
+
}
|
|
9089
|
+
|
|
9013
9090
|
element.style.top = `${topPosition}px`;
|
|
9091
|
+
element.style.left = `${Math.round(leftPosition)}px`;
|
|
9014
9092
|
element.style.width = `${width}px`;
|
|
9015
9093
|
element.style.height = `${height}px`;
|
|
9016
9094
|
|
|
9017
9095
|
// Set attribute for potential styling
|
|
9018
|
-
if (
|
|
9096
|
+
if (isStuckVertically || isStuckHorizontally) {
|
|
9019
9097
|
element.setAttribute("data-sticky", "");
|
|
9020
9098
|
} else {
|
|
9021
9099
|
element.removeAttribute("data-sticky");
|
|
@@ -9038,12 +9116,14 @@ const initPositionSticky = (element) => {
|
|
|
9038
9116
|
updatePosition();
|
|
9039
9117
|
};
|
|
9040
9118
|
|
|
9041
|
-
|
|
9042
|
-
|
|
9043
|
-
|
|
9044
|
-
|
|
9119
|
+
// Listen on all scroll containers (including document) since the element
|
|
9120
|
+
// uses position:fixed and any ancestor scroll changes its apparent position.
|
|
9121
|
+
const listenTargets = new Set(scrollContainerSet);
|
|
9122
|
+
listenTargets.add(document.documentElement);
|
|
9123
|
+
for (const scrollTarget of listenTargets) {
|
|
9124
|
+
scrollTarget.addEventListener("scroll", handleScroll, { passive: true });
|
|
9045
9125
|
cleanupCallbackSet.add(() => {
|
|
9046
|
-
|
|
9126
|
+
scrollTarget.removeEventListener("scroll", handleScroll, {
|
|
9047
9127
|
passive: true,
|
|
9048
9128
|
});
|
|
9049
9129
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsenv/dom",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
4
4
|
"description": "DOM utilities for writing frontend code",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"@jsenv/core": "../../../",
|
|
43
43
|
"@jsenv/navi": "../navi",
|
|
44
44
|
"@jsenv/snapshot": "../../tooling/snapshot",
|
|
45
|
-
"@preact/signals": "2.
|
|
45
|
+
"@preact/signals": "2.9.0",
|
|
46
46
|
"preact": "11.0.0-beta.0"
|
|
47
47
|
},
|
|
48
48
|
"publishConfig": {
|