SELF-Treffen in Mannheim 2025
SELFHTML wird 30 Jahre alt! → Veranstaltungs-Ankündigung.
Beispiel:Min-Max-Schieberegler.html
Aus SELFHTML-Wiki
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Min-Max-Schieberegler</title>
<style>
output {
display: block;
margin-top: 1em;
}
button { font-size: 1em; }
#Test {
double-range-input {
border: thin dotted black;
margin: 1em;
width: 20em;
height: 3em;
display: block;
}
double-range-input[name=dr2] {
--trackInnerColor: #800;
--trackOuterColor: #b55;
--thumbColor: #f00;
--thumbFocusColor: #f64;
--thumbSize: 2em;
--trackHeight: .5em;
box-sizing: border-box;
background-color: darkgray;
border-radius: .5em;
padding: .5em;
}
double-range-input[name=dr4]::part(valuedisplay) {
border: thin solid blue;
padding: .1em;
}
}
#Gebrauchtwagensuche fieldset {
width: fit-content;
* {
display: block;
margin-top: 1em;
box-sizing: border-box;
}
double-range-input {
width: 15em;
height: 3em;
border: thin solid black;
padding: 0 .5em;
&::part(valuedisplay) {
padding: .2em;
}
}
}
</style>
<script type='module'>
'use strict';
// Custom-Element double-range-input anlegen
customElements.define('double-range-input', class extends HTMLElement {
// Einige private Eigenschaften
#style;
#doubleSlider;
#inputLow;
#inputHigh;
#valueDisplay;
#Attributes;
#internals;
// Trennzeichen für value
#valueDelimiter = ',';
// double-range-input soll Formularelement sein
static formAssociated = true;
// Festlegen, welche Attribute überwacht werden sollen
static get observedAttributes() {
return ['min', 'max', 'step', 'showvalues', 'value'];
}
constructor() {
super();
// Zugriff auf die ElementInternals, wird benötigt, um auf die Werte zuzugreifen, die mit dem Formular versendet werden.
this.#internals = this.attachInternals();
// Aria-Rolle slider setzen
this.#internals.ariaRole = 'slider';
// Schatten-DOM anlegen
const shadow = this.attachShadow({mode: 'closed'});
this.#style = document.createElement('style');
shadow.appendChild(this.#style);
this.#doubleSlider = document.createElement('div');
this.#doubleSlider.id = 'wrapper';
this.#doubleSlider.part = 'doubleslider';
shadow.appendChild(this.#doubleSlider);
// Defaultwerte der Attribute setzen
this.#Attributes = {
min: 0,
max: 100,
step: 1,
showvalues: 'ever',
currentValue: `25${this.#valueDelimiter}75`
}
// Eventhandler
this.#doubleSlider.addEventListener('input', this.#handleInput.bind(this));
this.#doubleSlider.addEventListener('pointerdown', this.#handlePointerdown.bind(this));
this.#doubleSlider.addEventListener('pointerover', this.#handlePointerover.bind(this));
this.#doubleSlider.addEventListener('pointerout', this.#handlePointerout.bind(this));
this.#doubleSlider.addEventListener('change', this.#handleChange.bind(this));
}
connectedCallback() {
// CSS und Elemente anlegen
this.#style.innerHTML = this.#css();
this.#doubleSlider.innerHTML = this.#html();
this.#inputLow = this.#doubleSlider.querySelector('#Low');
this.#inputHigh = this.#doubleSlider.querySelector('#High');
this.#valueDisplay = this.#doubleSlider.querySelector('output');
this.#update();
}
attributeChangedCallback(name, oldValue, newValue) {
switch(name) {
case 'min':
this.#Attributes.min = Number(newValue);
if (this.#inputLow) this.#inputLow.min = Number(newValue);
if (this.#inputHigh) this.#inputHigh.min = Number(newValue);
break;
case 'max':
this.#Attributes.max = Number(newValue);
if (this.#inputLow) this.#inputLow.max = Number(newValue);
if (this.#inputHigh) this.#inputHigh.max = Number(newValue);
break;
case 'step':
this.#Attributes.step = Number(newValue);
if (this.#inputLow) this.#inputLow.step = Number(newValue);
if (this.#inputHigh) this.#inputHigh.step = Number(newValue);
break;
case 'value':
this.#setValue(newValue);
break;
case 'showvalues':
if(['ever', 'active', 'never'].includes(newValue))
this.#Attributes.showvalues = newValue;
break;
}
}
get form() {
return this.#internals.form;
}
set name(name) {
this.setAttribute('name', name);
}
get name() {
return this.getAttribute('name');
}
get valueDelimiter() {
return this.#valueDelimiter;
}
set min(min) {
this.setAttribute('min', min);
}
get min() {
return this.#Attributes.min;
}
set max(max) {
this.setAttribute('max', max);
}
get max() {
return this.#Attributes.max;
}
set step(step) {
this.setAttribute('step', step);
}
get step() {
return this.#Attributes.step;
}
set showvalues(showvalues) {
this.setAttribute('showvalues', showvalues);
}
get showvalues() {
return this.#Attributes.showvalues;
}
set value(value) {
this.#setValue(value);
}
get value() {
const valueLow = this.#inputLow.value;
const valueHigh = this.#inputHigh.value;
const currentValue = `${valueLow}${this.#valueDelimiter}${valueHigh}`;
return currentValue;
}
#setValue(value) {
if(value.split(this.#valueDelimiter).length == 2) {
let valueLow = Number(value.split(this.#valueDelimiter)[0]);
let valueHigh = Number(value.split(this.#valueDelimiter)[1]);
if ( valueLow > valueHigh ) {
let temp = valueLow;
valueLow = valueHigh;
valueHigh = temp;
}
valueLow =this.#round(valueLow,this.#Attributes.step);
valueHigh = this.#round(valueHigh,this.#Attributes.step);
this.#Attributes.currentValue = `${valueLow}${this.#valueDelimiter}${valueHigh}`;
this.#internals.setFormValue(this.#Attributes.currentValue);
if (this.#inputLow) this.#inputLow.value = valueLow;
if (this.#inputHigh) this.#inputHigh.value = valueHigh;
if (this.#inputLow && this.#inputHigh) this.#update();
}
}
#update() {
const currentValue = `${this.#inputLow.value}${this.#valueDelimiter}${this.#inputHigh.value}`;
this.#internals.setFormValue(currentValue);
this.#trackColor();
this.#valueDisplay.innerHTML = `${this.#inputLow.value} – ${this.#inputHigh.value}`;
}
#trackColor() {
const valueLow = Number(this.#inputLow.value);
const valueHigh = Number(this.#inputHigh.value);
const percentLow =
(valueLow - this.#Attributes.min) / (this.#Attributes.max - this.#Attributes.min) * 100;
const percentHigh =
(valueHigh - this.#Attributes.min) / (this.#Attributes.max - this.#Attributes.min) * 100;
this.#doubleSlider.style.setProperty('--percentLow', percentLow + '%');
this.#doubleSlider.style.setProperty('--percentHigh', percentHigh + '%');
}
#round(zahl, epsilon) {
if (epsilon>=1) {
return Math.round(zahl/epsilon)*epsilon;
}
else {
const n = Math.ceil(-Math.log10(epsilon));
return Number(zahl.toFixed(n));
}
}
// Eventhandler
// input: Für unterer Wert <= oberer Wert begrenzen sorgen,
// Werte an Internals übergeben und anzeigen
#handleInput(inputEvent) {
if(inputEvent.target == this.#inputLow
&& Number(this.#inputHigh.value) < Number(this.#inputLow.value)) {
this.#inputHigh.value = this.#inputLow.value;
}
else if(inputEvent.target == this.#inputHigh
&& Number(this.#inputHigh.value) < Number(this.#inputLow.value)) {
this.#inputLow.value = this.#inputHigh.value;
}
this.#update();
}
// pointerdown: den näher am Pointer liegenden Schieber zum Pointer schieben,
// Werte anzeigen
#handlePointerdown(pointerdownEvent) {
// Find the horizontal position that was clicked
let clickValue =
this.#Attributes.min +
(this.#Attributes.max - this.#Attributes.min) * pointerdownEvent.offsetX / pointerdownEvent.target.offsetWidth;
clickValue = this.#round(clickValue,this.#Attributes.step);
const middleValue = (Number(this.#inputHigh.value) + Number(this.#inputLow.value))/2;
if(clickValue < middleValue) {
this.#inputLow.value = clickValue;
}
else {
this.#inputHigh.value = clickValue;
}
this.#update();
this.dispatchEvent(new Event('input', { bubbles: true }));
this.dispatchEvent(new Event('change', { bubbles: true }));
}
// pointerover: Bei Bedarf Werteanzeige einblenden und Z-Index der Thumbs anpassen
#handlePointerover(pointeroverEvent) {
if(this.#Attributes.showvalues == 'active') this.#valueDisplay.className = 'ever';
const lowValue = Number(this.#inputLow.value);
const highValue = Number(this.#inputHigh.value);
let clickValue =
this.#Attributes.min +
(this.#Attributes.max - this.#Attributes.min) * pointeroverEvent.offsetX / pointeroverEvent.target.offsetWidth;
clickValue = this.#round(clickValue,this.#Attributes.step);
const middleValue = (lowValue + highValue)/2;
if(clickValue <= lowValue) {
this.#doubleSlider.style.setProperty('--thumbLowZIndex', 3);
this.#doubleSlider.style.setProperty('--thumbHighZIndex', 2);
}
else if(clickValue >= highValue) {
this.#doubleSlider.style.setProperty('--thumbLowZIndex', 2);
this.#doubleSlider.style.setProperty('--thumbHighZIndex', 3);
}
}
// pointerout: Den Schiebern den Focus nehmen und bei Bedarf Werteanzeige ausblenden
#handlePointerout(pointeroutEvent) {
this.#inputLow.blur();
this.#inputHigh.blur();
this.#valueDisplay.className = this.#Attributes.showvalues;
}
// change durchreichen
#handleChange(changeEvent) {
this.dispatchEvent(new Event('change', { bubbles: true }));
}
#html() {
return `
<label for='Low'><slot name='labellow'>Unterer Wert</slot></label>
<input id='Low'
type='range'
min=${this.#Attributes.min}
max=${this.#Attributes.max}
step=${this.#Attributes.step}
value=${this.#Attributes.currentValue.split(this.#valueDelimiter)[0]}></input>
<label for='High'><slot name='labelhigh'>Oberer Wert</slot></label>
<input id='High'
type='range'
min=${this.#Attributes.min}
max=${this.#Attributes.max}
step=${this.#Attributes.step}
value=${this.#Attributes.currentValue.split(this.#valueDelimiter)[1]}></input>
<output part='valuedisplay' role='status' aria-live='assertive' class='${this.#Attributes.showvalues}'></output>
`;
}
#css() {
return `
:host {
--trackInnerColor: lightblue;
--trackOuterColor: lightgray;
--thumbColor: blue;
--thumbFocusColor: lightblue;
--thumbSize: 1.7em;
--trackHeight: .3em;
--thumbLowZIndex: 2;
--thumbHighZIndex: 2;
display: inline-block;
width: 10em;
height: 2em;
}
#wrapper {
position: relative;
display: grid;
width: 100%;
height: 100%;
border: none;
padding: 0;
margin: 0;
background-size: auto var(--trackHeight) !important;
background-repeat: no-repeat !important;
background-position-y: 50% !important;
background: linear-gradient(to right,
var(--trackOuterColor) var(--percentLow),
var(--trackInnerColor) var(--percentLow),
var(--trackInnerColor) var(--percentHigh),
var(--trackOuterColor) var(--percentHigh) );
}
#wrapper label {
grid-area: 1 / 1 / 1 / 1;
clip: rect(0 0 0 0);
clip-path: inset(50%);
width: 1px;
height: 1px;
overflow: hidden;
white-space: nowrap;
}
#wrapper output {
position:absolute;
padding: 0;
left: 50%;
transform: translate(-50%, -50%);
text-wrap: nowrap;
background-color: white;
pointer-events: none;
display: none;
}
#wrapper output.ever {
display: block;
}
#wrapper:focus-within output.active {
display: block;
}
#wrapper input {
grid-area: 1 / 1 / 1 / 1;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
width: 100%;
background-color: transparent;
pointer-events: none;
}
#wrapper input::-webkit-slider-runnable-track {
-webkit-appearance: none;
appearance: none;
}
#wrapper input::-moz-range-track {
-moz-appearance: none;
appearance: none;
}
#wrapper input::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
height: var(--thumbSize);
width: var(--thumbSize);
cursor: pointer;
pointer-events: auto;
border-radius: 50%;
background-color: var(--thumbColor);
}
#wrapper input::-moz-range-thumb {
-moz-appearance: none;
appearance: none;
height: var(--thumbSize);
width: var(--thumbSize);
cursor: pointer;
pointer-events: auto;
border-radius: 50%;
background-color: var(--thumbColor);
}
#wrapper input:first-of-type {
z-index: var(--thumbLowZIndex)
}
#wrapper input:last-of-type {
z-index: var(--thumbHighZIndex)
}
#wrapper input:is(:focus,:hover)::-webkit-slider-thumb {
background-color: var(--thumbFocusColor);
border: medium solid var(--thumbColor);
}
#wrapper input:is(:focus,:hover)::-moz-range-thumb {
background-color: var(--thumbFocusColor);
border: medium solid var(--thumbColor);
}
`;
}
}); // double-range-input
// Diverse Tests
document.addEventListener('DOMContentLoaded', () => {
console.log('DOMContentLoaded');
const out_Test = document.querySelector('#output_Test');
const form_Test = document.querySelector('#Test');
// Element anlegen
const label = document.createElement('label');
// label.innerHTML = 'Wert 4: Geben Sie Minimal- und Maximalwert an.<br>Die Vorgaben werden alle 5s geändert.';
label.innerHTML = 'Wert 4: Geben Sie Minimal- und Maximalwert an.';
label.setAttribute('for', 'dr4');
const dr4 = document.createElement('double-range-input');
// Attributzuweisungen funktionieren auch mit setAttribute
dr4.name = 'dr4';
dr4.min = -1;
dr4.max = 1;
dr4.step = 0.01;
dr4.value = `-0.5${dr4.valueDelimiter}0.5`;
document.querySelector('[name=dr3]').insertAdjacentElement('afterend',label);
label.insertAdjacentElement('afterend',dr4);
const sliders = document.querySelectorAll('#Test double-range-input');
// Startwerte abfragen
sliders.forEach((v,i,a) => {
console.log(i,v.name,v.value,v.form);
if(i == 0) out_Test.innerText = 'Startwerte:';
out_Test.innerText += ` Element ${i}: ${v.name} ${v.value};`;
});
// Werte bei input-Event abfragen
form_Test.addEventListener('input', event => {
console.log('input', event.target.name, event.target.value);
out_Test.innerText = `input: Element ${event.target.name}: ${event.target.value};`;
});
// Werte bei change-Event abfragen
form_Test.addEventListener('change', event => {
console.log('change', event.target.name, event.target.value);
out_Test.innerText += ` change: Element ${event.target.name}: ${event.target.value};`;
});
// Submit abfangen und Formularwerte anzeigen
form_Test.addEventListener('submit', (event) => {
event.preventDefault();
const data = new FormData(event.target);
console.log([...data.entries()]);
out_Test.innerHTML = '';
[...data.entries()].forEach(v => out_Test.innerHTML += `${v[0]}: ${v[1]}<br>`)
});
// Value dynamisch ändern
//setInterval(() => {
let value = sliders[3].value;
const valueDelimiter = sliders[3].valueDelimiter;
let valueLow = Number(value.split(valueDelimiter)[0]);
let valueHigh = Number(value.split(valueDelimiter)[1]);
let min = sliders[3].min;
let max = sliders[3].max;
valueLow += (Math.random()-.5)*(max-min)/10;
valueHigh += (Math.random()-.5)*(max-min)/10;
value = `${valueLow}${valueDelimiter}${valueHigh}`;
sliders[3].value = value;
//}, 5000);
const form_Gebrauchtwagensuche = document.querySelector('#Gebrauchtwagensuche');
const out_Gebrauchtwagensuche = document.querySelector('#output_Gebrauchtwagensuche');
// Submit abfangen und Formularwerte anzeigen
form_Gebrauchtwagensuche.addEventListener('submit', (event) => {
event.preventDefault();
const data = new FormData(event.target);
console.log([...data.entries()]);
out_Gebrauchtwagensuche.innerHTML = '';
[...data.entries()].forEach(v => out_Gebrauchtwagensuche.innerHTML += `${v[0]}: ${v[1]}<br>`)
});
});
</script>
</head>
<body>
<h1>Min-Max-Schieberegler</h1>
<h2>Test des Custom-Elements</h2>
<form id="Test" method="get">
<label for="dr1">Wert 1: Geben Sie Minimal- und Maximalwert an.</label>
<double-range-input name="dr1"></double-range-input>
<label for="dr2">Wert 2: Geben Sie Minimal- und Maximalwert an.</label>
<double-range-input name="dr2" min=10 max=60 showvalues="never">
<span slot='labellow'>Nicht weniger als</span>
<span slot='labelhigh'>Nicht mehr als</span>
</double-range-input>
<label for="dr3">Wert 3: Geben Sie Minimal- und Maximalwert an.</label>
<double-range-input name="dr3" value="20,30" min=-10 max=60 showvalues="active"></double-range-input>
<button>Submit</button>
</form>
<output id="output_Test"></output>
<h2>Anwendungsbeispiel</h2>
<form id="Gebrauchtwagensuche" method="get">
<fieldset>
<legend>Gebrauchtwagensuche</legend>
<label for="Baujahr">Baujahr</label>
<double-range-input name="Baujahr" min="2000" max="2024" value="2000,2024"></double-range-input>
<label for="Kilometerstand">Kilometerstand</label>
<double-range-input name="Kilometerstand" min="0" max="200000" step="10000" value="0,200000"></double-range-input>
<label for="Preis">Preis in €</label>
<double-range-input name="Preis" min="2000" max="100000" step="1000" value=200,100000></double-range-input>
<button>Suche starten</button>
</fieldset>
</form>
<output id="output_Gebrauchtwagensuche"></output>
<p>Letzte Änderung: 17. 2. 2025</p>
</body>
</html>