TirayaFrontend/src/lib/components/ui/Slider.svelte

225 lines
5.6 KiB
Svelte

<!-- Source: https://svelte.dev/repl/7f0042a186ee4d8e949c46ca663dbe6c?version=3.33.0 -->
<script lang="ts">
import { createEventDispatcher } from "svelte";
import { kbd } from "@melt-ui/svelte/internal/helpers";
import { clamp } from "$lib/util/functions";
// Props
export let min = 0;
export let max = 100;
export let value = 0;
export let ariaLabel: string | undefined = undefined;
export let marginX = true;
/**
* Value to be displayed next to the slider
* (current position while dragging, actual value otherwise)
*/
export let displayValue = value;
/** Update value in real time while dragging */
export let liveUpdate = false;
// Node bindings
let element: HTMLElement;
// Internal state
let elementRect: DOMRect | null;
let holding = false;
let keydownAcceleration = 0;
let accelerationTimer: null | number = null;
let valueTmp = value;
let percent = 0;
// Dispatch 'change' events
const dispatch = createEventDispatcher();
function resizeSlider() {
elementRect = element.getBoundingClientRect();
}
// Allows both bind:value and on:change for parent value retrieval
function setValue(newValue: number) {
value = newValue;
dispatch("change", { value });
}
function onTrackEvent(e: Event) {
// Update value immediately before beginning drag
updateValueOnEvent(e);
onDragStart();
}
function onDragStart() {
holding = true;
}
function onDragEnd() {
if (holding) {
setValue(valueTmp);
holding = false;
}
}
// Accessible keypress handling
function onKeyPress(e: KeyboardEvent) {
// Max out at +/- 10 to value per event (50 events / 5)
// 100 below is to increase the amount of events required to reach max velocity
if (keydownAcceleration < 50) keydownAcceleration++;
let throttled = Math.ceil(keydownAcceleration / 5);
if (e.key === kbd.ARROW_UP || e.key === kbd.ARROW_RIGHT) {
if (value + throttled > max || value >= max) {
setValue(max);
} else {
setValue(value + throttled);
}
}
if (e.key === kbd.ARROW_DOWN || e.key === kbd.ARROW_LEFT) {
if (value - throttled < min || value <= min) {
setValue(min);
} else {
setValue(value - throttled);
}
}
// Reset acceleration after 100ms of no events
if (accelerationTimer !== null) {
clearTimeout(accelerationTimer);
}
accelerationTimer = window.setTimeout(() => (keydownAcceleration = 1), 100);
}
function calculateNewValue(clientX: number) {
if (elementRect === null) return;
// Find distance between cursor and element's left cord
let delta = clientX - elementRect.x;
let percent = (delta * 100) / elementRect.width;
// Limit percent 0 -> 100
percent = percent < 0 ? 0 : percent > 100 ? 100 : percent;
// Limit value min -> max
valueTmp = Math.round((percent * (max - min)) / 100 + min);
if (liveUpdate) {
setValue(valueTmp);
}
}
// Handles both dragging of touch/mouse as well as simple one-off click/touches
function updateValueOnEvent(e: Event) {
// touchstart && mousedown are one-off updates, otherwise expect a currentPointer node
if (!holding && e.type !== "touchstart" && e.type !== "mousedown") return false;
if (e.stopPropagation) e.stopPropagation();
if (e.preventDefault) e.preventDefault();
// Get client's x cord either touch or mouse
const coords =
e.type === "touchmove" || e.type === "touchstart"
? (e as TouchEvent).touches[0]
: (e as MouseEvent);
calculateNewValue(coords.clientX);
}
// React to left position of element relative to window
$: if (element) elementRect = element.getBoundingClientRect();
$: displayValue = holding ? valueTmp : value;
$: percent = ((clamp(displayValue, min, max) - min) * 100) / (max - min);
</script>
<svelte:window
on:touchmove|nonpassive={updateValueOnEvent}
on:touchcancel={onDragEnd}
on:touchend={onDragEnd}
on:mousemove={updateValueOnEvent}
on:mouseup={onDragEnd}
on:resize={resizeSlider}
/>
<div class="flex-1" class:mx-2={marginX}>
<div
class="slider-wrapper"
class:holding
tabindex="0"
on:keydown={onKeyPress}
bind:this={element}
role="slider"
aria-label={ariaLabel}
aria-valuemin={min}
aria-valuemax={max}
aria-valuenow={value}
on:mousedown={onTrackEvent}
on:touchstart|passive={onTrackEvent}
style={`--slider-transform:${percent}%;`}
>
<div class="slider-bg slider-shape">
<div class="slider-shape overflow-hidden">
<div class="slider-fill slider-shape" />
</div>
<div class="slider-thumb" />
</div>
</div>
</div>
<style lang="postcss">
.slider-wrapper {
min-width: 100%;
height: 1em;
position: relative;
&:hover,
&:focus-visible,
&.holding {
& .slider-fill {
@apply bg-primary;
}
& .slider-thumb {
display: block;
}
}
}
.slider-bg {
@apply bg-base-content bg-opacity-40;
display: flex;
position: absolute;
top: 50%;
transform: translateY(-50%);
}
.slider-shape {
width: 100%;
height: 4px;
border-radius: 2px;
}
.slider-fill {
@apply bg-base-content;
transform: translateX(calc(-100% + var(--slider-transform)));
}
.slider-thumb {
@apply bg-base-content;
display: none;
position: absolute;
top: 50%;
transform: translateY(-50%);
height: 12px;
width: 12px;
border: 0;
border-radius: 50%;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.5);
left: var(--slider-transform);
margin-left: -6px;
}
</style>