DominoKit / domino-ui

Domino-ui
Apache License 2.0
217 stars 44 forks source link

Enhance slider to range slider #516

Open schube opened 3 years ago

schube commented 3 years ago

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!

howudodat commented 2 weeks ago

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

schube commented 2 weeks ago

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!!!