SELF-Treffen in Mannheim 2025

SELFHTML wird 30 Jahre alt! → Veranstaltungs-Ankündigung.

Beispiel:Min-Max-Schieberegler.html

Aus SELFHTML-Wiki
Wechseln zu: Navigation, Suche
<!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} &ndash; ${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>