This commit is contained in:
+33
@@ -0,0 +1,33 @@
|
||||
<script lang="ts">
|
||||
import { boxWith, mergeProps } from "svelte-toolbelt";
|
||||
import type { SliderRangeProps } from "../types.js";
|
||||
import { SliderRangeState } from "../slider.svelte.js";
|
||||
import { createId } from "../../../internal/create-id.js";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
children,
|
||||
child,
|
||||
ref = $bindable(null),
|
||||
id = createId(uid),
|
||||
...restProps
|
||||
}: SliderRangeProps = $props();
|
||||
|
||||
const rangeState = SliderRangeState.create({
|
||||
id: boxWith(() => id),
|
||||
ref: boxWith(
|
||||
() => ref,
|
||||
(v) => (ref = v)
|
||||
),
|
||||
});
|
||||
const mergedProps = $derived(mergeProps(restProps, rangeState.props));
|
||||
</script>
|
||||
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps })}
|
||||
{:else}
|
||||
<span {...mergedProps}>
|
||||
{@render children?.()}
|
||||
</span>
|
||||
{/if}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import type { SliderRangeProps } from "../types.js";
|
||||
declare const SliderRange: import("svelte").Component<SliderRangeProps, {}, "ref">;
|
||||
type SliderRange = ReturnType<typeof SliderRange>;
|
||||
export default SliderRange;
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
<script lang="ts">
|
||||
import { boxWith, mergeProps } from "svelte-toolbelt";
|
||||
import type { SliderThumbLabelProps } from "../types.js";
|
||||
import { SliderRootContext, SliderThumbLabelState } from "../slider.svelte.js";
|
||||
import { createId } from "../../../internal/create-id.js";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
children,
|
||||
child,
|
||||
ref = $bindable(null),
|
||||
id = createId(uid),
|
||||
index,
|
||||
position: positionProp,
|
||||
...restProps
|
||||
}: SliderThumbLabelProps = $props();
|
||||
|
||||
const root = SliderRootContext.get();
|
||||
|
||||
const position = $derived.by(() => {
|
||||
if (positionProp !== undefined) return positionProp;
|
||||
switch (root.direction) {
|
||||
case "lr":
|
||||
case "rl":
|
||||
return "top";
|
||||
case "tb":
|
||||
case "bt":
|
||||
return "left";
|
||||
}
|
||||
});
|
||||
|
||||
const tickLabelState = SliderThumbLabelState.create({
|
||||
id: boxWith(() => id),
|
||||
ref: boxWith(
|
||||
() => ref,
|
||||
(v) => (ref = v)
|
||||
),
|
||||
index: boxWith(() => index),
|
||||
position: boxWith(() => position),
|
||||
});
|
||||
|
||||
const mergedProps = $derived(mergeProps(restProps, tickLabelState.props));
|
||||
</script>
|
||||
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps })}
|
||||
{:else}
|
||||
<span {...mergedProps}>{@render children?.()}</span>
|
||||
{/if}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import type { SliderThumbLabelProps } from "../types.js";
|
||||
declare const SliderThumbLabel: import("svelte").Component<SliderThumbLabelProps, {}, "ref">;
|
||||
type SliderThumbLabel = ReturnType<typeof SliderThumbLabel>;
|
||||
export default SliderThumbLabel;
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
<script lang="ts">
|
||||
import { boxWith, mergeProps } from "svelte-toolbelt";
|
||||
import type { SliderThumbProps } from "../types.js";
|
||||
import { SliderThumbState } from "../slider.svelte.js";
|
||||
import { createId } from "../../../internal/create-id.js";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
children,
|
||||
child,
|
||||
ref = $bindable(null),
|
||||
id = createId(uid),
|
||||
index,
|
||||
disabled = false,
|
||||
...restProps
|
||||
}: SliderThumbProps = $props();
|
||||
|
||||
const thumbState = SliderThumbState.create({
|
||||
id: boxWith(() => id),
|
||||
ref: boxWith(
|
||||
() => ref,
|
||||
(v) => (ref = v)
|
||||
),
|
||||
index: boxWith(() => index),
|
||||
disabled: boxWith(() => disabled),
|
||||
});
|
||||
|
||||
const mergedProps = $derived(mergeProps(restProps, thumbState.props));
|
||||
</script>
|
||||
|
||||
{#if child}
|
||||
{@render child({
|
||||
active: thumbState.root.isThumbActive(thumbState.opts.index.current),
|
||||
props: mergedProps,
|
||||
})}
|
||||
{:else}
|
||||
<span {...mergedProps}>
|
||||
{@render children?.({
|
||||
active: thumbState.root.isThumbActive(thumbState.opts.index.current),
|
||||
})}
|
||||
</span>
|
||||
{/if}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import type { SliderThumbProps } from "../types.js";
|
||||
declare const SliderThumb: import("svelte").Component<SliderThumbProps, {}, "ref">;
|
||||
type SliderThumb = ReturnType<typeof SliderThumb>;
|
||||
export default SliderThumb;
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
<script lang="ts">
|
||||
import { boxWith, mergeProps } from "svelte-toolbelt";
|
||||
import type { SliderTickLabelProps } from "../types.js";
|
||||
import { SliderRootContext, SliderTickLabelState } from "../slider.svelte.js";
|
||||
import { createId } from "../../../internal/create-id.js";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
children,
|
||||
child,
|
||||
ref = $bindable(null),
|
||||
id = createId(uid),
|
||||
index,
|
||||
position: positionProp,
|
||||
...restProps
|
||||
}: SliderTickLabelProps = $props();
|
||||
|
||||
const root = SliderRootContext.get();
|
||||
|
||||
const position = $derived.by(() => {
|
||||
if (positionProp !== undefined) return positionProp;
|
||||
switch (root.direction) {
|
||||
case "lr":
|
||||
case "rl":
|
||||
return "top";
|
||||
case "tb":
|
||||
case "bt":
|
||||
return "left";
|
||||
}
|
||||
});
|
||||
|
||||
const tickLabelState = SliderTickLabelState.create({
|
||||
id: boxWith(() => id),
|
||||
ref: boxWith(
|
||||
() => ref,
|
||||
(v) => (ref = v)
|
||||
),
|
||||
index: boxWith(() => index),
|
||||
position: boxWith(() => position),
|
||||
});
|
||||
|
||||
const mergedProps = $derived(mergeProps(restProps, tickLabelState.props));
|
||||
</script>
|
||||
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps })}
|
||||
{:else}
|
||||
<span {...mergedProps}>{@render children?.()}</span>
|
||||
{/if}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import type { SliderTickLabelProps } from "../types.js";
|
||||
declare const SliderTickLabel: import("svelte").Component<SliderTickLabelProps, {}, "ref">;
|
||||
type SliderTickLabel = ReturnType<typeof SliderTickLabel>;
|
||||
export default SliderTickLabel;
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
import { boxWith, mergeProps } from "svelte-toolbelt";
|
||||
import type { SliderTickProps } from "../types.js";
|
||||
import { SliderTickState } from "../slider.svelte.js";
|
||||
import { createId } from "../../../internal/create-id.js";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
children,
|
||||
child,
|
||||
ref = $bindable(null),
|
||||
id = createId(uid),
|
||||
index,
|
||||
...restProps
|
||||
}: SliderTickProps = $props();
|
||||
|
||||
const tickState = SliderTickState.create({
|
||||
id: boxWith(() => id),
|
||||
ref: boxWith(
|
||||
() => ref,
|
||||
(v) => (ref = v)
|
||||
),
|
||||
index: boxWith(() => index),
|
||||
});
|
||||
|
||||
const mergedProps = $derived(mergeProps(restProps, tickState.props));
|
||||
</script>
|
||||
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps })}
|
||||
{:else}
|
||||
<span {...mergedProps}>{@render children?.()}</span>
|
||||
{/if}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import type { SliderTickProps } from "../types.js";
|
||||
declare const SliderTick: import("svelte").Component<SliderTickProps, {}, "ref">;
|
||||
type SliderTick = ReturnType<typeof SliderTick>;
|
||||
export default SliderTick;
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
<script lang="ts">
|
||||
import { boxWith, mergeProps, type WritableBox } from "svelte-toolbelt";
|
||||
import type { SliderRootProps } from "../types.js";
|
||||
import { SliderRootState } from "../slider.svelte.js";
|
||||
import { createId } from "../../../internal/create-id.js";
|
||||
import { noop } from "../../../internal/noop.js";
|
||||
import { watch } from "runed";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
children,
|
||||
child,
|
||||
id = createId(uid),
|
||||
ref = $bindable(null),
|
||||
value = $bindable(),
|
||||
type,
|
||||
onValueChange = noop,
|
||||
onValueCommit = noop,
|
||||
disabled = false,
|
||||
min: minProp,
|
||||
max: maxProp,
|
||||
step = 1,
|
||||
dir = "ltr",
|
||||
autoSort = true,
|
||||
orientation = "horizontal",
|
||||
thumbPositioning = "contain",
|
||||
trackPadding,
|
||||
...restProps
|
||||
}: SliderRootProps = $props();
|
||||
|
||||
const min = $derived.by(() => {
|
||||
if (minProp !== undefined) return minProp;
|
||||
if (Array.isArray(step)) return Math.min(...step);
|
||||
return 0;
|
||||
});
|
||||
|
||||
const max = $derived.by(() => {
|
||||
if (maxProp !== undefined) return maxProp;
|
||||
if (Array.isArray(step)) return Math.max(...step);
|
||||
return 100;
|
||||
});
|
||||
|
||||
function handleDefaultValue() {
|
||||
if (value !== undefined) return;
|
||||
if (type === "single") {
|
||||
return min;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// SSR
|
||||
handleDefaultValue();
|
||||
|
||||
watch.pre(
|
||||
() => value,
|
||||
() => {
|
||||
handleDefaultValue();
|
||||
}
|
||||
);
|
||||
|
||||
const rootState = SliderRootState.create({
|
||||
id: boxWith(() => id),
|
||||
ref: boxWith(
|
||||
() => ref,
|
||||
(v) => (ref = v)
|
||||
),
|
||||
value: boxWith(
|
||||
() => value,
|
||||
(v) => {
|
||||
value = v;
|
||||
// @ts-expect-error - we know
|
||||
onValueChange(v);
|
||||
}
|
||||
) as WritableBox<number> | WritableBox<number[]>,
|
||||
// @ts-expect-error - we know
|
||||
onValueCommit: boxWith(() => onValueCommit),
|
||||
disabled: boxWith(() => disabled),
|
||||
min: boxWith(() => min),
|
||||
max: boxWith(() => max),
|
||||
step: boxWith(() => step),
|
||||
dir: boxWith(() => dir),
|
||||
autoSort: boxWith(() => autoSort),
|
||||
orientation: boxWith(() => orientation),
|
||||
thumbPositioning: boxWith(() => thumbPositioning),
|
||||
type,
|
||||
trackPadding: boxWith(() => trackPadding),
|
||||
});
|
||||
|
||||
const mergedProps = $derived(mergeProps(restProps, rootState.props));
|
||||
</script>
|
||||
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps, ...rootState.snippetProps })}
|
||||
{:else}
|
||||
<span {...mergedProps}>
|
||||
{@render children?.(rootState.snippetProps)}
|
||||
</span>
|
||||
{/if}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import type { SliderRootProps } from "../types.js";
|
||||
declare const Slider: import("svelte").Component<SliderRootProps, {}, "value" | "ref">;
|
||||
type Slider = ReturnType<typeof Slider>;
|
||||
export default Slider;
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
export { default as Root } from "./components/slider.svelte";
|
||||
export { default as Range } from "./components/slider-range.svelte";
|
||||
export { default as Thumb } from "./components/slider-thumb.svelte";
|
||||
export { default as Tick } from "./components/slider-tick.svelte";
|
||||
export { default as TickLabel } from "./components/slider-tick-label.svelte";
|
||||
export { default as ThumbLabel } from "./components/slider-thumb-label.svelte";
|
||||
export type { SliderRootProps as RootProps, SliderRangeProps as RangeProps, SliderThumbProps as ThumbProps, SliderTickProps as TickProps, SliderTickLabelProps as TickLabelProps, SliderThumbLabelProps as ThumbLabelProps, } from "./types.js";
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
export { default as Root } from "./components/slider.svelte";
|
||||
export { default as Range } from "./components/slider-range.svelte";
|
||||
export { default as Thumb } from "./components/slider-thumb.svelte";
|
||||
export { default as Tick } from "./components/slider-tick.svelte";
|
||||
export { default as TickLabel } from "./components/slider-tick-label.svelte";
|
||||
export { default as ThumbLabel } from "./components/slider-thumb-label.svelte";
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import type { StyleProperties } from "../../shared/index.js";
|
||||
export declare function getRangeStyles(direction: "lr" | "rl" | "tb" | "bt", min: number, max: number): StyleProperties;
|
||||
export declare function getThumbStyles(direction: "lr" | "rl" | "tb" | "bt", thumbPos: number): StyleProperties;
|
||||
export declare function getTickStyles(direction: "lr" | "rl" | "tb" | "bt", tickPosition: number, offsetPercentage: number): StyleProperties;
|
||||
export declare function getTickLabelStyles(direction: "lr" | "rl" | "tb" | "bt", tickPosition: number, labelPosition?: "top" | "bottom" | "left" | "right"): StyleProperties;
|
||||
export declare function getThumbLabelStyles(direction: "lr" | "rl" | "tb" | "bt", thumbPosition: number, labelPosition?: "top" | "bottom" | "left" | "right"): StyleProperties;
|
||||
/**
|
||||
* Normalizes step to always be a sorted array of valid values within min/max range
|
||||
*/
|
||||
export declare function normalizeSteps(step: number | number[], min: number, max: number): number[];
|
||||
/**
|
||||
* Snaps a value to the nearest step in a custom steps array
|
||||
*/
|
||||
export declare function snapValueToCustomSteps(value: number, steps: number[]): number;
|
||||
/**
|
||||
* Gets the next/previous step value for keyboard navigation
|
||||
*/
|
||||
export declare function getAdjacentStepValue(currentValue: number, steps: number[], direction: "next" | "prev"): number;
|
||||
+219
@@ -0,0 +1,219 @@
|
||||
export function getRangeStyles(direction, min, max) {
|
||||
const styles = {
|
||||
position: "absolute",
|
||||
};
|
||||
if (direction === "lr") {
|
||||
styles.left = `${min}%`;
|
||||
styles.right = `${max}%`;
|
||||
}
|
||||
else if (direction === "rl") {
|
||||
styles.right = `${min}%`;
|
||||
styles.left = `${max}%`;
|
||||
}
|
||||
else if (direction === "bt") {
|
||||
styles.bottom = `${min}%`;
|
||||
styles.top = `${max}%`;
|
||||
}
|
||||
else {
|
||||
styles.top = `${min}%`;
|
||||
styles.bottom = `${max}%`;
|
||||
}
|
||||
return styles;
|
||||
}
|
||||
export function getThumbStyles(direction, thumbPos) {
|
||||
const styles = {
|
||||
position: "absolute",
|
||||
};
|
||||
if (direction === "lr") {
|
||||
styles.left = `${thumbPos}%`;
|
||||
styles.translate = "-50% 0";
|
||||
}
|
||||
else if (direction === "rl") {
|
||||
styles.right = `${thumbPos}%`;
|
||||
styles.translate = "50% 0";
|
||||
}
|
||||
else if (direction === "bt") {
|
||||
styles.bottom = `${thumbPos}%`;
|
||||
styles.translate = "0 50%";
|
||||
}
|
||||
else {
|
||||
styles.top = `${thumbPos}%`;
|
||||
styles.translate = "0 -50%";
|
||||
}
|
||||
return styles;
|
||||
}
|
||||
export function getTickStyles(direction, tickPosition, offsetPercentage) {
|
||||
const style = {
|
||||
position: "absolute",
|
||||
};
|
||||
if (direction === "lr") {
|
||||
style.left = `${tickPosition}%`;
|
||||
style.translate = `${offsetPercentage}% 0`;
|
||||
}
|
||||
else if (direction === "rl") {
|
||||
style.right = `${tickPosition}%`;
|
||||
style.translate = `${-offsetPercentage}% 0`;
|
||||
}
|
||||
else if (direction === "bt") {
|
||||
style.bottom = `${tickPosition}%`;
|
||||
style.translate = `0 ${-offsetPercentage}%`;
|
||||
}
|
||||
else {
|
||||
style.top = `${tickPosition}%`;
|
||||
style.translate = `0 ${offsetPercentage}%`;
|
||||
}
|
||||
return style;
|
||||
}
|
||||
export function getTickLabelStyles(direction, tickPosition, labelPosition = "top") {
|
||||
const style = {
|
||||
position: "absolute",
|
||||
};
|
||||
if (direction === "lr" || direction === "rl") {
|
||||
// Horizontal slider
|
||||
style.left = direction === "lr" ? `${tickPosition}%` : undefined;
|
||||
style.right = direction === "rl" ? `${tickPosition}%` : undefined;
|
||||
style.translate = "-50% 0";
|
||||
if (labelPosition === "top") {
|
||||
style.bottom = "100%";
|
||||
}
|
||||
else if (labelPosition === "bottom") {
|
||||
style.top = "100%";
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Vertical slider - use same positioning as ticks
|
||||
if (direction === "tb") {
|
||||
style.top = `${tickPosition}%`;
|
||||
}
|
||||
else {
|
||||
style.bottom = `${tickPosition}%`;
|
||||
}
|
||||
style.translate = "0 50%";
|
||||
if (labelPosition === "left") {
|
||||
style.right = "100%";
|
||||
}
|
||||
else if (labelPosition === "right") {
|
||||
style.left = "100%";
|
||||
}
|
||||
}
|
||||
return style;
|
||||
}
|
||||
export function getThumbLabelStyles(direction, thumbPosition, labelPosition = "top") {
|
||||
const style = {
|
||||
position: "absolute",
|
||||
};
|
||||
if (direction === "lr" || direction === "rl") {
|
||||
// Horizontal slider
|
||||
style.left = direction === "lr" ? `${thumbPosition}%` : undefined;
|
||||
style.right = direction === "rl" ? `${thumbPosition}%` : undefined;
|
||||
style.translate = "-50% 0";
|
||||
if (labelPosition === "top") {
|
||||
style.bottom = "100%";
|
||||
}
|
||||
else if (labelPosition === "bottom") {
|
||||
style.top = "100%";
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Vertical slider
|
||||
if (direction === "tb") {
|
||||
style.top = `${thumbPosition}%`;
|
||||
}
|
||||
else {
|
||||
style.bottom = `${thumbPosition}%`;
|
||||
}
|
||||
style.translate = "0 -50%";
|
||||
if (labelPosition === "left") {
|
||||
style.right = "100%";
|
||||
}
|
||||
else if (labelPosition === "right") {
|
||||
style.left = "100%";
|
||||
}
|
||||
}
|
||||
return style;
|
||||
}
|
||||
/**
|
||||
* Gets the number of decimal places in a number
|
||||
*/
|
||||
function getDecimalPlaces(num) {
|
||||
if (Math.floor(num) === num)
|
||||
return 0;
|
||||
const str = num.toString();
|
||||
if (str.indexOf(".") !== -1 && str.indexOf("e-") === -1) {
|
||||
return str.split(".")[1].length;
|
||||
}
|
||||
else if (str.indexOf("e-") !== -1) {
|
||||
const parts = str.split("e-");
|
||||
return parseInt(parts[1], 10);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
/**
|
||||
* Rounds a number to the specified number of decimal places
|
||||
*/
|
||||
function roundToPrecision(num, precision) {
|
||||
const factor = Math.pow(10, precision);
|
||||
return Math.round(num * factor) / factor;
|
||||
}
|
||||
/**
|
||||
* Normalizes step to always be a sorted array of valid values within min/max range
|
||||
*/
|
||||
export function normalizeSteps(step, min, max) {
|
||||
if (typeof step === "number") {
|
||||
// generate regular steps - match original behavior exactly
|
||||
const difference = max - min;
|
||||
let count = Math.ceil(difference / step);
|
||||
// Get precision from step to avoid floating point errors
|
||||
const precision = getDecimalPlaces(step);
|
||||
// Check if difference is divisible by step using integer arithmetic to avoid floating point errors
|
||||
const factor = Math.pow(10, precision);
|
||||
const intDifference = Math.round(difference * factor);
|
||||
const intStep = Math.round(step * factor);
|
||||
if (intDifference % intStep === 0) {
|
||||
count++;
|
||||
}
|
||||
const steps = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
const value = min + i * step;
|
||||
// Round to the precision of the step to avoid floating point errors
|
||||
const roundedValue = roundToPrecision(value, precision);
|
||||
steps.push(roundedValue);
|
||||
}
|
||||
return steps;
|
||||
}
|
||||
return [...new Set(step)].filter((value) => value >= min && value <= max).sort((a, b) => a - b);
|
||||
}
|
||||
/**
|
||||
* Snaps a value to the nearest step in a custom steps array
|
||||
*/
|
||||
export function snapValueToCustomSteps(value, steps) {
|
||||
if (steps.length === 0)
|
||||
return value;
|
||||
// Find the closest step
|
||||
let closest = steps[0];
|
||||
let minDistance = Math.abs(value - closest);
|
||||
for (const step of steps) {
|
||||
const distance = Math.abs(value - step);
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
closest = step;
|
||||
}
|
||||
}
|
||||
return closest;
|
||||
}
|
||||
/**
|
||||
* Gets the next/previous step value for keyboard navigation
|
||||
*/
|
||||
export function getAdjacentStepValue(currentValue, steps, direction) {
|
||||
const currentIndex = steps.indexOf(currentValue);
|
||||
if (currentIndex === -1) {
|
||||
// current value is not in steps, snap to nearest
|
||||
return snapValueToCustomSteps(currentValue, steps);
|
||||
}
|
||||
if (direction === "next") {
|
||||
return currentIndex < steps.length - 1 ? steps[currentIndex + 1] : currentValue;
|
||||
}
|
||||
else {
|
||||
return currentIndex > 0 ? steps[currentIndex - 1] : currentValue;
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * as Slider from "./exports.js";
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * as Slider from "./exports.js";
|
||||
+1970
File diff suppressed because it is too large
Load Diff
+810
@@ -0,0 +1,810 @@
|
||||
/**
|
||||
* This logic is adapted from the @melt-ui/svelte slider, which was mostly written by
|
||||
* Abdelrahman (https://github.com/abdel-17)
|
||||
*/
|
||||
import { untrack } from "svelte";
|
||||
import { executeCallbacks, onMountEffect, attachRef, DOMContext, } from "svelte-toolbelt";
|
||||
import { on } from "svelte/events";
|
||||
import { Context, watch } from "runed";
|
||||
import { getRangeStyles, getThumbStyles, getTickStyles, normalizeSteps, snapValueToCustomSteps, getAdjacentStepValue, getTickLabelStyles, getThumbLabelStyles, } from "./helpers.js";
|
||||
import { createBitsAttrs, boolToStr, boolToEmptyStrOrUndef } from "../../internal/attrs.js";
|
||||
import { kbd } from "../../internal/kbd.js";
|
||||
import { isElementOrSVGElement } from "../../internal/is.js";
|
||||
import { isValidIndex } from "../../internal/arrays.js";
|
||||
import { linearScale } from "../../internal/math.js";
|
||||
const sliderAttrs = createBitsAttrs({
|
||||
component: "slider",
|
||||
parts: ["root", "thumb", "range", "tick", "tick-label", "thumb-label"],
|
||||
});
|
||||
export const SliderRootContext = new Context("Slider.Root");
|
||||
class SliderBaseRootState {
|
||||
opts;
|
||||
attachment;
|
||||
isActive = $state(false);
|
||||
direction = $derived.by(() => {
|
||||
if (this.opts.orientation.current === "horizontal") {
|
||||
return this.opts.dir.current === "rtl" ? "rl" : "lr";
|
||||
}
|
||||
else {
|
||||
return this.opts.dir.current === "rtl" ? "tb" : "bt";
|
||||
}
|
||||
});
|
||||
// Normalized steps array for consistent handling
|
||||
normalizedSteps = $derived.by(() => {
|
||||
return normalizeSteps(this.opts.step.current, this.opts.min.current, this.opts.max.current);
|
||||
});
|
||||
domContext;
|
||||
constructor(opts) {
|
||||
this.opts = opts;
|
||||
this.attachment = attachRef(opts.ref);
|
||||
this.domContext = new DOMContext(this.opts.ref);
|
||||
}
|
||||
isThumbActive(_index) {
|
||||
return this.isActive;
|
||||
}
|
||||
#touchAction = $derived.by(() => {
|
||||
if (this.opts.disabled.current)
|
||||
return undefined;
|
||||
return this.opts.orientation.current === "horizontal" ? "pan-y" : "pan-x";
|
||||
});
|
||||
getAllThumbs = () => {
|
||||
const node = this.opts.ref.current;
|
||||
if (!node)
|
||||
return [];
|
||||
return Array.from(node.querySelectorAll(sliderAttrs.selector("thumb")));
|
||||
};
|
||||
getThumbScale = () => {
|
||||
// If trackPadding is explicitly set, use it directly instead of calculating from thumb size
|
||||
const trackPadding = this.opts.trackPadding?.current;
|
||||
if (trackPadding !== undefined && trackPadding > 0) {
|
||||
return [trackPadding, 100 - trackPadding];
|
||||
}
|
||||
if (this.opts.thumbPositioning.current === "exact") {
|
||||
// User opted out of containment
|
||||
return [0, 100];
|
||||
}
|
||||
const isVertical = this.opts.orientation.current === "vertical";
|
||||
// this assumes all thumbs are the same width
|
||||
const activeThumb = this.getAllThumbs()[0];
|
||||
const thumbSize = isVertical ? activeThumb?.offsetHeight : activeThumb?.offsetWidth;
|
||||
// if thumb size is undefined or 0, fallback to a 0-100 scale
|
||||
if (thumbSize === undefined || Number.isNaN(thumbSize) || thumbSize === 0)
|
||||
return [0, 100];
|
||||
const trackSize = isVertical
|
||||
? this.opts.ref.current?.offsetHeight
|
||||
: this.opts.ref.current?.offsetWidth;
|
||||
// if track size is undefined or 0, fallback to a 0-100 scale
|
||||
if (trackSize === undefined || Number.isNaN(trackSize) || trackSize === 0)
|
||||
return [0, 100];
|
||||
// the padding on either side
|
||||
// half the width of the thumb
|
||||
const percentPadding = (thumbSize / 2 / trackSize) * 100;
|
||||
const min = percentPadding;
|
||||
const max = 100 - percentPadding;
|
||||
return [min, max];
|
||||
};
|
||||
getPositionFromValue = (thumbValue) => {
|
||||
const thumbScale = this.getThumbScale();
|
||||
const scale = linearScale([this.opts.min.current, this.opts.max.current], thumbScale);
|
||||
return scale(thumbValue);
|
||||
};
|
||||
props = $derived.by(() => ({
|
||||
id: this.opts.id.current,
|
||||
"data-orientation": this.opts.orientation.current,
|
||||
"data-disabled": boolToEmptyStrOrUndef(this.opts.disabled.current),
|
||||
style: {
|
||||
touchAction: this.#touchAction,
|
||||
},
|
||||
[sliderAttrs.root]: "",
|
||||
...this.attachment,
|
||||
}));
|
||||
}
|
||||
class SliderSingleRootState extends SliderBaseRootState {
|
||||
opts;
|
||||
isMulti = false;
|
||||
constructor(opts) {
|
||||
super(opts);
|
||||
this.opts = opts;
|
||||
onMountEffect(() => {
|
||||
return executeCallbacks(on(this.domContext.getDocument(), "pointerdown", this.handlePointerDown), on(this.domContext.getDocument(), "pointerup", this.handlePointerUp), on(this.domContext.getDocument(), "pointermove", this.handlePointerMove), on(this.domContext.getDocument(), "pointerleave", this.handlePointerUp));
|
||||
});
|
||||
watch([
|
||||
() => this.opts.step.current,
|
||||
() => this.opts.min.current,
|
||||
() => this.opts.max.current,
|
||||
() => this.opts.value.current,
|
||||
], ([step, min, max, value]) => {
|
||||
const steps = normalizeSteps(step, min, max);
|
||||
const isValidValue = (v) => {
|
||||
return steps.includes(v);
|
||||
};
|
||||
const gcv = (v) => {
|
||||
return snapValueToCustomSteps(v, steps);
|
||||
};
|
||||
if (!isValidValue(value)) {
|
||||
this.opts.value.current = gcv(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
isTickValueSelected = (tickValue) => {
|
||||
return this.opts.value.current === tickValue;
|
||||
};
|
||||
applyPosition({ clientXY, start, end }) {
|
||||
const min = this.opts.min.current;
|
||||
const max = this.opts.max.current;
|
||||
const percent = (clientXY - start) / (end - start);
|
||||
const val = percent * (max - min) + min;
|
||||
if (val < min) {
|
||||
this.updateValue(min);
|
||||
}
|
||||
else if (val > max) {
|
||||
this.updateValue(max);
|
||||
}
|
||||
else {
|
||||
const steps = this.normalizedSteps;
|
||||
const newValue = snapValueToCustomSteps(val, steps);
|
||||
this.updateValue(newValue);
|
||||
}
|
||||
}
|
||||
updateValue = (newValue) => {
|
||||
this.opts.value.current = snapValueToCustomSteps(newValue, this.normalizedSteps);
|
||||
};
|
||||
handlePointerMove = (e) => {
|
||||
if (!this.isActive || this.opts.disabled.current)
|
||||
return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const sliderNode = this.opts.ref.current;
|
||||
const activeThumb = this.getAllThumbs()[0];
|
||||
if (!sliderNode || !activeThumb)
|
||||
return;
|
||||
activeThumb.focus();
|
||||
const { left, right, top, bottom } = sliderNode.getBoundingClientRect();
|
||||
if (this.direction === "lr") {
|
||||
this.applyPosition({
|
||||
clientXY: e.clientX,
|
||||
start: left,
|
||||
end: right,
|
||||
});
|
||||
}
|
||||
else if (this.direction === "rl") {
|
||||
this.applyPosition({
|
||||
clientXY: e.clientX,
|
||||
start: right,
|
||||
end: left,
|
||||
});
|
||||
}
|
||||
else if (this.direction === "bt") {
|
||||
this.applyPosition({
|
||||
clientXY: e.clientY,
|
||||
start: bottom,
|
||||
end: top,
|
||||
});
|
||||
}
|
||||
else if (this.direction === "tb") {
|
||||
this.applyPosition({
|
||||
clientXY: e.clientY,
|
||||
start: top,
|
||||
end: bottom,
|
||||
});
|
||||
}
|
||||
};
|
||||
handlePointerDown = (e) => {
|
||||
if (e.button !== 0 || this.opts.disabled.current)
|
||||
return;
|
||||
const sliderNode = this.opts.ref.current;
|
||||
const closestThumb = this.getAllThumbs()[0];
|
||||
if (!closestThumb || !sliderNode)
|
||||
return;
|
||||
const target = e.composedPath()[0] ?? e.target;
|
||||
if (!isElementOrSVGElement(target) || !sliderNode.contains(target))
|
||||
return;
|
||||
e.preventDefault();
|
||||
closestThumb.focus();
|
||||
this.isActive = true;
|
||||
this.handlePointerMove(e);
|
||||
};
|
||||
handlePointerUp = () => {
|
||||
if (this.opts.disabled.current)
|
||||
return;
|
||||
if (this.isActive) {
|
||||
this.opts.onValueCommit.current(untrack(() => this.opts.value.current));
|
||||
}
|
||||
this.isActive = false;
|
||||
};
|
||||
thumbsPropsArr = $derived.by(() => {
|
||||
const currValue = this.opts.value.current;
|
||||
return Array.from({ length: 1 }, () => {
|
||||
const thumbValue = currValue;
|
||||
const thumbPosition = this.getPositionFromValue(thumbValue);
|
||||
const style = getThumbStyles(this.direction, thumbPosition);
|
||||
return {
|
||||
role: "slider",
|
||||
"aria-valuemin": this.opts.min.current,
|
||||
"aria-valuemax": this.opts.max.current,
|
||||
"aria-valuenow": thumbValue,
|
||||
"aria-disabled": boolToStr(this.opts.disabled.current),
|
||||
"aria-orientation": this.opts.orientation.current,
|
||||
"data-value": thumbValue,
|
||||
"data-orientation": this.opts.orientation.current,
|
||||
style,
|
||||
[sliderAttrs.thumb]: "",
|
||||
};
|
||||
});
|
||||
});
|
||||
thumbsRenderArr = $derived.by(() => {
|
||||
return this.thumbsPropsArr.map((_, i) => i);
|
||||
});
|
||||
ticksPropsArr = $derived.by(() => {
|
||||
const steps = this.normalizedSteps;
|
||||
const currValue = this.opts.value.current;
|
||||
return steps.map((tickValue, i) => {
|
||||
// Calculate position relative to the range
|
||||
const tickPosition = this.getPositionFromValue(tickValue);
|
||||
const isFirst = i === 0;
|
||||
const isLast = i === steps.length - 1;
|
||||
const offsetPercentage = isFirst ? 0 : isLast ? -100 : -50;
|
||||
const style = getTickStyles(this.direction, tickPosition, offsetPercentage);
|
||||
const bounded = tickValue <= currValue;
|
||||
return {
|
||||
"data-disabled": boolToEmptyStrOrUndef(this.opts.disabled.current),
|
||||
"data-orientation": this.opts.orientation.current,
|
||||
"data-bounded": bounded ? "" : undefined,
|
||||
"data-value": tickValue,
|
||||
"data-selected": this.isTickValueSelected(tickValue) ? "" : undefined,
|
||||
style,
|
||||
[sliderAttrs.tick]: "",
|
||||
};
|
||||
});
|
||||
});
|
||||
ticksRenderArr = $derived.by(() => {
|
||||
return this.ticksPropsArr.map((_, i) => i);
|
||||
});
|
||||
tickItemsArr = $derived.by(() => {
|
||||
return this.ticksPropsArr.map((tick, i) => ({
|
||||
value: tick["data-value"],
|
||||
index: i,
|
||||
}));
|
||||
});
|
||||
thumbItemsArr = $derived.by(() => {
|
||||
const currValue = this.opts.value.current;
|
||||
return [
|
||||
{
|
||||
value: currValue,
|
||||
index: 0,
|
||||
},
|
||||
];
|
||||
});
|
||||
snippetProps = $derived.by(() => ({
|
||||
ticks: this.ticksRenderArr,
|
||||
thumbs: this.thumbsRenderArr,
|
||||
tickItems: this.tickItemsArr,
|
||||
thumbItems: this.thumbItemsArr,
|
||||
}));
|
||||
}
|
||||
class SliderMultiRootState extends SliderBaseRootState {
|
||||
opts;
|
||||
isMulti = true;
|
||||
activeThumb = $state(null);
|
||||
currentThumbIdx = $state(0);
|
||||
constructor(opts) {
|
||||
super(opts);
|
||||
this.opts = opts;
|
||||
onMountEffect(() => {
|
||||
return executeCallbacks(on(this.domContext.getDocument(), "pointerdown", this.handlePointerDown), on(this.domContext.getDocument(), "pointerup", this.handlePointerUp), on(this.domContext.getDocument(), "pointermove", this.handlePointerMove), on(this.domContext.getDocument(), "pointerleave", this.handlePointerUp));
|
||||
});
|
||||
watch([
|
||||
() => this.opts.step.current,
|
||||
() => this.opts.min.current,
|
||||
() => this.opts.max.current,
|
||||
() => this.opts.value.current,
|
||||
], ([step, min, max, value]) => {
|
||||
const steps = normalizeSteps(step, min, max);
|
||||
const isValidValue = (v) => {
|
||||
return steps.includes(v);
|
||||
};
|
||||
const gcv = (v) => {
|
||||
return snapValueToCustomSteps(v, steps);
|
||||
};
|
||||
if (value.some((v) => !isValidValue(v))) {
|
||||
this.opts.value.current = value.map(gcv);
|
||||
}
|
||||
});
|
||||
}
|
||||
isTickValueSelected = (tickValue) => {
|
||||
return this.opts.value.current.includes(tickValue);
|
||||
};
|
||||
isThumbActive(index) {
|
||||
return this.isActive && this.activeThumb?.idx === index;
|
||||
}
|
||||
applyPosition({ clientXY, activeThumbIdx, start, end, }) {
|
||||
const min = this.opts.min.current;
|
||||
const max = this.opts.max.current;
|
||||
const percent = (clientXY - start) / (end - start);
|
||||
const val = percent * (max - min) + min;
|
||||
if (val < min) {
|
||||
this.updateValue(min, activeThumbIdx);
|
||||
}
|
||||
else if (val > max) {
|
||||
this.updateValue(max, activeThumbIdx);
|
||||
}
|
||||
else {
|
||||
const steps = this.normalizedSteps;
|
||||
const newValue = snapValueToCustomSteps(val, steps);
|
||||
this.updateValue(newValue, activeThumbIdx);
|
||||
}
|
||||
}
|
||||
#getClosestThumb = (e) => {
|
||||
const thumbs = this.getAllThumbs();
|
||||
if (!thumbs.length)
|
||||
return;
|
||||
for (const thumb of thumbs) {
|
||||
thumb.blur();
|
||||
}
|
||||
const distances = thumbs.map((thumb) => {
|
||||
if (this.opts.orientation.current === "horizontal") {
|
||||
const { left, right } = thumb.getBoundingClientRect();
|
||||
return Math.abs(e.clientX - (left + right) / 2);
|
||||
}
|
||||
else {
|
||||
const { top, bottom } = thumb.getBoundingClientRect();
|
||||
return Math.abs(e.clientY - (top + bottom) / 2);
|
||||
}
|
||||
});
|
||||
const node = thumbs[distances.indexOf(Math.min(...distances))];
|
||||
const idx = thumbs.indexOf(node);
|
||||
return {
|
||||
node,
|
||||
idx,
|
||||
};
|
||||
};
|
||||
handlePointerMove = (e) => {
|
||||
if (!this.isActive || this.opts.disabled.current)
|
||||
return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const sliderNode = this.opts.ref.current;
|
||||
const activeThumb = this.activeThumb;
|
||||
if (!sliderNode || !activeThumb)
|
||||
return;
|
||||
activeThumb.node.focus();
|
||||
const { left, right, top, bottom } = sliderNode.getBoundingClientRect();
|
||||
const direction = this.direction;
|
||||
if (direction === "lr") {
|
||||
this.applyPosition({
|
||||
clientXY: e.clientX,
|
||||
activeThumbIdx: activeThumb.idx,
|
||||
start: left,
|
||||
end: right,
|
||||
});
|
||||
}
|
||||
else if (direction === "rl") {
|
||||
this.applyPosition({
|
||||
clientXY: e.clientX,
|
||||
activeThumbIdx: activeThumb.idx,
|
||||
start: right,
|
||||
end: left,
|
||||
});
|
||||
}
|
||||
else if (direction === "bt") {
|
||||
this.applyPosition({
|
||||
clientXY: e.clientY,
|
||||
activeThumbIdx: activeThumb.idx,
|
||||
start: bottom,
|
||||
end: top,
|
||||
});
|
||||
}
|
||||
else if (direction === "tb") {
|
||||
this.applyPosition({
|
||||
clientXY: e.clientY,
|
||||
activeThumbIdx: activeThumb.idx,
|
||||
start: top,
|
||||
end: bottom,
|
||||
});
|
||||
}
|
||||
};
|
||||
handlePointerDown = (e) => {
|
||||
if (e.button !== 0 || this.opts.disabled.current)
|
||||
return;
|
||||
const sliderNode = this.opts.ref.current;
|
||||
const closestThumb = this.#getClosestThumb(e);
|
||||
if (!closestThumb || !sliderNode)
|
||||
return;
|
||||
const target = e.composedPath()[0] ?? e.target;
|
||||
if (!isElementOrSVGElement(target) || !sliderNode.contains(target))
|
||||
return;
|
||||
e.preventDefault();
|
||||
this.activeThumb = closestThumb;
|
||||
closestThumb.node.focus();
|
||||
this.isActive = true;
|
||||
this.handlePointerMove(e);
|
||||
};
|
||||
handlePointerUp = () => {
|
||||
if (this.opts.disabled.current)
|
||||
return;
|
||||
if (this.isActive) {
|
||||
this.opts.onValueCommit.current(untrack(() => this.opts.value.current));
|
||||
}
|
||||
this.isActive = false;
|
||||
};
|
||||
getAllThumbs = () => {
|
||||
const node = this.opts.ref.current;
|
||||
if (!node)
|
||||
return [];
|
||||
const thumbs = Array.from(node.querySelectorAll(sliderAttrs.selector("thumb")));
|
||||
return thumbs;
|
||||
};
|
||||
updateValue = (thumbValue, idx) => {
|
||||
const currValue = this.opts.value.current;
|
||||
if (!currValue.length) {
|
||||
this.opts.value.current.push(thumbValue);
|
||||
return;
|
||||
}
|
||||
const valueAtIndex = currValue[idx];
|
||||
if (valueAtIndex === thumbValue)
|
||||
return;
|
||||
const newValue = [...currValue];
|
||||
if (!isValidIndex(idx, newValue))
|
||||
return;
|
||||
const direction = newValue[idx] > thumbValue ? -1 : +1;
|
||||
const swap = () => {
|
||||
const diffIndex = idx + direction;
|
||||
newValue[idx] = newValue[diffIndex];
|
||||
newValue[diffIndex] = thumbValue;
|
||||
const thumbs = this.getAllThumbs();
|
||||
if (!thumbs.length)
|
||||
return;
|
||||
thumbs[diffIndex]?.focus();
|
||||
this.activeThumb = { node: thumbs[diffIndex], idx: diffIndex };
|
||||
};
|
||||
if (this.opts.autoSort.current &&
|
||||
((direction === -1 && thumbValue < newValue[idx - 1]) ||
|
||||
(direction === 1 && thumbValue > newValue[idx + 1]))) {
|
||||
swap();
|
||||
this.opts.value.current = newValue;
|
||||
return;
|
||||
}
|
||||
const steps = this.normalizedSteps;
|
||||
newValue[idx] = snapValueToCustomSteps(thumbValue, steps);
|
||||
this.opts.value.current = newValue;
|
||||
};
|
||||
thumbsPropsArr = $derived.by(() => {
|
||||
const currValue = this.opts.value.current;
|
||||
return Array.from({ length: currValue.length || 1 }, (_, i) => {
|
||||
const currThumb = untrack(() => this.currentThumbIdx);
|
||||
if (currThumb < currValue.length) {
|
||||
untrack(() => {
|
||||
this.currentThumbIdx = currThumb + 1;
|
||||
});
|
||||
}
|
||||
const thumbValue = currValue[i];
|
||||
const thumbPosition = this.getPositionFromValue(thumbValue ?? 0);
|
||||
const style = getThumbStyles(this.direction, thumbPosition);
|
||||
return {
|
||||
role: "slider",
|
||||
"aria-valuemin": this.opts.min.current,
|
||||
"aria-valuemax": this.opts.max.current,
|
||||
"aria-valuenow": thumbValue,
|
||||
"aria-disabled": boolToStr(this.opts.disabled.current),
|
||||
"aria-orientation": this.opts.orientation.current,
|
||||
"data-value": thumbValue,
|
||||
"data-orientation": this.opts.orientation.current,
|
||||
style,
|
||||
[sliderAttrs.thumb]: "",
|
||||
};
|
||||
});
|
||||
});
|
||||
thumbsRenderArr = $derived.by(() => {
|
||||
return this.thumbsPropsArr.map((_, i) => i);
|
||||
});
|
||||
ticksPropsArr = $derived.by(() => {
|
||||
const steps = this.normalizedSteps;
|
||||
const currValue = this.opts.value.current;
|
||||
return steps.map((tickValue, i) => {
|
||||
// Calculate position relative to the range
|
||||
const tickPosition = this.getPositionFromValue(tickValue);
|
||||
const isFirst = i === 0;
|
||||
const isLast = i === steps.length - 1;
|
||||
const offsetPercentage = isFirst ? 0 : isLast ? -100 : -50;
|
||||
const style = getTickStyles(this.direction, tickPosition, offsetPercentage);
|
||||
const bounded = currValue.length === 1
|
||||
? tickValue <= currValue[0]
|
||||
: currValue[0] <= tickValue && tickValue <= currValue[currValue.length - 1];
|
||||
return {
|
||||
"data-disabled": boolToEmptyStrOrUndef(this.opts.disabled.current),
|
||||
"data-orientation": this.opts.orientation.current,
|
||||
"data-bounded": bounded ? "" : undefined,
|
||||
"data-value": tickValue,
|
||||
style,
|
||||
[sliderAttrs.tick]: "",
|
||||
};
|
||||
});
|
||||
});
|
||||
ticksRenderArr = $derived.by(() => {
|
||||
return this.ticksPropsArr.map((_, i) => i);
|
||||
});
|
||||
tickItemsArr = $derived.by(() => {
|
||||
return this.ticksPropsArr.map((tick, i) => ({
|
||||
value: tick["data-value"],
|
||||
index: i,
|
||||
}));
|
||||
});
|
||||
thumbItemsArr = $derived.by(() => {
|
||||
const currValue = this.opts.value.current;
|
||||
return currValue.map((value, index) => ({
|
||||
value,
|
||||
index,
|
||||
}));
|
||||
});
|
||||
snippetProps = $derived.by(() => ({
|
||||
ticks: this.ticksRenderArr,
|
||||
thumbs: this.thumbsRenderArr,
|
||||
tickItems: this.tickItemsArr,
|
||||
thumbItems: this.thumbItemsArr,
|
||||
}));
|
||||
}
|
||||
export class SliderRootState {
|
||||
static create(opts) {
|
||||
const { type, ...rest } = opts;
|
||||
const rootState = type === "single"
|
||||
? new SliderSingleRootState(rest)
|
||||
: new SliderMultiRootState(rest);
|
||||
return SliderRootContext.set(rootState);
|
||||
}
|
||||
}
|
||||
const VALID_SLIDER_KEYS = [
|
||||
kbd.ARROW_LEFT,
|
||||
kbd.ARROW_RIGHT,
|
||||
kbd.ARROW_UP,
|
||||
kbd.ARROW_DOWN,
|
||||
kbd.HOME,
|
||||
kbd.END,
|
||||
];
|
||||
export class SliderRangeState {
|
||||
static create(opts) {
|
||||
return new SliderRangeState(opts, SliderRootContext.get());
|
||||
}
|
||||
opts;
|
||||
root;
|
||||
attachment;
|
||||
constructor(opts, root) {
|
||||
this.opts = opts;
|
||||
this.root = root;
|
||||
this.attachment = attachRef(opts.ref);
|
||||
}
|
||||
rangeStyles = $derived.by(() => {
|
||||
if (Array.isArray(this.root.opts.value.current)) {
|
||||
// Multi-slider: range between min and max thumbs
|
||||
const min = this.root.opts.value.current.length > 1
|
||||
? this.root.getPositionFromValue(Math.min(...this.root.opts.value.current) ?? 0)
|
||||
: 0;
|
||||
const max = 100 -
|
||||
this.root.getPositionFromValue(Math.max(...this.root.opts.value.current) ?? 0);
|
||||
return {
|
||||
position: "absolute",
|
||||
...getRangeStyles(this.root.direction, min, max),
|
||||
};
|
||||
}
|
||||
else {
|
||||
// Single slider: range from start to current value
|
||||
const trackPadding = this.root.opts.trackPadding?.current;
|
||||
const currentValue = this.root.opts.value.current;
|
||||
const maxValue = this.root.opts.max.current;
|
||||
// Always start from 0% (beginning of track container)
|
||||
const min = 0;
|
||||
// If trackPadding is set and we're at max value, extend to fill the container
|
||||
// Otherwise use the thumb position
|
||||
const max = trackPadding !== undefined && trackPadding > 0 && currentValue === maxValue
|
||||
? 0 // 100% - 0% = full width
|
||||
: 100 - this.root.getPositionFromValue(currentValue);
|
||||
return {
|
||||
position: "absolute",
|
||||
...getRangeStyles(this.root.direction, min, max),
|
||||
};
|
||||
}
|
||||
});
|
||||
props = $derived.by(() => ({
|
||||
id: this.opts.id.current,
|
||||
"data-orientation": this.root.opts.orientation.current,
|
||||
"data-disabled": boolToEmptyStrOrUndef(this.root.opts.disabled.current),
|
||||
style: this.rangeStyles,
|
||||
[sliderAttrs.range]: "",
|
||||
...this.attachment,
|
||||
}));
|
||||
}
|
||||
export class SliderThumbState {
|
||||
static create(opts) {
|
||||
return new SliderThumbState(opts, SliderRootContext.get());
|
||||
}
|
||||
opts;
|
||||
root;
|
||||
attachment;
|
||||
#isDisabled = $derived.by(() => this.root.opts.disabled.current || this.opts.disabled.current);
|
||||
constructor(opts, root) {
|
||||
this.opts = opts;
|
||||
this.root = root;
|
||||
this.attachment = attachRef(opts.ref);
|
||||
this.onkeydown = this.onkeydown.bind(this);
|
||||
}
|
||||
#updateValue(newValue) {
|
||||
if (this.root.isMulti) {
|
||||
this.root.updateValue(newValue, this.opts.index.current);
|
||||
}
|
||||
else {
|
||||
this.root.updateValue(newValue);
|
||||
}
|
||||
}
|
||||
onkeydown(e) {
|
||||
if (this.#isDisabled)
|
||||
return;
|
||||
const currNode = this.opts.ref.current;
|
||||
if (!currNode)
|
||||
return;
|
||||
const thumbs = this.root.getAllThumbs();
|
||||
if (!thumbs.length)
|
||||
return;
|
||||
const idx = thumbs.indexOf(currNode);
|
||||
if (this.root.isMulti) {
|
||||
this.root.currentThumbIdx = idx;
|
||||
}
|
||||
if (!VALID_SLIDER_KEYS.includes(e.key))
|
||||
return;
|
||||
e.preventDefault();
|
||||
const min = this.root.opts.min.current;
|
||||
const max = this.root.opts.max.current;
|
||||
const value = this.root.opts.value.current;
|
||||
const thumbValue = Array.isArray(value) ? value[idx] : value;
|
||||
const orientation = this.root.opts.orientation.current;
|
||||
const direction = this.root.direction;
|
||||
const steps = this.root.normalizedSteps;
|
||||
switch (e.key) {
|
||||
case kbd.HOME:
|
||||
this.#updateValue(min);
|
||||
break;
|
||||
case kbd.END:
|
||||
this.#updateValue(max);
|
||||
break;
|
||||
case kbd.ARROW_LEFT:
|
||||
if (orientation !== "horizontal")
|
||||
break;
|
||||
if (e.metaKey) {
|
||||
const newValue = direction === "rl" ? max : min;
|
||||
this.#updateValue(newValue);
|
||||
}
|
||||
else {
|
||||
const stepDirection = direction === "rl" ? "next" : "prev";
|
||||
const newValue = getAdjacentStepValue(thumbValue, steps, stepDirection);
|
||||
this.#updateValue(newValue);
|
||||
}
|
||||
break;
|
||||
case kbd.ARROW_RIGHT:
|
||||
if (orientation !== "horizontal")
|
||||
break;
|
||||
if (e.metaKey) {
|
||||
const newValue = direction === "rl" ? min : max;
|
||||
this.#updateValue(newValue);
|
||||
}
|
||||
else {
|
||||
const stepDirection = direction === "rl" ? "prev" : "next";
|
||||
const newValue = getAdjacentStepValue(thumbValue, steps, stepDirection);
|
||||
this.#updateValue(newValue);
|
||||
}
|
||||
break;
|
||||
case kbd.ARROW_UP:
|
||||
if (e.metaKey) {
|
||||
const newValue = direction === "tb" ? min : max;
|
||||
this.#updateValue(newValue);
|
||||
}
|
||||
else {
|
||||
const stepDirection = direction === "tb" ? "prev" : "next";
|
||||
const newValue = getAdjacentStepValue(thumbValue, steps, stepDirection);
|
||||
this.#updateValue(newValue);
|
||||
}
|
||||
break;
|
||||
case kbd.ARROW_DOWN:
|
||||
if (e.metaKey) {
|
||||
const newValue = direction === "tb" ? max : min;
|
||||
this.#updateValue(newValue);
|
||||
}
|
||||
else {
|
||||
const stepDirection = direction === "tb" ? "next" : "prev";
|
||||
const newValue = getAdjacentStepValue(thumbValue, steps, stepDirection);
|
||||
this.#updateValue(newValue);
|
||||
}
|
||||
break;
|
||||
}
|
||||
// @ts-expect-error - this is fine
|
||||
this.root.opts.onValueCommit.current(this.root.opts.value.current);
|
||||
}
|
||||
props = $derived.by(() => ({
|
||||
...this.root.thumbsPropsArr[this.opts.index.current],
|
||||
id: this.opts.id.current,
|
||||
onkeydown: this.onkeydown,
|
||||
"data-active": this.root.isThumbActive(this.opts.index.current) ? "" : undefined,
|
||||
"data-disabled": boolToEmptyStrOrUndef(this.opts.disabled.current || this.root.opts.disabled.current),
|
||||
tabindex: this.opts.disabled.current || this.root.opts.disabled.current ? -1 : 0,
|
||||
...this.attachment,
|
||||
}));
|
||||
}
|
||||
export class SliderTickState {
|
||||
static create(opts) {
|
||||
return new SliderTickState(opts, SliderRootContext.get());
|
||||
}
|
||||
opts;
|
||||
root;
|
||||
attachment;
|
||||
constructor(opts, root) {
|
||||
this.opts = opts;
|
||||
this.root = root;
|
||||
this.attachment = attachRef(opts.ref);
|
||||
}
|
||||
props = $derived.by(() => ({
|
||||
...this.root.ticksPropsArr[this.opts.index.current],
|
||||
id: this.opts.id.current,
|
||||
...this.attachment,
|
||||
}));
|
||||
}
|
||||
export class SliderTickLabelState {
|
||||
static create(opts) {
|
||||
return new SliderTickLabelState(opts, SliderRootContext.get());
|
||||
}
|
||||
opts;
|
||||
root;
|
||||
attachment;
|
||||
constructor(opts, root) {
|
||||
this.opts = opts;
|
||||
this.root = root;
|
||||
this.attachment = attachRef(opts.ref);
|
||||
}
|
||||
props = $derived.by(() => {
|
||||
const tickProps = this.root.ticksPropsArr[this.opts.index.current];
|
||||
const steps = this.root.normalizedSteps;
|
||||
const tickValue = steps[this.opts.index.current];
|
||||
const tickPosition = this.root.getPositionFromValue(tickValue);
|
||||
const labelPosition = this.opts.position?.current ?? "top";
|
||||
const style = getTickLabelStyles(this.root.direction, tickPosition, labelPosition);
|
||||
return {
|
||||
id: this.opts.id.current,
|
||||
"data-orientation": this.root.opts.orientation.current,
|
||||
"data-disabled": boolToEmptyStrOrUndef(this.root.opts.disabled.current),
|
||||
"data-bounded": tickProps["data-bounded"],
|
||||
"data-value": tickValue,
|
||||
"data-selected": this.root.isTickValueSelected(tickValue) ? "" : undefined,
|
||||
"data-position": labelPosition,
|
||||
style,
|
||||
[sliderAttrs["tick-label"]]: "",
|
||||
...this.attachment,
|
||||
};
|
||||
});
|
||||
}
|
||||
export class SliderThumbLabelState {
|
||||
static create(opts) {
|
||||
return new SliderThumbLabelState(opts, SliderRootContext.get());
|
||||
}
|
||||
opts;
|
||||
root;
|
||||
attachment;
|
||||
constructor(opts, root) {
|
||||
this.opts = opts;
|
||||
this.root = root;
|
||||
this.attachment = attachRef(opts.ref);
|
||||
}
|
||||
props = $derived.by(() => {
|
||||
const value = this.root.opts.value.current;
|
||||
const thumbValue = Array.isArray(value) ? value[this.opts.index.current] : value;
|
||||
const thumbPosition = this.root.getPositionFromValue(thumbValue);
|
||||
const labelPosition = this.opts.position?.current ?? "top";
|
||||
const style = getThumbLabelStyles(this.root.direction, thumbPosition, labelPosition);
|
||||
return {
|
||||
id: this.opts.id.current,
|
||||
"data-orientation": this.root.opts.orientation.current,
|
||||
"data-disabled": boolToEmptyStrOrUndef(this.root.opts.disabled.current),
|
||||
"data-value": thumbValue,
|
||||
"data-active": this.root.isThumbActive(this.opts.index.current) ? "" : undefined,
|
||||
"data-position": labelPosition,
|
||||
style,
|
||||
[sliderAttrs["thumb-label"]]: "",
|
||||
...this.attachment,
|
||||
};
|
||||
});
|
||||
}
|
||||
+233
@@ -0,0 +1,233 @@
|
||||
import type { OnChangeFn, WithChild, Without } from "../../internal/types.js";
|
||||
import type { BitsPrimitiveSpanAttributes } from "../../shared/attributes.js";
|
||||
import type { Direction, Orientation, SliderThumbPositioning } from "../../shared/index.js";
|
||||
export type TickItem = {
|
||||
/**
|
||||
* The value this tick represents
|
||||
*/
|
||||
value: number;
|
||||
/**
|
||||
* The index of this tick in the array of ticks provided by the `children`
|
||||
* or `child` snippet prop of the `Slider.Root` component.
|
||||
*/
|
||||
index: number;
|
||||
};
|
||||
export type ThumbItem = {
|
||||
/**
|
||||
* The value this thumb represents
|
||||
*/
|
||||
value: number;
|
||||
/**
|
||||
* The index of this thumb in the array of thumbs provided by the `children`
|
||||
* or `child` snippet prop of the `Slider.Root` component.
|
||||
*/
|
||||
index: number;
|
||||
};
|
||||
export type SliderLabelPosition = "top" | "bottom" | "left" | "right";
|
||||
export type SliderRootSnippetProps = {
|
||||
/**
|
||||
* Use `tickItems` instead. Will be removed in Bits UI v3.
|
||||
*
|
||||
* The indices of the ticks.
|
||||
*
|
||||
* @deprecated Use `tickItems` instead.
|
||||
*/
|
||||
ticks: number[];
|
||||
/**
|
||||
* Use `thumbItems` instead. Will be removed in Bits UI v3.
|
||||
*
|
||||
* The indices of the thumbs.
|
||||
*
|
||||
* @deprecated Use `thumbItems` instead
|
||||
*/
|
||||
thumbs: number[];
|
||||
/**
|
||||
* An array of objects containing the value and index of each tick, useful for
|
||||
* rendering ticks along with labels for each tick.
|
||||
*/
|
||||
tickItems: TickItem[];
|
||||
/**
|
||||
* An array of objects containing the value and index of each thumb, useful for
|
||||
* rendering thumbs along with labels for each thumb.
|
||||
*/
|
||||
thumbItems: ThumbItem[];
|
||||
};
|
||||
export type BaseSliderRootPropsWithoutHTML = {
|
||||
/**
|
||||
* Whether to automatically sort the values in the array when moving thumbs past
|
||||
* one another.
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
autoSort?: boolean;
|
||||
/**
|
||||
* The minimum value of the slider.
|
||||
*
|
||||
* @default 0 (for number step) or the min value of the step array (for array step)
|
||||
*/
|
||||
min?: number;
|
||||
/**
|
||||
* The maximum value of the slider.
|
||||
*
|
||||
* @default 100 (for number step) or the max value of the step array (for array step)
|
||||
*/
|
||||
max?: number;
|
||||
/**
|
||||
* The amount to increment the value by when the user presses the arrow keys,
|
||||
* or an array of specific values that the slider can snap to.
|
||||
*
|
||||
* When an array is provided, the slider will only allow values that exist in the array,
|
||||
* creating discrete tick points. The array values should be within the min/max range
|
||||
* and will be automatically sorted.
|
||||
*
|
||||
* @default 1
|
||||
*/
|
||||
step?: number | number[];
|
||||
/**
|
||||
* The direction of the slider.
|
||||
*
|
||||
* For vertical sliders, setting `dir` to `'rtl'` will caus the slider to start
|
||||
* from the top and move downwards. For horizontal sliders, setting `dir` to `'rtl'`
|
||||
* will cause the slider to start from the left and move rightwards.
|
||||
*
|
||||
* @default 'ltr'
|
||||
*/
|
||||
dir?: Direction;
|
||||
/**
|
||||
* The orientation of the slider.
|
||||
*
|
||||
* @default "horizontal"
|
||||
*/
|
||||
orientation?: Orientation;
|
||||
/**
|
||||
* Whether the slider is disabled or not.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
disabled?: boolean;
|
||||
/**
|
||||
* The positioning of the slider thumb.
|
||||
*
|
||||
* @default "contain"
|
||||
*/
|
||||
thumbPositioning?: SliderThumbPositioning;
|
||||
/**
|
||||
* Padding percentage for the track. Creates space before the first
|
||||
* and after the last tick positions.
|
||||
*
|
||||
* This can also be used as an SSR-friendly alternative to `thumbPositioning="contain"`,
|
||||
* which requires client-side measurement.
|
||||
*/
|
||||
trackPadding?: number;
|
||||
};
|
||||
export type SliderSingleRootPropsWithoutHTML = BaseSliderRootPropsWithoutHTML & {
|
||||
/**
|
||||
* The type of slider. If set to `'multiple'`, the slider will
|
||||
* allow multiple ticks and the `value` will be an array of numbers.
|
||||
*
|
||||
* @required
|
||||
*/
|
||||
type: "single";
|
||||
/**
|
||||
* The value of the slider.
|
||||
* @bindable
|
||||
*
|
||||
* @default min
|
||||
*/
|
||||
value?: number;
|
||||
/**
|
||||
* A callback function called when the value changes.
|
||||
*/
|
||||
onValueChange?: OnChangeFn<number>;
|
||||
/**
|
||||
* A callback function called when the user stops dragging the
|
||||
* thumb and the value is committed.
|
||||
*/
|
||||
onValueCommit?: OnChangeFn<number>;
|
||||
};
|
||||
export type SliderMultiRootPropsWithoutHTML = BaseSliderRootPropsWithoutHTML & {
|
||||
/**
|
||||
* The type of slider. If set to `'multiple'`, the slider will
|
||||
* allow multiple ticks and the `value` will be an array of numbers.
|
||||
*
|
||||
* @required
|
||||
*/
|
||||
type: "multiple";
|
||||
/**
|
||||
* The value of the slider.
|
||||
* @bindable
|
||||
*/
|
||||
value?: number[];
|
||||
/**
|
||||
* A callback function called when the value changes.
|
||||
*/
|
||||
onValueChange?: OnChangeFn<number[]>;
|
||||
/**
|
||||
* A callback function called when the user stops dragging the
|
||||
* thumb and the value is committed.
|
||||
*/
|
||||
onValueCommit?: OnChangeFn<number[]>;
|
||||
};
|
||||
export type SliderRootPropsWithoutHTML = WithChild<SliderSingleRootPropsWithoutHTML, SliderRootSnippetProps> | WithChild<SliderMultiRootPropsWithoutHTML, SliderRootSnippetProps>;
|
||||
export type SliderSingleRootProps = SliderSingleRootPropsWithoutHTML & Without<BitsPrimitiveSpanAttributes, WithChild<SliderSingleRootPropsWithoutHTML, SliderRootSnippetProps>>;
|
||||
export type SliderMultipleRootProps = SliderMultiRootPropsWithoutHTML & Without<BitsPrimitiveSpanAttributes, WithChild<SliderMultiRootPropsWithoutHTML, SliderRootSnippetProps>>;
|
||||
export type SliderRootProps = SliderRootPropsWithoutHTML & Without<BitsPrimitiveSpanAttributes, SliderRootPropsWithoutHTML>;
|
||||
export type SliderRangePropsWithoutHTML = WithChild;
|
||||
export type SliderRangeProps = SliderRangePropsWithoutHTML & Without<BitsPrimitiveSpanAttributes, SliderRangePropsWithoutHTML>;
|
||||
export type SliderThumbSnippetProps = {
|
||||
active: boolean;
|
||||
};
|
||||
export type SliderThumbPropsWithoutHTML = WithChild<{
|
||||
/**
|
||||
* Whether the thumb is disabled or not.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
disabled?: boolean;
|
||||
/**
|
||||
* The index of the thumb in the array of thumbs provided by the `children` snippet prop of the
|
||||
* `Slider.Root` component.
|
||||
*/
|
||||
index: number;
|
||||
}, SliderThumbSnippetProps>;
|
||||
export type SliderThumbProps = SliderThumbPropsWithoutHTML & Without<BitsPrimitiveSpanAttributes, SliderThumbPropsWithoutHTML>;
|
||||
export type SliderTickPropsWithoutHTML = WithChild<{
|
||||
/**
|
||||
* The index of the tick in the array of ticks provided by the `children` snippet prop of the
|
||||
* `Slider.Root` component.
|
||||
*/
|
||||
index: number;
|
||||
}>;
|
||||
export type SliderTickProps = SliderTickPropsWithoutHTML & Without<BitsPrimitiveSpanAttributes, SliderTickPropsWithoutHTML>;
|
||||
export type SliderTickLabelPropsWithoutHTML = WithChild<{
|
||||
/**
|
||||
* The index of the tick the label represents in the array of ticks
|
||||
* provided by the `children` snippet prop of the `Slider.Root` component.
|
||||
*/
|
||||
index: number;
|
||||
/**
|
||||
* The position of the label relative to the tick.
|
||||
* For horizontal sliders: "top" | "bottom"
|
||||
* For vertical sliders: "left" | "right"
|
||||
*
|
||||
* @default for horizontal sliders = "top" and for vertical sliders = "left"
|
||||
*/
|
||||
position?: SliderLabelPosition;
|
||||
}>;
|
||||
export type SliderTickLabelProps = SliderTickLabelPropsWithoutHTML & Without<BitsPrimitiveSpanAttributes, SliderTickLabelPropsWithoutHTML>;
|
||||
export type SliderThumbLabelPropsWithoutHTML = WithChild<{
|
||||
/**
|
||||
* The index of the thumb the label represents in the array of thumbs
|
||||
* provided by the `children` snippet prop of the `Slider.Root` component.
|
||||
*/
|
||||
index: number;
|
||||
/**
|
||||
* The position of the label relative to the thumb.
|
||||
* For horizontal sliders: "top" | "bottom"
|
||||
* For vertical sliders: "left" | "right"
|
||||
*
|
||||
* @default for horizontal sliders = "top" and for vertical sliders = "left"
|
||||
*/
|
||||
position?: SliderLabelPosition;
|
||||
}>;
|
||||
export type SliderThumbLabelProps = SliderThumbLabelPropsWithoutHTML & Without<BitsPrimitiveSpanAttributes, SliderThumbLabelPropsWithoutHTML>;
|
||||
+1
@@ -0,0 +1 @@
|
||||
export {};
|
||||
Reference in New Issue
Block a user