domino-ui icon indicating copy to clipboard operation
domino-ui copied to clipboard

Enhance slider to range slider

Open schube opened this issue 4 years ago • 2 comments

The slider currently allows only to select one value. I am asking for an enhanced slider - a range slider. There you can select a range, i.e. a lower and an upper value.

This screenshots describes what I mean: https://flutter.github.io/assets-for-api-docs/assets/material/range_slider.png

Thank you!

schube avatar Jan 18 '21 18:01 schube

This is not 100% standard to domino v2, but is really close a little bit of work and this could be integrated into domino.

css:

.dualslider input {
	--start: 0%;
	--stop: 100%;
	-webkit-appearance: none;
	appearance: none;
	background: none;
	pointer-events: none;
	position: absolute;
	height: 5px;
	width: 100%;
	align-items: center;
	gap: var(--dui-slider-gap);
	margin: var(--dui-slider-margin);
}

.dualslider input:first-of-type {
	background-image: linear-gradient(to right, lightgrey var(--start), var(--dui-accent-l-1) var(--start), var(--dui-accent-l-1) var(--stop), lightgrey var(--stop));
}

.dualslider ::-moz-range-thumb {
	cursor: pointer;
	pointer-events: auto;
}

.dualslider ::-webkit-slider-thumb {
	cursor: pointer;
	pointer-events: auto;
}

.dualslider-thumb {
	background-color: var(--dui-slider-thumb-background, var(--dui-accent-l-1));
	position: absolute;
	border: var(--dui-slider-thumb-border);
	transform-origin: var(--dui-slider-thumb-transform-origin);
	transform: var(--dui-slider-thumb-transform);
	border-radius: var(--dui-slider-thumb-value-radius);
	height: var(--dui-slider-thumb-value-height);
	width: var(--dui-slider-thumb-value-width);
	top: -50px;
	margin: var(--dui-slider-thumb-value-margin);
	-webkit-transition: var(--dui-slider-thumb-transition);
	transition: var(--dui-slider-thumb-transition);
	transition-property: var(--dui-slider-thumb-property);
}

code:

package com.howudodat.ui.beans;

import static org.dominokit.domino.ui.style.GenericCss.dui_active;
import static org.dominokit.domino.ui.utils.Domino.*;

import java.util.HashSet;
import java.util.Set;

import org.dominokit.domino.ui.elements.DivElement;
import org.dominokit.domino.ui.elements.InputElement;
import org.dominokit.domino.ui.elements.LabelElement;
import org.dominokit.domino.ui.elements.SpanElement;
import org.dominokit.domino.ui.events.EventOptions;
import org.dominokit.domino.ui.events.EventType;
import org.dominokit.domino.ui.forms.FormsStyles;
import org.dominokit.domino.ui.sliders.SliderStyles;
import org.dominokit.domino.ui.utils.BaseDominoElement;
import org.dominokit.domino.ui.utils.HasChangeListeners;
import org.dominokit.domino.ui.utils.LazyChild;

import elemental2.dom.HTMLElement;
import elemental2.dom.Text;

public class DualSlider extends BaseDominoElement<HTMLElement, DualSlider>
		implements HasChangeListeners<DualSlider, Double[]>, SliderStyles, FormsStyles {

	private final DivElement root;
	protected final LazyChild<LabelElement> labelElement;
	private Text labelText = text();

	private Double[] oldValue;
	private final InputElement inputLow;
	private final SpanElement thumbLow;
	private final SpanElement valueElementLow;
	private final InputElement inputHigh;
	private final SpanElement thumbHigh;
	private final SpanElement valueElementHigh;
	private boolean withThumb;
	private boolean mouseDown;

	private Set<ChangeListener<? super Double[]>> changeListeners = new HashSet<>();
	private boolean changeListenersPaused;

	/**
	 * Creates a slider with a specified maximum value.
	 *
	 * @param max the maximum value for the slider
	 * @return a new slider instance
	 */
	public static DualSlider create(double max) {
		return create(max, 0, new Double[] { 0.0, max });
	}

	/**
	 * Creates a slider with a specified maximum and minimum value.
	 *
	 * @param max the maximum value for the slider
	 * @param min the minimum value for the slider
	 * @return a new slider instance
	 */
	public static DualSlider create(double max, double min) {
		return create(max, min, new Double[] { min, max });
	}

	/**
	 * Creates a slider with specified maximum, minimum, and initial value.
	 *
	 * @param max   the maximum value for the slider
	 * @param min   the minimum value for the slider
	 * @param value the initial value for the slider
	 * @return a new slider instance
	 */
	public static DualSlider create(double max, double min, Double[] value) {
		return new DualSlider(max, min, value);
	}

	/**
	 * Main constructor to create a Slider.
	 *
	 * @param max   the maximum value
	 * @param min   the minimum value
	 * @param value the initial value
	 */
	public DualSlider(double max, double min, Double[] value) {
		root = div().addCss("dui-slider").addCss("dualslider")
				.appendChild(inputLow = input("range").setAttribute("step", "any"))
				.appendChild(thumbLow = span().addCss("dualslider-thumb").collapse()
						.appendChild(valueElementLow = span().addCss(dui_slider_value)))
				.appendChild(inputHigh = input("range").setAttribute("step", "any"))
				.appendChild(thumbHigh = span().addCss("dualslider-thumb").collapse()
						.appendChild(valueElementHigh = span().addCss(dui_slider_value)));

		labelElement = LazyChild.of(label().addCss(dui_field_label), root);
		labelElement.whenInitialized(() -> labelElement.element().appendChild(labelText));

		setMaxValue(max);
		setMinValue(min);
		setValue(value);

		inputLow.addEventListener(EventType.input, evt -> onMouseMove(inputLow));
		inputLow.addEventListener(EventType.touchmove, evt -> onMouseMove(inputLow));
		inputLow.addEventListener(EventType.mousemove, evt -> onMouseMove(inputLow));
		inputHigh.addEventListener(EventType.input, evt -> onMouseMove(inputHigh));
		inputHigh.addEventListener(EventType.mousemove, evt -> onMouseMove(inputHigh));
		inputHigh.addEventListener(EventType.input, evt -> onMouseMove(inputHigh));

		inputLow.addEventListener(EventType.change, evt -> triggerChangeListeners(oldValue, getValue()));
		inputHigh.addEventListener(EventType.change, evt -> triggerChangeListeners(oldValue, getValue()));

		inputLow.addEventListener(EventType.mousedown, evt -> onMouseDown(inputLow));
		inputLow.addEventListener(EventType.touchstart, evt -> onMouseDown(inputLow), EventOptions.of().setPassive(true));
		inputHigh.addEventListener(EventType.mousedown, evt -> onMouseDown(inputHigh));
		inputHigh.addEventListener(EventType.touchstart, evt -> onMouseDown(inputHigh), EventOptions.of().setPassive(true));

		inputLow.addEventListener(EventType.mouseup, evt -> onMouseUp(inputLow));
		inputLow.addEventListener(EventType.touchend, evt -> onMouseUp(inputLow), EventOptions.of().setPassive(true));
		inputHigh.addEventListener(EventType.mouseup, evt -> onMouseUp(inputHigh));
		inputHigh.addEventListener(EventType.touchend, evt -> onMouseUp(inputHigh), EventOptions.of().setPassive(true));

		inputLow.addEventListener(EventType.mouseout, evt -> hideThumb(inputLow));
		inputLow.addEventListener(EventType.blur, evt -> hideThumb(inputLow));
		inputHigh.addEventListener(EventType.mouseout, evt -> hideThumb(inputHigh));
		inputHigh.addEventListener(EventType.blur, evt -> hideThumb(inputHigh));

	}

	private void onMouseMove(InputElement el) {
		if (el.equals(inputLow))
			onAdjustLow();
		else
			onAdjustHigh();

		if (mouseDown) {
			if (withThumb) {
				evaluateThumbPosition(el);
				updateThumbValue(el);
			}
		}
	}

	private void onAdjustLow() {
		// test for bounds
		Double val = Double.parseDouble(inputLow.getValue());
		Double valMax = Double.parseDouble(inputHigh.getValue());
		if (val > valMax) {
			val = valMax;
			inputLow.element().value = "" + val;
		}

		val = (val / Double.parseDouble(inputLow.element().max) * 100);
		inputLow.style().setCssProperty("--start", val + "%");
	}

	private void onAdjustHigh() {
		Double val = Double.parseDouble(inputHigh.getValue());
		Double valMin = Double.parseDouble(inputLow.getValue());
		if (val < valMin) {
			val = valMin;
			inputHigh.element().value = "" + val;
		}
		val = (val / Double.parseDouble(inputHigh.element().max) * 100);
		inputLow.style().setCssProperty("--stop", val + "%");
	}

	private void onMouseDown(InputElement el) {
		this.oldValue = getValue();
		el.addCss(dui_active);
		this.mouseDown = true;
		if (withThumb) {
			showThumb(el);
			evaluateThumbPosition(el);
		}
	}

	private void onMouseUp(InputElement el) {
		mouseDown = false;
		el.removeCss(dui_active);
		hideThumb(el);
	}

	/**
	 * Sets the maximum value of the slider.
	 *
	 * @param max the maximum value to be set
	 * @return the current slider instance
	 */
	public DualSlider setMaxValue(double max) {
		inputLow.element().max = String.valueOf(max);
		inputHigh.element().max = String.valueOf(max);
		return this;
	}

	/**
	 * Sets the minimum value of the slider.
	 *
	 * @param min the minimum value to be set
	 * @return the current slider instance
	 */
	public DualSlider setMinValue(double min) {
		inputLow.element().min = String.valueOf(min);
		inputHigh.element().min = String.valueOf(min);
		return this;
	}

	/**
	 * Sets the value of the slider and optionally triggers change listeners.
	 *
	 * @param newValue the new value to be set
	 * @param silent   if true, change listeners won't be triggered
	 * @return the current slider instance
	 */
	public DualSlider setValue(Double[] newValue, boolean silent) {
		Double[] oldValue = getValue();
		inputLow.element().value = String.valueOf(newValue[0]);
		inputHigh.element().value = String.valueOf(newValue[1]);
		if (!silent) {
			triggerChangeListeners(oldValue, newValue);
		}
		return this;
	}

	/**
	 * Sets the value of the slider and triggers change listeners.
	 *
	 * @param newValue the new value to be set
	 * @return the current slider instance
	 */
	public DualSlider setValue(Double[] newValue) {
		return setValue(newValue, false);
	}

	/**
	 * Gets the current value of the slider.
	 *
	 * @return the current value
	 */
	public Double[] getValue() {
		return new Double[] { Double.parseDouble(inputLow.element().value),
				Double.parseDouble(inputHigh.element().value) };
	}

	/**
	 * Gets the maximum value of the slider.
	 *
	 * @return the maximum value
	 */
	public double getMax() {
		return Double.parseDouble(inputHigh.element().max);
	}

	/**
	 * Gets the minimum value of the slider.
	 *
	 * @return the minimum value
	 */
	public double getMin() {
		return Double.parseDouble(inputLow.element().min);
	}

	/**
	 * Sets the stepping value of the slider.
	 *
	 * @param step the stepping value to be set
	 * @return the current slider instance
	 */
	public DualSlider setStep(double step) {
		inputLow.element().step = String.valueOf(step);
		inputHigh.element().step = String.valueOf(step);
		return this;
	}

	/**
	 * Sets whether the slider should show its thumb.
	 *
	 * @param withThumb if true, the thumb will be shown
	 * @return the current slider instance
	 */
	public DualSlider setShowThumb(boolean withThumb) {
		this.withThumb = withThumb;
		return this;
	}

	/** Shows the slider's thumb and updates its value display. */
	private void showThumb(InputElement el) {
		if (el.equals(inputLow)) {
			// thumbLow.style().setTop((root.getBoundingClientRect().top - 40) + "px");
			thumbLow.expand();
		} else {
			// thumbHigh.style().setTop((root.getBoundingClientRect().top - 40) + "px");
			thumbHigh.expand();
		}
		updateThumbValue(el);
	}

	/** Hides the slider's thumb. */
	private void hideThumb(InputElement el) {
		if (el.equals(inputLow))
			thumbLow.collapse();
		else
			thumbHigh.collapse();
	}

	/**
	 * Updates the display value of the slider's thumb based on its current value.
	 */
	private void updateThumbValue(InputElement el) {
		if (withThumb) {
			if (el.equals(inputLow))
				valueElementLow.setTextContent(String.valueOf(Double.valueOf(getValue()[0])));
			else
				valueElementHigh.setTextContent(String.valueOf(Double.valueOf(getValue()[1]).intValue()));
		}
	}

	/**
	 * Evaluates the position of the slider's thumb based on the current value of
	 * the slider.
	 */
	private void evaluateThumbPosition(InputElement el) {
		if (mouseDown) {
			if (el.equals(inputLow)) {
				thumbLow.style().setLeft(calculateRangeOffset(el) + "px");
			} else {
				thumbHigh.style().setLeft(calculateRangeOffset(el) + "px");
			}
		}
	}

	/**
	 * Calculates the range offset of the slider's thumb based on its current value.
	 *
	 * @return the calculated range offset in pixels
	 */
	private double calculateRangeOffset(InputElement el) {
		InputElement input = (el.equals(inputLow) ? inputLow : inputHigh);
		Double val = (el.equals(inputLow) ? getValue()[0] : getValue()[1]);
		int width = input.element().offsetWidth - 15;
		double percent = (val - getMin()) / (getMax() - getMin());
		return percent * width + input.element().offsetLeft;
	}

	@Override
	public HTMLElement element() {
		return root.element();
	}

	/**
	 * Adds a change listener to the slider. This listener will be notified of value
	 * changes.
	 *
	 * @param changeListener the listener to be added
	 * @return the current slider instance
	 */
	@Override
	public DualSlider addChangeListener(ChangeListener<? super Double[]> changeListener) {
		changeListeners.add(changeListener);
		return this;
	}

	/**
	 * Pauses the change listeners so they won't get triggered on value changes.
	 *
	 * @return the current slider instance
	 */
	@Override
	public DualSlider pauseChangeListeners() {
		this.changeListenersPaused = true;
		return this;
	}

	/**
	 * Resumes the change listeners so they get triggered on value changes.
	 *
	 * @return the current slider instance
	 */
	@Override
	public DualSlider resumeChangeListeners() {
		this.changeListenersPaused = false;
		return this;
	}

	/**
	 * Toggles the pause state of the change listeners.
	 *
	 * @param toggle if true, pause the listeners, otherwise resume them
	 * @return the current slider instance
	 */
	@Override
	public DualSlider togglePauseChangeListeners(boolean toggle) {
		this.changeListenersPaused = toggle;
		return this;
	}

	/**
	 * Gets the set of change listeners attached to the slider.
	 *
	 * @return the set of change listeners
	 */
	@Override
	public Set<ChangeListener<? super Double[]>> getChangeListeners() {
		return changeListeners;
	}

	/**
	 * Checks if the change listeners are currently paused.
	 *
	 * @return true if listeners are paused, false otherwise
	 */
	@Override
	public boolean isChangeListenersPaused() {
		return this.changeListenersPaused;
	}

	/**
	 * Triggers the change listeners manually with given old and new values.
	 *
	 * @param oldValue the previous value
	 * @param newValue the current value
	 * @return the current slider instance
	 */
	@Override
	public DualSlider triggerChangeListeners(Double[] oldValue, Double[] newValue) {
		if (!isChangeListenersPaused()) {
			changeListeners.forEach(changeListener -> changeListener.onValueChanged(oldValue, newValue));
		}
		return this;
	}

	/**
	 * Sets the label for this form element.
	 *
	 * @param label The label to set.
	 * @return This form element instance.
	 */
	public DualSlider setLabel(String label) {
		labelElement.get();
		labelText.textContent = label;
		return this;
	}

	/**
	 * Gets the label of this form element.
	 *
	 * @return The label of this form element.
	 */
	public String getLabel() {
		if (labelElement.isInitialized()) {
			return labelElement.get().getTextContent();
		}
		return "";
	}
}

to use:

protected DualSlider sldPriceRange = DualSlider.create(10, 0);
sldPriceRange.setShowThumb(true).setStep(.5).setLabel("Price Range");

image

howudodat avatar Oct 10 '24 01:10 howudodat

Very cool, thank you. The project where I needed such a component is long finished, but next time when I need a range slider, I will get back to this code. Thank you!!!

schube avatar Oct 10 '24 05:10 schube

Added to domino-ui-pro.

vegegoku avatar Nov 26 '24 15:11 vegegoku