Skip to content

Instantly share code, notes, and snippets.

@erikvullings
Last active February 23, 2024 10:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save erikvullings/6dc68b4c5b8d3f7c90e8afedb3b2a6a4 to your computer and use it in GitHub Desktop.
Save erikvullings/6dc68b4c5b8d3f7c90e8afedb3b2a6a4 to your computer and use it in GitHub Desktop.
Mithril double-range slider

A double range slider for mithril

You can specify the minimum and maximum value, the selected range ([min: number, max: number]), and an optional rangeMin distance between the two. A playground is available in Flems.

image

Example usage

const min = 0;
const max = 1000;
let range: [number, number] = [300, 700];

const onchange = (newRange: [number, number]) => {
    range = newRange;
};

m.mount(document.body, {
    view: () => m(DoubleRangeSlider, { min, max, range, onchange })
});
.double-range-slider > .range-slider {
margin: 0;
height: 8px;
position: relative;
background-color: #e1e9f6;
border-radius: 2px;
}
.double-range-slider > .range-slider > .range-selected {
height: 100%;
position: absolute;
border-radius: 5px;
background-color: #1b53c0;
z-index: 1;
}
.double-range-slider > .range-input {
position: relative;
}
.double-range-slider > .range-input > input[type='range'] {
position: absolute;
width: 100%;
height: 4px;
top: -8px;
left: -2px;
margin: 0;
padding: 0;
border: none;
background: none;
pointer-events: none;
cursor: pointer;
z-index: 1;
appearance: none;
background: transparent;
/* width: 15rem; */
-webkit-appearance: none;
-moz-appearance: none;
}
.double-range-slider > .range-input > input[type='range']::-webkit-slider-thumb {
height: 20px;
width: 20px;
border-radius: 50%;
border: 10px solid #1b53c0;
background-color: #fff;
pointer-events: auto;
-webkit-appearance: none;
cursor: pointer;
z-index: 2;
}
.double-range-slider > .range-input > input[type='range']::-moz-range-thumb {
height: 15px;
width: 15px;
border-radius: 50%;
border: 3px solid #1b53c0;
background-color: #fff;
pointer-events: auto;
-moz-appearance: none;
z-index: 2;
}
.double-range-slider input[type='range']::-webkit-slider-runnable-track {
margin: 0 -2px;
background: none;
height: 8px;
}
.double-range-slider input[type='range']::-moz-range-track {
margin: 0 -2px;
background: none;
height: 8px;
}
.double-range-slider > .range-value {
margin: 0 0 10px 0;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.double-range-slider > .range-value label {
margin-right: 5px;
}
.double-range-slider > .range-value input {
text-align: center;
width: 60px;
padding: 5px;
}
.double-range-slider > .range-value input:first-of-type {
margin-right: 15px;
}
import m, { FactoryComponent, Attributes } from 'mithril';
export type MinMax = [min: number, max: number];
export interface DoubleRangeSliderAttrs extends Attributes {
/** Minimum value */
min: number;
/** Maximum value */
max: number;
/** Selected range */
range?: MinMax;
/** Step size, @default 1 */
step?: number;
/** Minimum value between selected minimum and maximum, e.g. to avoid the user selecting a range of 5-5, @default 1 */
rangeMin?: number;
/** Onchange handler */
onchange?: (range: MinMax) => void;
}
export const DoubleRangeSlider: FactoryComponent<DoubleRangeSliderAttrs> = () => {
let rangeElement: HTMLSpanElement;
let minRangeInput: HTMLInputElement;
let maxRangeInput: HTMLInputElement;
let minValueInput: HTMLInputElement;
let maxValueInput: HTMLInputElement;
let range: MinMax = [0, 0];
return {
oninit: ({ attrs }) => {
const { min, max } = attrs;
range = attrs.range || [min, max];
},
view: ({ attrs }) => {
const { min, max, onchange, className, step = 1, rangeMin = 1 } = attrs;
const handleInput = (e: Event) => {
const isMin = (e.target as HTMLInputElement).classList.contains('min');
const minRange = parseInt(minRangeInput.value);
const maxRange = parseInt(maxRangeInput.value);
if (maxRange - minRange < rangeMin) {
if (isMin) {
minRangeInput.value = (maxRange - rangeMin).toString();
range = [maxRange - rangeMin, maxRange];
} else {
maxRangeInput.value = (minRange + rangeMin).toString();
range = [minRange, minRange + rangeMin];
}
} else {
minValueInput.value = minRange.toString();
maxValueInput.value = maxRange.toString();
range = [minRange, maxRange];
}
onchange && onchange(range);
};
const handleValueInput = (e: Event) => {
const isMin = (e.target as HTMLInputElement).classList.contains('min');
const minValue = parseInt(minValueInput.value);
const maxValue = parseInt(maxValueInput.value);
if (maxValue - minValue >= rangeMin && maxValue <= max) {
if (isMin) {
minRangeInput.value = minValue.toString();
} else {
maxRangeInput.value = maxValue.toString();
}
}
range = [minValue, maxValue];
onchange && onchange(range);
};
return m('.double-range-slider', { className }, [
m('.range-slider', [
m('span.range-selected', {
style: {
left: `${((range[0] - min) / (max - min)) * 100}%`,
right: `${100 - ((range[1] - min) / (max - min)) * 100}%`,
},
oncreate: ({ dom }) => (rangeElement = dom as HTMLSpanElement),
}),
]),
m('.range-input', [
m('input[type=range].min', {
min,
max,
value: range[0],
step,
oninput: handleInput,
oncreate: ({ dom }) => (minRangeInput = dom as HTMLInputElement),
}),
m('input[type=range].max', {
min,
max,
value: range[1],
step,
oninput: handleInput,
oncreate: ({ dom }) => (maxRangeInput = dom as HTMLInputElement),
}),
]),
m('.range-value', [
m('label', { for: 'min' }, 'Min'),
m('input[type=number][name=min]', {
min,
max,
value: range[0],
oninput: handleValueInput,
oncreate: ({ dom }) => (minValueInput = dom as HTMLInputElement),
}),
m('label', { for: 'max' }, 'Max'),
m('input[type=number][name=max]', {
min,
max,
value: range[1],
oninput: handleValueInput,
oncreate: ({ dom }) => (maxValueInput = dom as HTMLInputElement),
}),
]),
]);
},
};
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment