Files
cc-backend/web/frontend/src/generic/select/DoubleRangeSlider.svelte

274 lines
7.3 KiB
Svelte

<!--
Copyright (c) 2021 Michael Keller
Originally created by Michael Keller (https://github.com/mhkeller/svelte-double-range-slider)
Changes: remove dependency, text inputs, configurable value ranges, on:change event
Changes #2: Rewritten for Svelte 5, removed bodyHandler
-->
<!--
@component Selector component to display range selections via min and max double-sliders
Properties:
- `sliderMin Number!`: Minimum possible value for slider
- `sliderMax Number!`: Maximum possible value for slider
- `fromPreset Number?`: Latest "from" value selection [Default: 1]
- `toPreset Number?`: Latest "to" value selection [Default: 100]
- `changeRange Func`: The callback function to apply current range selections
-->
<script>
/* Svelte 5 Props */
let {
sliderMin,
sliderMax,
fromPreset = 1,
toPreset = 100,
changeRange
} = $props();
/* State Init */
let leftHandle = $state();
let sliderMain = $state();
/* Derived */
let pendingValues = $derived([fromPreset, toPreset]);
let sliderFrom = $derived(Math.max(((fromPreset == null ? sliderMin : fromPreset) - sliderMin) / (sliderMax - sliderMin), 0.));
let sliderTo = $derived(Math.min(((toPreset == null ? sliderMin : toPreset) - sliderMin) / (sliderMax - sliderMin), 1.));
let inputFieldFrom = $derived(fromPreset ? fromPreset.toString() : null);
let inputFieldTo = $derived(toPreset ? toPreset.toString() : null);
/* Var Init */
let timeoutId = null;
/* Functions */
function queueChangeEvent() {
if (timeoutId !== null) {
clearTimeout(timeoutId)
}
timeoutId = setTimeout(() => {
timeoutId = null
changeRange(pendingValues);
}, 100);
}
function updateStates(newValue, newPosition, target) {
if (target === 'from') {
pendingValues[0] = isNaN(newValue) ? null : newValue;
inputFieldFrom = isNaN(newValue) ? null : newValue.toString();
sliderFrom = newPosition;
} else if (target === 'to') {
pendingValues[1] = isNaN(newValue) ? null : newValue;
inputFieldTo = isNaN(newValue) ? null : newValue.toString();
sliderTo = newPosition;
}
queueChangeEvent();
}
function rangeChanged (evt, target) {
evt.preventDefault()
evt.stopPropagation()
const { left, right } = sliderMain.getBoundingClientRect();
const parentWidth = right - left;
const newP = Math.min(Math.max((evt.detail.x - left) / parentWidth, 0), 1);
const newV = Math.floor(sliderMin + newP * (sliderMax - sliderMin));
updateStates(newV, newP, target);
}
function inputChanged(evt, target) {
evt.preventDefault()
evt.stopPropagation()
const newV = Number.parseInt(evt.target.value);
const newP = clamp((newV - sliderMin) / (sliderMax - sliderMin), 0., 1.)
updateStates(newV, newP, target);
}
function clamp(x, testMin, testMax) {
return x < testMin
? testMin
: (x > testMax
? testMax
: x
);
}
function draggable(node) {
let x;
let y;
function handleMousedown(event) {
if (event.type === 'touchstart') {
event = event.touches[0];
}
x = event.clientX;
y = event.clientY;
node.dispatchEvent(new CustomEvent('dragstart', {
detail: { x, y }
}));
window.addEventListener('mousemove', handleMousemove);
window.addEventListener('mouseup', handleMouseup);
window.addEventListener('touchmove', handleMousemove);
window.addEventListener('touchend', handleMouseup);
}
function handleMousemove(event) {
if (event.type === 'touchmove') {
event = event.changedTouches[0];
}
const dx = event.clientX - x;
const dy = event.clientY - y;
x = event.clientX;
y = event.clientY;
node.dispatchEvent(new CustomEvent('dragmove', {
detail: { x, y, dx, dy }
}));
}
function handleMouseup(event) {
x = event.clientX;
y = event.clientY;
node.dispatchEvent(new CustomEvent('dragend', {
detail: { x, y }
}));
window.removeEventListener('mousemove', handleMousemove);
window.removeEventListener('mouseup', handleMouseup);
window.removeEventListener('touchmove', handleMousemove);
window.removeEventListener('touchend', handleMouseup);
}
node.addEventListener('mousedown', handleMousedown);
node.addEventListener('touchstart', handleMousedown);
return {
destroy() {
node.removeEventListener('mousedown', handleMousedown);
node.removeEventListener('touchstart', handleMousedown);
}
};
}
</script>
<div class="double-range-container">
<div class="header">
<input class="form-control" type="text" placeholder="from..." value={inputFieldFrom}
oninput={(e) => {
inputChanged(e, 'from');
}}
/>
{#if inputFieldFrom != "1" && inputFieldTo != sliderMax?.toString() }
<span>Selected: Range <b> {inputFieldFrom} </b> - <b> {inputFieldTo} </b></span>
{:else if inputFieldFrom != "1" && inputFieldTo == sliderMax?.toString() }
<span>Selected: More than <b> {inputFieldFrom} </b> </span>
{:else if inputFieldFrom == "1" && inputFieldTo != sliderMax?.toString() }
<span>Selected: Less than <b> {inputFieldTo} </b></span>
{:else}
<span><i>No Selection</i></span>
{/if}
<input class="form-control" type="text" placeholder="to..." value={inputFieldTo}
oninput={(e) => {
inputChanged(e, 'to');
}}
/>
</div>
<div id="slider-active" class="slider" bind:this={sliderMain}>
<div
class="slider-body"
style="left: {100 * sliderFrom}%;right: {100 * (1 - sliderTo)}%;"
></div>
<div
class="slider-handle"
bind:this={leftHandle}
data-which="from"
use:draggable
ondragmove={(e) => rangeChanged(e, 'from')}
style="left: {100 * sliderFrom}%"
></div>
<div
class="slider-handle"
data-which="to"
use:draggable
ondragmove={(e) => rangeChanged(e, 'to')}
style="left: {100 * sliderTo}%"
></div>
</div>
</div>
<style>
.header {
width: 100%;
display: flex;
justify-content: space-between;
align-items: flex-end;
margin-bottom: 5px;
}
.header :nth-child(2) {
padding-top: 10px;
}
.header input {
height: 25px;
border-radius: 5px;
width: 100px;
}
.double-range-container {
width: 100%;
height: 50px;
user-select: none;
box-sizing: border-box;
white-space: nowrap;
margin-top: -4px;
}
.slider {
position: relative;
width: 100%;
height: 6px;
top: 10px;
transform: translate(0, -50%);
background-color: #e2e2e2;
box-shadow: inset 0 7px 10px -5px #4a4a4a, inset 0 -1px 0px 0px #9c9c9c;
border-radius: 6px;
}
.slider-handle {
position: absolute;
top: 50%;
width: 0;
height: 0;
}
.slider-handle:after {
content: ' ';
box-sizing: border-box;
position: absolute;
border-radius: 50%;
width: 16px;
height: 16px;
background-color: #fdfdfd;
border: 1px solid #7b7b7b;
transform: translate(-50%, -50%)
}
/* .handle[data-which="end"]:after{
transform: translate(-100%, -50%);
} */
.slider-handle:active:after {
background-color: #ddd;
z-index: 9;
}
.slider-body {
top: 0;
position: absolute;
background-color: #34a1ff;
bottom: 0;
}
</style>