@zag-js/scroll-snap
Version:
Scroll snap utilities
235 lines (233 loc) • 9.41 kB
JavaScript
;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
findSnapPoint: () => findSnapPoint,
getScrollSnapPositions: () => getScrollSnapPositions,
getSnapPointTarget: () => getSnapPointTarget,
getSnapPositions: () => getSnapPositions
});
module.exports = __toCommonJS(index_exports);
var import_dom_query = require("@zag-js/dom-query");
var getDirection = (element) => (0, import_dom_query.getComputedStyle)(element).direction;
var convert = (raw, size) => {
let n = parseFloat(raw);
if (/%/.test(raw)) {
n /= 100;
n *= size;
}
return Number.isNaN(n) ? 0 : n;
};
function getScrollPadding(element) {
const style = (0, import_dom_query.getComputedStyle)(element);
const layoutWidth = element.offsetWidth;
const layoutHeight = element.offsetHeight;
let xBeforeRaw = style.getPropertyValue("scroll-padding-left").replace("auto", "0px");
let yBeforeRaw = style.getPropertyValue("scroll-padding-top").replace("auto", "0px");
let xAfterRaw = style.getPropertyValue("scroll-padding-right").replace("auto", "0px");
let yAfterRaw = style.getPropertyValue("scroll-padding-bottom").replace("auto", "0px");
let xBefore = convert(xBeforeRaw, layoutWidth);
let yBefore = convert(yBeforeRaw, layoutHeight);
let xAfter = convert(xAfterRaw, layoutWidth);
let yAfter = convert(yAfterRaw, layoutHeight);
return {
x: { before: xBefore, after: xAfter },
y: { before: yBefore, after: yAfter }
};
}
function isRectIntersecting(a, b, axis = "both") {
return axis === "x" && a.right >= b.left && a.left <= b.right || axis === "y" && a.bottom >= b.top && a.top <= b.bottom || axis === "both" && a.right >= b.left && a.left <= b.right && a.bottom >= b.top && a.top <= b.bottom;
}
function getDescendants(parent) {
let children = [];
for (const child of parent.children) {
children = children.concat(child, getDescendants(child));
}
return children;
}
function getSnapPositions(parent, subtree = false) {
const parentRect = parent.getBoundingClientRect();
const dir = getDirection(parent);
const isRtl = dir === "rtl";
const scale = (0, import_dom_query.getScale)(parent);
const positions = {
x: { start: [], center: [], end: [] },
y: { start: [], center: [], end: [] }
};
const children = subtree ? getDescendants(parent) : parent.children;
for (const axis of ["x", "y"]) {
const orthogonalAxis = axis === "x" ? "y" : "x";
const axisStart = axis === "x" ? "left" : "top";
const axisEnd = axis === "x" ? "right" : "bottom";
const axisSize = axis === "x" ? "width" : "height";
const axisScroll = axis === "x" ? "scrollLeft" : "scrollTop";
const axisScale = axis === "x" ? scale.x : scale.y;
const useRtlCalc = isRtl && axis === "x";
for (const child of children) {
const childRect = child.getBoundingClientRect();
if (!isRectIntersecting(parentRect, childRect, orthogonalAxis)) {
continue;
}
const childStyle = (0, import_dom_query.getComputedStyle)(child);
let [childAlignY, childAlignX] = childStyle.getPropertyValue("scroll-snap-align").split(" ");
if (typeof childAlignX === "undefined") {
childAlignX = childAlignY;
}
const childAlign = axis === "x" ? childAlignX : childAlignY;
let childOffsetStart;
let childOffsetEnd;
let childOffsetCenter;
if (useRtlCalc) {
const scrollOffset = Math.abs(parent[axisScroll]);
const rightOffset = (parentRect[axisEnd] - childRect[axisEnd]) / axisScale + scrollOffset;
childOffsetStart = rightOffset;
childOffsetEnd = rightOffset + childRect[axisSize] / axisScale;
childOffsetCenter = rightOffset + childRect[axisSize] / (2 * axisScale);
} else {
childOffsetStart = (childRect[axisStart] - parentRect[axisStart]) / axisScale + parent[axisScroll];
childOffsetEnd = childOffsetStart + childRect[axisSize] / axisScale;
childOffsetCenter = childOffsetStart + childRect[axisSize] / (2 * axisScale);
}
switch (childAlign) {
case "none":
break;
case "start":
positions[axis].start.push({ node: child, position: childOffsetStart });
break;
case "center":
positions[axis].center.push({ node: child, position: childOffsetCenter });
break;
case "end":
positions[axis].end.push({ node: child, position: childOffsetEnd });
break;
}
}
}
return positions;
}
function getScrollSnapPositions(element) {
const dir = getDirection(element);
const scrollPadding = getScrollPadding(element);
const snapPositions = getSnapPositions(element);
const layoutWidth = element.offsetWidth;
const layoutHeight = element.offsetHeight;
const maxScroll = {
x: element.scrollWidth - element.offsetWidth,
y: element.scrollHeight - element.offsetHeight
};
const isRtl = dir === "rtl";
const usesNegativeScrollLeft = isRtl && element.scrollLeft <= 0;
let xPositions;
if (isRtl) {
xPositions = uniq(
[
...snapPositions.x.start.map((v) => v.position - scrollPadding.x.after),
...snapPositions.x.center.map((v) => v.position - layoutWidth / 2),
...snapPositions.x.end.map((v) => v.position - layoutWidth + scrollPadding.x.before)
].map(clamp(0, maxScroll.x))
);
if (usesNegativeScrollLeft) {
xPositions = xPositions.map((pos) => -pos);
}
} else {
xPositions = uniq(
[
...snapPositions.x.start.map((v) => v.position - scrollPadding.x.before),
...snapPositions.x.center.map((v) => v.position - layoutWidth / 2),
...snapPositions.x.end.map((v) => v.position - layoutWidth + scrollPadding.x.after)
].map(clamp(0, maxScroll.x))
);
}
return {
x: xPositions,
y: uniq(
[
...snapPositions.y.start.map((v) => v.position - scrollPadding.y.before),
...snapPositions.y.center.map((v) => v.position - layoutHeight / 2),
...snapPositions.y.end.map((v) => v.position - layoutHeight + scrollPadding.y.after)
].map(clamp(0, maxScroll.y))
)
};
}
function findSnapPoint(parent, axis, predicate) {
const dir = getDirection(parent);
const scrollPadding = getScrollPadding(parent);
const snapPositions = getSnapPositions(parent);
const items = [...snapPositions[axis].start, ...snapPositions[axis].center, ...snapPositions[axis].end];
const isRtl = dir === "rtl";
const usesNegativeScrollLeft = isRtl && axis === "x" && parent.scrollLeft <= 0;
for (const item of items) {
if (predicate(item.node)) {
let position;
if (axis === "x" && isRtl) {
position = item.position - scrollPadding.x.after;
if (usesNegativeScrollLeft) {
position = -position;
}
} else {
position = item.position - (axis === "x" ? scrollPadding.x.before : scrollPadding.y.before);
}
return position;
}
}
}
function getSnapPointTarget(parent, snapPoint) {
const rect = parent.getBoundingClientRect();
const scale = (0, import_dom_query.getScale)(parent);
const scrollPadding = getScrollPadding(parent);
const children = Array.from(parent.children);
const layoutWidth = parent.offsetWidth;
const layoutHeight = parent.offsetHeight;
for (const child of children) {
const childRect = child.getBoundingClientRect();
const childOffsetStart = {
x: (childRect.left - rect.left) / scale.x + parent.scrollLeft,
y: (childRect.top - rect.top) / scale.y + parent.scrollTop
};
const childLayoutWidth = childRect.width / scale.x;
const childLayoutHeight = childRect.height / scale.y;
const matchesX = [
childOffsetStart.x - scrollPadding.x.before,
// start
childOffsetStart.x + childLayoutWidth / 2 - layoutWidth / 2,
// center
childOffsetStart.x + childLayoutWidth - layoutWidth + scrollPadding.x.after
// end
].some((pos) => Math.abs(pos - snapPoint) < 1);
const matchesY = [
childOffsetStart.y - scrollPadding.y.before,
childOffsetStart.y + childLayoutHeight / 2 - layoutHeight / 2,
childOffsetStart.y + childLayoutHeight - layoutHeight + scrollPadding.y.after
].some((pos) => Math.abs(pos - snapPoint) < 1);
if (matchesX || matchesY) {
return child;
}
}
return children[0];
}
var uniq = (arr) => [...new Set(arr)];
var clamp = (min, max) => (value) => Math.max(min, Math.min(max, value));
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
findSnapPoint,
getScrollSnapPositions,
getSnapPointTarget,
getSnapPositions
});