aboutsummaryrefslogtreecommitdiffstats
path: root/src/lib/vendor
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/vendor')
-rw-r--r--src/lib/vendor/svelte-range-slider/README1
-rw-r--r--src/lib/vendor/svelte-range-slider/range-pips.svelte303
-rw-r--r--src/lib/vendor/svelte-range-slider/range-slider.svelte1026
3 files changed, 1330 insertions, 0 deletions
diff --git a/src/lib/vendor/svelte-range-slider/README b/src/lib/vendor/svelte-range-slider/README
new file mode 100644
index 0000000..b17797b
--- /dev/null
+++ b/src/lib/vendor/svelte-range-slider/README
@@ -0,0 +1 @@
+https://github.com/roycrippen4/svelte-range-slider/tree/master
diff --git a/src/lib/vendor/svelte-range-slider/range-pips.svelte b/src/lib/vendor/svelte-range-slider/range-pips.svelte
new file mode 100644
index 0000000..418fc7e
--- /dev/null
+++ b/src/lib/vendor/svelte-range-slider/range-pips.svelte
@@ -0,0 +1,303 @@
+<script lang="ts" module>
+ export interface PipsProps {
+ min?: number;
+ max?: number;
+ step?: number;
+ values?: number[];
+ vertical?: boolean;
+ reversed?: boolean;
+ hoverable?: boolean;
+ disabled?: boolean;
+ pipstep?: number;
+ prefix?: string;
+ suffix?: string;
+ focus?: boolean;
+ range?: undefined | boolean | 'min' | 'max';
+ all?: undefined | boolean | 'pip' | 'label';
+ first?: boolean | 'pip' | 'label';
+ last?: boolean | 'pip' | 'label';
+ rest?: boolean | 'pip' | 'label';
+ percentOf: (v: number) => number;
+ fixFloat: (v: number) => number;
+ orientationStart?: 'top' | 'bottom' | 'left' | 'right';
+ orientationEnd?: 'top' | 'bottom' | 'left' | 'right';
+ formatter?: (v: number, i: number, p: number) => string;
+ moveHandle: undefined | ((index: number | undefined, value: number) => number);
+ normalisedClient: (e: MouseEvent | TouchEvent) => { x: number; y: number };
+ }
+</script>
+
+<script lang="ts">
+ let {
+ range = false,
+ min = 0,
+ max = 100,
+ step = 1,
+ values = [(max + min) / 2],
+ vertical = false,
+ reversed = false,
+ hoverable = true,
+ disabled = false,
+ pipstep,
+ all = true,
+ first,
+ last,
+ rest,
+ prefix = '',
+ suffix = '',
+ focus,
+ orientationStart,
+ // eslint-disable-next-line no-unused-vars
+ formatter = (v, i, p) => v.toString(),
+ percentOf,
+ moveHandle,
+ fixFloat,
+ normalisedClient
+ }: PipsProps = $props();
+
+ let clientStart = $state({ x: 0, y: 0 });
+ let pipStep = $derived(
+ pipstep ||
+ ((max - min) / step >= (vertical ? 50 : 100) ? (max - min) / (vertical ? 10 : 20) : 1)
+ );
+ let pipCount = $derived(parseInt(((max - min) / (step * pipStep)).toString(), 10));
+ let pipVal = $derived((val: number) => fixFloat(min + val * step * pipStep));
+ let isSelected = $derived((val: number) => values.some((v) => fixFloat(v) === fixFloat(val)));
+ let inRange = $derived((val: number) => {
+ if (range === 'min') {
+ return values[0] > val;
+ }
+ if (range === 'max') {
+ return values[0] < val;
+ }
+ if (range) {
+ return values[0] < val && values[1] > val;
+ }
+ });
+
+ /**
+ * function to run when the user clicks on a label
+ * we store the original client position so we can check if the user has moved the mouse/finger
+ * @param {MouseEvent} e the event from browser
+ **/
+ const labelDown = (e: MouseEvent) => {
+ clientStart = { x: e.clientX, y: e.clientY };
+ };
+
+ /**
+ * function to run when the user releases the mouse/finger
+ * we check if the user has moved the mouse/finger, if not we "click" the label
+ * and move the handle it to the label position
+ * @param {number} val the value of the label
+ * @param {MouseEvent|TouchEvent} e the event from browser
+ */
+ function labelUp(val: number, e: MouseEvent | TouchEvent) {
+ if (disabled) {
+ return;
+ }
+
+ const clientPos = normalisedClient(e);
+ const distanceMoved = Math.sqrt(
+ Math.pow(clientStart.x - clientPos.x, 2) + Math.pow(clientStart.y - clientPos.y, 2)
+ );
+
+ if (clientStart && distanceMoved <= 5) {
+ moveHandle?.(undefined, val);
+ }
+ }
+</script>
+
+<div
+ class="rangePips-f75c52e3-b799-4c81-8238-035d862cc2e6"
+ class:disabled
+ class:hoverable
+ class:vertical
+ class:reversed
+ class:focus
+>
+ {#if (all && first !== false) || first}
+ <span
+ class="pip-680f0f01-664b-43b5-9e1c-789449c63c62 first"
+ class:selected={isSelected(min)}
+ class:in-range={inRange(min)}
+ style="{orientationStart}: 0%;"
+ onpointerdown={labelDown}
+ onpointerup={(e) => labelUp(min, e)}
+ >
+ {#if all === 'label' || first === 'label'}
+ <span class="pipVal-c41e7185-de59-40a7-90f2-e3d98b1e844b">
+ {prefix}{formatter(fixFloat(min), 0, 0)}{suffix}
+ </span>
+ {/if}
+ </span>
+ {/if}
+
+ {#if (all && rest !== false) || rest}
+ <!-- eslint-disable-next-line no-unused-vars -->
+ {#each Array(pipCount + 1) as _, i}
+ {#if pipVal(i) !== min && pipVal(i) !== max}
+ <span
+ class="pip-680f0f01-664b-43b5-9e1c-789449c63c62"
+ class:selected={isSelected(pipVal(i))}
+ class:in-range={inRange(pipVal(i))}
+ style="{orientationStart}: {percentOf(pipVal(i))}%;"
+ onpointerdown={labelDown}
+ onpointerup={(e) => labelUp(pipVal(i), e)}
+ >
+ {#if all === 'label' || rest === 'label'}
+ <span class="pipVal-c41e7185-de59-40a7-90f2-e3d98b1e844b">
+ {prefix}{formatter(pipVal(i), i, percentOf(pipVal(i)))}{suffix}
+ </span>
+ {/if}
+ </span>
+ {/if}
+ {/each}
+ {/if}
+
+ {#if (all && last !== false) || last}
+ <span
+ class="pip last"
+ class:selected={isSelected(max)}
+ class:in-range={inRange(max)}
+ style="{orientationStart}: 100%;"
+ onpointerdown={labelDown}
+ onpointerup={(e) => labelUp(max, e)}
+ >
+ {#if all === 'label' || last === 'label'}
+ <span class="pipVal">
+ {prefix}{formatter(fixFloat(max), pipCount, 100)}{suffix}
+ </span>
+ {/if}
+ </span>
+ {/if}
+</div>
+
+<style>
+ :global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28) {
+ --pip: var(--range-pip, lightslategray);
+ --pip-text: var(--range-pip-text, var(--pip));
+ --pip-active: var(--range-pip-active, darkslategrey);
+ --pip-active-text: var(--range-pip-active-text, var(--pip-active));
+ --pip-hover: var(--range-pip-hover, darkslategrey);
+ --pip-hover-text: var(--range-pip-hover-text, var(--pip-hover));
+ --pip-in-range: var(--range-pip-in-range, var(--pip-active));
+ --pip-in-range-text: var(--range-pip-in-range-text, var(--pip-active-text));
+ }
+ :global(.rangePips-f75c52e3-b799-4c81-8238-035d862cc2e6) {
+ position: absolute;
+ height: 1em;
+ left: 0;
+ right: 0;
+ bottom: -1em;
+ }
+ :global(.rangePips-f75c52e3-b799-4c81-8238-035d862cc2e6.vertical) {
+ height: auto;
+ width: 1em;
+ left: 100%;
+ right: auto;
+ top: 0;
+ bottom: 0;
+ }
+ :global(
+ .rangePips-f75c52e3-b799-4c81-8238-035d862cc2e6 .pip-680f0f01-664b-43b5-9e1c-789449c63c62
+ ) {
+ height: 0.4em;
+ position: absolute;
+ top: 0.25em;
+ width: 1px;
+ white-space: nowrap;
+ }
+ :global(
+ .rangePips-f75c52e3-b799-4c81-8238-035d862cc2e6.vertical
+ .pip-680f0f01-664b-43b5-9e1c-789449c63c62
+ ) {
+ height: 1px;
+ width: 0.4em;
+ left: 0.25em;
+ top: auto;
+ bottom: auto;
+ }
+ :global(
+ .rangePips-f75c52e3-b799-4c81-8238-035d862cc2e6 .pipVal-c41e7185-de59-40a7-90f2-e3d98b1e844b
+ ) {
+ position: absolute;
+ top: 0.4em;
+ transform: translate(-50%, 25%);
+ }
+ :global(
+ .rangePips-f75c52e3-b799-4c81-8238-035d862cc2e6.vertical
+ .pipVal-c41e7185-de59-40a7-90f2-e3d98b1e844b
+ ) {
+ position: absolute;
+ top: 0;
+ left: 0.4em;
+ transform: translate(25%, -50%);
+ }
+ :global(
+ .rangePips-f75c52e3-b799-4c81-8238-035d862cc2e6 .pip-680f0f01-664b-43b5-9e1c-789449c63c62
+ ) {
+ transition: all 0.15s ease;
+ }
+ :global(
+ .rangePips-f75c52e3-b799-4c81-8238-035d862cc2e6 .pipVal-c41e7185-de59-40a7-90f2-e3d98b1e844b
+ ) {
+ transition:
+ all 0.15s ease,
+ font-weight 0s linear;
+ }
+ :global(
+ .rangePips-f75c52e3-b799-4c81-8238-035d862cc2e6 .pip-680f0f01-664b-43b5-9e1c-789449c63c62
+ ) {
+ color: var(--pip-text, lightslategray);
+ background-color: var(--pip, lightslategray);
+ }
+ :global(.rangePips-f75c52e3-b799-4c81-8238-035d862cc2e6 .pip.selected) {
+ color: var(--pip-active-text, darkslategrey);
+ background-color: var(--pip-active, darkslategrey);
+ }
+ :global(.rangePips-f75c52e3-b799-4c81-8238-035d862cc2e6.hoverable:not(.disabled) .pip:hover) {
+ color: var(--pip-hover-text, darkslategrey);
+ background-color: var(--pip-hover, darkslategrey);
+ }
+ :global(.rangePips-f75c52e3-b799-4c81-8238-035d862cc2e6 .pip.in-range) {
+ color: var(--pip-in-range-text, darkslategrey);
+ background-color: var(--pip-in-range, darkslategrey);
+ }
+ :global(.rangePips-f75c52e3-b799-4c81-8238-035d862cc2e6 .pip.selected) {
+ height: 0.75em;
+ }
+ :global(.rangePips-f75c52e3-b799-4c81-8238-035d862cc2e6.vertical .pip.selected) {
+ height: 1px;
+ width: 0.75em;
+ }
+ :global(
+ .rangePips-f75c52e3-b799-4c81-8238-035d862cc2e6
+ .pip.selected
+ .pipVal-c41e7185-de59-40a7-90f2-e3d98b1e844b
+ ) {
+ font-weight: bold;
+ top: 0.75em;
+ }
+ :global(
+ .rangePips-f75c52e3-b799-4c81-8238-035d862cc2e6.vertical
+ .pip.selected
+ .pipVal-c41e7185-de59-40a7-90f2-e3d98b1e844b
+ ) {
+ top: 0;
+ left: 0.75em;
+ }
+ :global(
+ .rangePips-f75c52e3-b799-4c81-8238-035d862cc2e6.hoverable:not(.disabled)
+ .pip:not(.selected):hover
+ ) {
+ transition: none;
+ }
+ :global(
+ .rangePips-f75c52e3-b799-4c81-8238-035d862cc2e6.hoverable:not(.disabled)
+ .pip:not(.selected):hover
+ .pipVal-c41e7185-de59-40a7-90f2-e3d98b1e844b
+ ) {
+ transition: none;
+ font-weight: bold;
+ }
+</style>
diff --git a/src/lib/vendor/svelte-range-slider/range-slider.svelte b/src/lib/vendor/svelte-range-slider/range-slider.svelte
new file mode 100644
index 0000000..7f522e2
--- /dev/null
+++ b/src/lib/vendor/svelte-range-slider/range-slider.svelte
@@ -0,0 +1,1026 @@
+<script lang="ts" module>
+ export type ChangeEvent = {
+ activeHandle: number;
+ startValue: number;
+ previousValue: number;
+ value: number;
+ values: number[];
+ };
+
+ export type StartEvent = { activeHandle: number; value: number; values: number[] };
+
+ export type StopEvent = {
+ activeHandle: number;
+ startValue: number;
+ value: number;
+ values: number[];
+ };
+
+ export interface RangeSliderProps {
+ range?: boolean | 'min' | 'max';
+ onchange?: (event: ChangeEvent) => void;
+ onstart?: (event: StartEvent) => void;
+ onstop?: (event: StopEvent) => void;
+ pushy?: boolean;
+ min?: number;
+ max?: number;
+ ariaLabels?: string[];
+ precision?: number;
+ springOptions?: { stiffness: number; damping: number };
+ id?: string;
+ prefix?: string;
+ suffix?: string;
+ pips?: boolean;
+ pipstep?: number;
+ all?: boolean | 'pip' | 'label';
+ first?: boolean | 'pip' | 'label';
+ last?: boolean | 'pip' | 'label';
+ rest?: boolean | 'pip' | 'label';
+ step?: number;
+ value?: number;
+ values?: number[];
+ vertical?: boolean;
+ float?: boolean;
+ reversed?: boolean;
+ hoverable?: boolean;
+ disabled?: boolean;
+ formatter?: (value: number, index: number, percent: number) => string;
+ handleFormatter?: (value: number, index: number, percent: number) => string;
+ }
+</script>
+
+<script lang="ts">
+ import { spring } from 'svelte/motion';
+ import RangePips from './range-pips.svelte';
+
+ let {
+ range = false,
+ pushy = false,
+ min = 0,
+ max = 100,
+ ariaLabels = [],
+ precision = 2,
+ springOptions = { stiffness: 0.15, damping: 0.4 },
+ id = '',
+ prefix = '',
+ suffix = '',
+ pips = false,
+ pipstep,
+ all,
+ first,
+ last,
+ rest,
+ step = 1,
+ value = $bindable(0),
+ values = $bindable([(max + min) / 2]),
+ vertical = false,
+ float = false,
+ reversed = false,
+ hoverable = true,
+ disabled = false,
+ onchange,
+ onstart,
+ onstop,
+ formatter = (value: { toString: () => string }) => value.toString(),
+ handleFormatter = formatter
+ }: RangeSliderProps = $props();
+
+ if (value) {
+ values = [value];
+ }
+
+ let slider: Element | undefined = $state(undefined);
+ let valueLength = $state(0);
+ let focus = $state(false);
+ let handleActivated = $state(false);
+ let handlePressed = $state(false);
+ let keyboardActive = $state(false);
+ let activeHandle = $state(values.length - 1);
+
+ let startValue: number | undefined = $state();
+
+ let previousValue: number | undefined = $state();
+
+ /**
+ * make sure the value is coerced to a float value
+ * @param {number} v the value to fix
+ * @return {number} a float version of the input
+ **/
+ const fixFloat = (v: number): number => parseFloat((+v).toFixed(precision));
+
+ $effect(() => {
+ // check that "values" is an array, or set it as array to prevent any errors in springs, or range trimming
+ if (!Array.isArray(values)) {
+ values = [(max + min) / 2];
+ console.error(
+ "'values' prop should be an Array (https://github.com/simeydotme/svelte-range-slider-pips#slider-props)"
+ );
+ }
+
+ // trim the range so it remains as a min/max (only 2 handles)
+ // and also align the handles to the steps
+ const trimmedAlignedValues = trimRange(values.map((v) => alignValueToStep(v)));
+ if (
+ !(values.length === trimmedAlignedValues.length) ||
+ !values.every((element, index) => fixFloat(element) === trimmedAlignedValues[index])
+ ) {
+ values = trimmedAlignedValues;
+ }
+
+ // check if the valueLength (length of values[]) has changed,
+ // because if so we need to re-seed the spring function with the new values array.
+ if (valueLength !== values.length) {
+ // set the initial spring values when the slider initialises, or when values array length has changed
+ springPositions = spring(
+ values.map((v) => percentOf(v)),
+ springOptions
+ );
+ } else {
+ // update the value of the spring function for animated handles whenever the values has updated
+ springPositions.set(values.map((v) => percentOf(v)));
+ }
+ // set the valueLength for the next check
+ valueLength = values.length;
+
+ if (values.length > 1 && !Array.isArray(ariaLabels)) {
+ console.warn(
+ `'ariaLabels' prop should be an Array (https://github.com/simeydotme/svelte-range-slider-pips#slider-props)`
+ );
+ }
+ });
+
+ /**
+ * take in a value, and then calculate that value's percentage
+ * of the overall range (min-max);
+ * @param {number} val the value we're getting percent for
+ * @return {number} the percentage value
+ **/
+ const percentOf = (/** @type {number} */ val: number): number => {
+ let percent = ((val - min) / (max - min)) * 100;
+
+ if (isNaN(percent) || percent <= 0) {
+ return 0;
+ }
+
+ if (percent >= 100) {
+ return 100;
+ }
+
+ return fixFloat(percent);
+ };
+
+ /**
+ * clamp a value from the range so that it always
+ * falls within the min/max values
+ * @param {number} val the value to clamp
+ * @return {number} the value after it's been clamped
+ **/
+ const clampValue = (/** @type {number} */ val: number): number => {
+ // return the min/max if outside of that range
+ return val <= min ? min : val >= max ? max : val;
+ };
+
+ /**
+ * align the value with the steps so that it
+ * always sits on the closest (above/below) step
+ * @param {number} val the value to align
+ * @return {number} the value after it's been aligned
+ **/
+ const alignValueToStep = (/** @type {number} */ val: number): number => {
+ // sanity check for performance
+ if (val <= min) {
+ return fixFloat(min);
+ }
+
+ if (val >= max) {
+ return fixFloat(max);
+ }
+
+ val = fixFloat(val);
+
+ // find the middle-point between steps and see if the value is closer to the next step, or previous step
+ let remainder = (val - min) % step;
+ let aligned = val - remainder;
+
+ if (Math.abs(remainder) * 2 >= step) {
+ aligned += remainder > 0 ? step : -step;
+ }
+
+ aligned = clampValue(aligned); // make sure the value is within acceptable limits
+
+ // make sure the returned value is set to the precision desired
+ // this is also because javascript often returns weird floats
+ // when dealing with odd numbers and percentages
+ return fixFloat(aligned);
+ };
+
+ /**
+ * the orientation of the handles/pips based on the
+ * input values of vertical and reversed
+ * @type {"top"|"bottom"|"left"|"right"} orientationStart
+ **/
+ let orientationStart: 'top' | 'bottom' | 'left' | 'right' = $derived(
+ vertical ? (reversed ? 'top' : 'bottom') : reversed ? 'right' : 'left'
+ );
+ let orientationEnd = $derived(
+ vertical ? (reversed ? 'bottom' : 'top') : reversed ? 'left' : 'right'
+ );
+
+ /**
+ * helper function to get the index of an element in it's DOM container
+ * @param {Element|null} el dom object reference we want the index of
+ * @returns {number} the index of the input element
+ **/
+ function index(el: Element | null): number {
+ if (!el) {
+ return -1;
+ }
+
+ let i = 0;
+ while ((el = el.previousElementSibling)) {
+ i++;
+ }
+ return i;
+ }
+
+ /**
+ * normalise a mouse or touch event to return the
+ * client (x/y) object for that event
+ * @param {MouseEvent|TouchEvent} e a mouse/touch event to normalise
+ * @returns {{ x: number, y: number }} normalised event client object (x,y)
+ **/
+ function normalisedClient(e: MouseEvent | TouchEvent): { x: number; y: number } {
+ if (e.type.includes('touch')) {
+ const touchEvent = e as TouchEvent;
+ const touch = touchEvent.touches[0] || touchEvent.changedTouches[0];
+ return { x: touch.clientX, y: touch.clientY };
+ } else {
+ const mouseEvent = e as MouseEvent;
+ return { x: mouseEvent.clientX, y: mouseEvent.clientY };
+ }
+ }
+
+ /**
+ * check if an element is a handle on the slider
+ * @param {Element} el dom object reference we want to check
+ * @returns {boolean}
+ **/
+ function targetIsHandle(el: Element): boolean {
+ if (!slider) return false;
+ const handles = [...slider.querySelectorAll('.handle')];
+ const isHandle = handles.includes(el);
+ const isChild = handles.some((handle) => handle.contains(el));
+ return isHandle || isChild;
+ }
+
+ /**
+ * trim the values array based on whether the property
+ * for 'range' is 'min', 'max', or truthy. This is because we
+ * do not want more than one handle for a min/max range, and we do
+ * not want more than two handles for a true range.
+ * @param {number[]} values the input values for the rangeSlider
+ * @return {number[]} the range array for creating a rangeSlider
+ **/
+ function trimRange(values: number[]): number[] {
+ if (range === 'min' || range === 'max') {
+ return values.slice(0, 1);
+ }
+ if (range) {
+ return values.slice(0, 2);
+ }
+
+ return values;
+ }
+
+ /**
+ * helper to return the slider dimensions for finding
+ * the closest handle to user interaction
+ * @return {DOMRect} the range slider DOM client rect
+ **/
+ function getSliderDimensions(): DOMRect | undefined {
+ return slider?.getBoundingClientRect();
+ }
+
+ /**
+ * helper to return closest handle to user interaction
+ * @param {{ x: number, y: number }} clientPos the client{x,y} positions to check against
+ * @return {number} the index of the closest handle to clientPos
+ **/
+ function getClosestHandle(clientPos: { x: number; y: number }): number {
+ // first make sure we have the latest dimensions
+ // of the slider, as it may have changed size
+ const dims = getSliderDimensions();
+ if (!dims) throw new Error('No Slider Dimensions yet.');
+ // calculate the interaction position, percent and value
+ let handlePos = 0;
+ let handlePercent = 0;
+ let handleVal = 0;
+ if (vertical) {
+ handlePos = clientPos.y - dims.top;
+ handlePercent = (handlePos / dims.height) * 100;
+ handlePercent = reversed ? handlePercent : 100 - handlePercent;
+ } else {
+ handlePos = clientPos.x - dims.left;
+ handlePercent = (handlePos / dims.width) * 100;
+ handlePercent = reversed ? 100 - handlePercent : handlePercent;
+ }
+ handleVal = ((max - min) / 100) * handlePercent + min;
+
+ // if we have a range, and the handles are at the same
+ // position, we want a simple check if the interaction
+ // value is greater than return the second handle
+ if (range === true && values[0] === values[1]) {
+ if (handleVal > values[1]) {
+ return 1;
+ }
+
+ return 0;
+
+ // if there are multiple handles, and not a range, then
+ // we sort the handles values, and return the first one closest
+ // to the interaction value
+ }
+
+ return values.indexOf(
+ [...values].sort((a, b) => Math.abs(handleVal - a) - Math.abs(handleVal - b))[0]
+ );
+ }
+
+ /**
+ * take the interaction position on the slider, convert
+ * it to a value on the range, and then send that value
+ * through to the moveHandle() method to set the active
+ * handle's position
+ * @param {{ x: number, y: number }} clientPos the client{x,y} of the interaction
+ **/
+ function handleInteract(clientPos: { x: number; y: number }) {
+ // first make sure we have the latest dimensions
+ // of the slider, as it may have changed size
+ const dims = getSliderDimensions();
+ if (!dims) throw new Error('No Slider Dimensions yet.');
+ // calculate the interaction position, percent and value
+ let handlePos = 0;
+ let handlePercent = 0;
+ let handleVal = 0;
+ if (vertical) {
+ handlePos = clientPos.y - dims.top;
+ handlePercent = (handlePos / dims.height) * 100;
+ handlePercent = reversed ? handlePercent : 100 - handlePercent;
+ } else {
+ handlePos = clientPos.x - dims.left;
+ handlePercent = (handlePos / dims.width) * 100;
+ handlePercent = reversed ? 100 - handlePercent : handlePercent;
+ }
+ handleVal = ((max - min) / 100) * handlePercent + min;
+ // move handle to the value
+ moveHandle(activeHandle, handleVal);
+ }
+
+ let lastSetValue = NaN;
+ /**
+ * move a handle to a specific value, respecting the clamp/align rules
+ * @param {number} index the index of the handle we want to move
+ * @param {number} handleValue the value to move the handle to
+ * @return {number} the value that was moved to (after alignment/clamping)
+ **/
+ function moveHandle(index: number | undefined, handleValue: number): number {
+ // align & clamp the value so we're not doing extra
+ // calculation on an out-of-range value down below
+ handleValue = alignValueToStep(handleValue);
+ // use the active handle if handle index is not provided
+ if (typeof index === 'undefined') {
+ index = activeHandle;
+ }
+ // if this is a range slider perform special checks
+ if (range) {
+ // restrict the handles of a range-slider from
+ // going past one-another unless "pushy" is true
+ if (index === 0 && handleValue > values[1]) {
+ if (pushy) {
+ values[1] = handleValue;
+ } else {
+ handleValue = values[1];
+ }
+ } else if (index === 1 && handleValue < values[0]) {
+ if (pushy) {
+ values[0] = handleValue;
+ } else {
+ handleValue = values[0];
+ }
+ }
+ }
+
+ // if the value has changed, update it
+ if (values[index] !== handleValue) {
+ values[index] = handleValue;
+ }
+
+ // fire the change event when the handle moves,
+ // and store the previous value for the next time
+ if (previousValue !== handleValue) {
+ handleOnChange();
+ previousValue = handleValue;
+ }
+ lastSetValue = handleValue;
+ value = handleValue;
+ return handleValue;
+ }
+ $effect(() => {
+ if (value !== lastSetValue) moveHandle(undefined, value);
+ });
+
+ /**
+ * helper to find the beginning range value for use with css style
+ * @param {number[]} values the input values for the rangeSlider
+ * @return {number} the beginning of the range
+ **/
+ function rangeStart(values: number[]): number {
+ if (range === 'min') {
+ return 0;
+ }
+
+ return values[0];
+ }
+
+ /**
+ * helper to find the ending range value for use with css style
+ * @param {array} values the input values for the rangeSlider
+ * @return {number} the end of the range
+ **/
+ function rangeEnd(values: Array<any>): number {
+ if (range === 'max') {
+ return 0;
+ }
+
+ if (range === 'min') {
+ return 100 - values[0];
+ }
+
+ return 100 - values[1];
+ }
+
+ /**
+ * helper to take a string of html and return only the text
+ * @param {string} possibleHtml the string that may contain html
+ * @return {string} the text from the input
+ */
+ function pureText(possibleHtml: string): string {
+ return `${possibleHtml}`.replace(/<[^>]*>/g, '');
+ }
+
+ /**
+ * when the user has unfocussed (blurred) from the
+ * slider, deactivate all handles
+ **/
+ function sliderBlurHandle() {
+ if (!keyboardActive) {
+ return;
+ }
+
+ focus = false;
+ handleActivated = false;
+ handlePressed = false;
+ }
+
+ /**
+ * when the user focusses the handle of a slider
+ * set it to be active
+ * @param {Event} e the event from browser
+ **/
+ function sliderFocusHandle(e: Event) {
+ if (disabled) {
+ return;
+ }
+
+ const target = e.target as HTMLElement;
+ activeHandle = index(target);
+ focus = true;
+ }
+
+ /**
+ * handle the keyboard accessible features by checking the
+ * input type, and modfier key then moving handle by appropriate amount
+ * @param {KeyboardEvent} e the event from browser
+ **/
+ function sliderKeydown(e: KeyboardEvent) {
+ if (disabled) {
+ return;
+ }
+
+ const target = e.target as HTMLElement;
+ const handle = index(target);
+ let jump = e.ctrlKey || e.metaKey || e.shiftKey ? step * 10 : step;
+ let prevent = false;
+
+ if (e.key === 'PageDown' || e.key === 'PageUp') {
+ jump *= 10;
+ }
+
+ if (e.key === 'ArrowRight' || e.key === 'ArrowUp') {
+ moveHandle(handle, values[handle] + jump);
+ prevent = true;
+ }
+
+ if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') {
+ moveHandle(handle, values[handle] - jump);
+ prevent = true;
+ }
+
+ if (e.key === 'Home') {
+ moveHandle(handle, min);
+ prevent = true;
+ }
+
+ if (e.key === 'End') {
+ moveHandle(handle, max);
+ prevent = true;
+ }
+
+ if (prevent) {
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ }
+
+ /**
+ * function to run when the user touches the slider element anywhere
+ * @param {MouseEvent|TouchEvent} e the event from browser
+ **/
+ function sliderInteractStart(e: MouseEvent | TouchEvent) {
+ if (disabled) {
+ return;
+ }
+
+ const element = e.target as HTMLElement;
+ const clientPos = normalisedClient(e);
+ // set the closest handle as active
+ focus = true;
+ handleActivated = true;
+ handlePressed = true;
+ activeHandle = getClosestHandle(clientPos);
+
+ // fire the start event
+ startValue = previousValue = alignValueToStep(values[activeHandle]);
+ handleOnStart();
+
+ // for touch devices we want the handle to instantly
+ // move to the position touched for more responsive feeling
+ if (e.type === 'touchstart' && !element.matches('.pipVal')) {
+ handleInteract(clientPos);
+ }
+ }
+
+ /**
+ * function to run when the user stops touching
+ * down on the slider element anywhere
+ * @param {Event} e the event from browser
+ **/
+ function sliderInteractEnd(e: Event) {
+ // fire the stop event for touch devices
+ if (e.type === 'touchend') {
+ handleOnStop();
+ }
+ handlePressed = false;
+ }
+
+ /**
+ * unfocus the slider if the user clicked off of
+ * it, somewhere else on the screen
+ * @param {MouseEvent|TouchEvent} e the event from browser
+ **/
+ function bodyInteractStart(e: MouseEvent | TouchEvent) {
+ keyboardActive = false;
+ const target = e.target as HTMLElement;
+ if (slider && focus && e.target !== slider && !slider.contains(target)) {
+ focus = false;
+ }
+ }
+
+ /**
+ * send the clientX through to handle the interaction
+ * whenever the user moves across screen while active
+ * @param {MouseEvent|TouchEvent} e the event from browser
+ **/
+ function bodyInteract(e: MouseEvent | TouchEvent) {
+ if (!disabled) {
+ if (handleActivated) {
+ handleInteract(normalisedClient(e));
+ }
+ }
+ }
+
+ /**
+ * if user triggers mouseup on the body while
+ * a handle is active (without moving) then we
+ * trigger an interact event there
+ * @param {MouseEvent|TouchEvent} e the event from browser
+ **/
+ function bodyMouseUp(e: MouseEvent | TouchEvent) {
+ if (!disabled) {
+ const el = e.target as HTMLElement;
+ // this only works if a handle is active, which can
+ // only happen if there was sliderInteractStart triggered
+ // on the slider, already
+ if (handleActivated) {
+ if (slider && (el === slider || slider.contains(el))) {
+ focus = true;
+ // don't trigger interact if the target is a handle (no need) or
+ // if the target is a label (we want to move to that value from rangePips)
+ if (!targetIsHandle(el) && !el.matches('.pipVal')) {
+ handleInteract(normalisedClient(e));
+ }
+ }
+ // fire the stop event for mouse device
+ // when the body is triggered with an active handle
+ handleOnStop();
+ }
+ }
+ handleActivated = false;
+ handlePressed = false;
+ }
+
+ /**
+ * @param {KeyboardEvent} e
+ */
+ function bodyKeyDown(e: KeyboardEvent) {
+ if (disabled) {
+ return;
+ }
+
+ const target = e.target as HTMLElement;
+
+ if (slider && (e.target === slider || slider.contains(target))) {
+ keyboardActive = true;
+ }
+ }
+
+ function handleOnStop() {
+ if (disabled || !onstop || typeof onstop !== 'function') {
+ return;
+ }
+
+ onstop({
+ activeHandle,
+ startValue: startValue ?? 0,
+ value: values[activeHandle],
+ values: values.map((v) => alignValueToStep(v))
+ });
+ }
+
+ function handleOnStart() {
+ if (disabled || !onstart || typeof onstart !== 'function') {
+ return;
+ }
+
+ onstart({
+ activeHandle,
+ value: startValue ?? 0,
+ values: values.map((v) => alignValueToStep(v))
+ });
+ }
+
+ function handleOnChange() {
+ if (disabled || !onchange || typeof onchange !== 'function') {
+ return;
+ }
+
+ onchange({
+ activeHandle,
+ startValue: startValue ?? 0,
+ previousValue: typeof previousValue === 'undefined' ? (startValue ?? 0) : previousValue,
+ value: values[activeHandle],
+ values: values.map((v) => alignValueToStep(v))
+ });
+ }
+
+ /** @type {import('svelte/motion').Spring<number[]>} */
+ let springPositions: import('svelte/motion').Spring<number[]> = spring(
+ values.map((v) => percentOf(v)),
+ springOptions
+ );
+</script>
+
+<div
+ {id}
+ bind:this={slider}
+ role="none"
+ class="_rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28"
+ class:range
+ class:disabled
+ class:hoverable
+ class:vertical
+ class:reversed
+ class:focus
+ class:min={range === 'min'}
+ class:max={range === 'max'}
+ class:pips
+ class:pip-labels={all === 'label' || first === 'label' || last === 'label' || rest === 'label'}
+ onmousedown={sliderInteractStart}
+ onmouseup={sliderInteractEnd}
+>
+ {#each values as value, index}
+ <span
+ role="slider"
+ class="rangeHandle"
+ class:active={focus && activeHandle === index}
+ class:press={handlePressed && activeHandle === index}
+ data-handle={index}
+ onblur={sliderBlurHandle}
+ onfocus={sliderFocusHandle}
+ onkeydown={sliderKeydown}
+ style="{orientationStart}: {$springPositions[index]}%; z-index: {activeHandle === index
+ ? 3
+ : 2};"
+ aria-label={ariaLabels[index]}
+ aria-valuemin={range === true && index === 1 ? values[0] : min}
+ aria-valuemax={range === true && index === 0 ? values[1] : max}
+ aria-valuenow={value}
+ aria-valuetext="{prefix}{pureText(handleFormatter(value, index, percentOf(value)))}{suffix}"
+ aria-orientation={vertical ? 'vertical' : 'horizontal'}
+ aria-disabled={disabled}
+ tabindex={disabled ? -1 : 0}
+ >
+ <span class="rangeNub"></span>
+ {#if float}
+ <span class="rangeFloat">
+ {prefix}{handleFormatter(value, index, percentOf(value))}{suffix}
+ </span>
+ {/if}
+ </span>
+ {/each}
+
+ {#if range}
+ <span
+ class="rangeBar"
+ style="{orientationStart}: {rangeStart($springPositions)}%;
+ {orientationEnd}: {rangeEnd($springPositions)}%;"
+ ></span>
+ {/if}
+
+ {#if pips}
+ <RangePips
+ {values}
+ {min}
+ {max}
+ {step}
+ {range}
+ {vertical}
+ {reversed}
+ {orientationStart}
+ {hoverable}
+ {disabled}
+ {all}
+ {first}
+ {last}
+ {rest}
+ {pipstep}
+ {prefix}
+ {suffix}
+ {formatter}
+ {focus}
+ {percentOf}
+ {moveHandle}
+ {fixFloat}
+ {normalisedClient}
+ />
+ {/if}
+</div>
+
+<svelte:window
+ onmousedown={bodyInteractStart}
+ onmousemove={bodyInteract}
+ onmouseup={bodyMouseUp}
+ onkeydown={bodyKeyDown}
+/>
+
+<style>
+ :global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28) {
+ --slider: var(--range-slider, #d7dada);
+ --handle-inactive: var(--range-handle-inactive, #99a2a2);
+ --handle: var(--range-handle, #838de7);
+ --handle-focus: var(--range-handle-focus, #4a40d4);
+ --handle-border: var(--range-handle-border, var(--handle));
+ --range-inactive: var(--range-range-inactive, var(--handle-inactive));
+ --range: var(--range-range, var(--handle-focus));
+ --float-inactive: var(--range-float-inactive, var(--handle-inactive));
+ --float: var(--range-float, var(--handle-focus));
+ --float-text: var(--range-float-text, white);
+ }
+ :global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28) {
+ position: relative;
+ border-radius: 100px;
+ height: 0.5em;
+ margin: 1em;
+ transition: opacity 0.2s ease;
+ user-select: none;
+ }
+ :global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28 *) {
+ user-select: none;
+ }
+ :global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.pips) {
+ margin-bottom: 1.8em;
+ }
+ :global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.pip-labels) {
+ margin-bottom: 2.8em;
+ }
+ :global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.vertical) {
+ display: inline-block;
+ border-radius: 100px;
+ width: 0.5em;
+ min-height: 200px;
+ }
+ :global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.vertical.pips) {
+ margin-right: 1.8em;
+ margin-bottom: 1em;
+ }
+ :global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.vertical.pip-labels) {
+ margin-right: 2.8em;
+ margin-bottom: 1em;
+ }
+ :global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28 .rangeHandle) {
+ position: absolute;
+ display: block;
+ height: 1.4em;
+ width: 1.4em;
+ top: 0.25em;
+ bottom: auto;
+ transform: translateY(-50%) translateX(-50%);
+ z-index: 2;
+ }
+ :global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.reversed .rangeHandle) {
+ transform: translateY(-50%) translateX(50%);
+ }
+ :global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.vertical .rangeHandle) {
+ left: 0.25em;
+ top: auto;
+ transform: translateY(50%) translateX(-50%);
+ }
+ :global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.vertical.reversed .rangeHandle) {
+ transform: translateY(-50%) translateX(-50%);
+ }
+ :global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28 .rangeNub),
+ :global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28 .rangeHandle:before) {
+ position: absolute;
+ left: 0;
+ top: 0;
+ display: block;
+ border-radius: 10em;
+ height: 100%;
+ width: 100%;
+ transition: box-shadow 0.2s ease;
+ }
+ :global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28 .rangeHandle:before) {
+ content: '';
+ left: 1px;
+ top: 1px;
+ bottom: 1px;
+ right: 1px;
+ height: auto;
+ width: auto;
+ box-shadow: 0 0 0 0px var(--handle-border);
+ opacity: 0;
+ }
+ :global(
+ ._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.hoverable:not(.disabled)
+ .rangeHandle:hover:before
+ ) {
+ box-shadow: 0 0 0 8px var(--handle-border);
+ opacity: 0.2;
+ }
+ :global(
+ ._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.hoverable:not(.disabled)
+ .rangeHandle.press:before
+ ),
+ :global(
+ ._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.hoverable:not(.disabled)
+ .rangeHandle.press:hover:before
+ ) {
+ box-shadow: 0 0 0 12px var(--handle-border);
+ opacity: 0.4;
+ }
+ :global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.range:not(.min):not(.max) .rangeNub) {
+ border-radius: 10em 10em 10em 1.6em;
+ }
+ :global(
+ ._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.range .rangeHandle:nth-of-type(1) .rangeNub
+ ) {
+ transform: rotate(-135deg);
+ }
+ :global(
+ ._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.range .rangeHandle:nth-of-type(2) .rangeNub
+ ) {
+ transform: rotate(45deg);
+ }
+ :global(
+ ._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.range.reversed
+ .rangeHandle:nth-of-type(1)
+ .rangeNub
+ ) {
+ transform: rotate(45deg);
+ }
+ :global(
+ ._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.range.reversed
+ .rangeHandle:nth-of-type(2)
+ .rangeNub
+ ) {
+ transform: rotate(-135deg);
+ }
+ :global(
+ ._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.range.vertical
+ .rangeHandle:nth-of-type(1)
+ .rangeNub
+ ) {
+ transform: rotate(135deg);
+ }
+ :global(
+ ._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.range.vertical
+ .rangeHandle:nth-of-type(2)
+ .rangeNub
+ ) {
+ transform: rotate(-45deg);
+ }
+ :global(
+ ._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.range.vertical.reversed
+ .rangeHandle:nth-of-type(1)
+ .rangeNub
+ ) {
+ transform: rotate(-45deg);
+ }
+ :global(
+ ._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.range.vertical.reversed
+ .rangeHandle:nth-of-type(2)
+ .rangeNub
+ ) {
+ transform: rotate(135deg);
+ }
+ :global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28 .rangeFloat) {
+ display: block;
+ position: absolute;
+ left: 50%;
+ top: -0.5em;
+ transform: translate(-50%, -100%);
+ font-size: 1em;
+ text-align: center;
+ opacity: 0;
+ pointer-events: none;
+ white-space: nowrap;
+ transition: all 0.2s ease;
+ font-size: 0.9em;
+ padding: 0.2em 0.4em;
+ border-radius: 0.2em;
+ }
+ :global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28 .rangeHandle.active .rangeFloat),
+ :global(
+ ._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.hoverable .rangeHandle:hover .rangeFloat
+ ) {
+ opacity: 1;
+ top: -0.2em;
+ transform: translate(-50%, -100%);
+ }
+ :global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28 .rangeBar) {
+ position: absolute;
+ display: block;
+ transition: background 0.2s ease;
+ border-radius: 1em;
+ height: 0.5em;
+ top: 0;
+ user-select: none;
+ z-index: 1;
+ }
+ :global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.vertical .rangeBar) {
+ width: 0.5em;
+ height: auto;
+ }
+ :global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28) {
+ background-color: var(--slider, #d7dada);
+ }
+ :global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28 .rangeBar) {
+ background-color: var(--range-inactive, #99a2a2);
+ }
+ :global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.focus .rangeBar) {
+ background-color: var(--range, #838de7);
+ }
+ :global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28 .rangeNub) {
+ background-color: var(--handle-inactive, #99a2a2);
+ }
+ :global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.focus .rangeNub) {
+ background-color: var(--handle, #838de7);
+ }
+ :global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28 .rangeHandle.active .rangeNub) {
+ background-color: var(--handle-focus, #4a40d4);
+ }
+ :global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28 .rangeFloat) {
+ color: white;
+ color: var(--float-text);
+ background-color: var(--float-inactive, #99a2a2);
+ }
+ :global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.focus .rangeFloat) {
+ background-color: var(--float, #4a40d4);
+ }
+ :global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.disabled) {
+ opacity: 0.5;
+ }
+ :global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.disabled .rangeNub) {
+ background-color: var(--slider, #d7dada);
+ }
+</style>