Source: elements/GameRangeSlider.js

import {GameElement} from "./GameElement.js";
import {Point, randomColor} from "../Misc.js";

/**
 * Range slider class
 * @extends GameElement
 *
 * @property {number} width Width of slider in pixels
 * @property {string} color Color of the whole slider
 * @property {Array<function>} onChange Events to be trigerred on change of slider value
 * @property {GameShape} scaleElement Line that represents slider scale
 * @property {GameShape} handleElement Rectangle that represents slider handle
 * @property {{min:number, max:number}} bounds Maximum value of slider
 */
class GameRangeSlider extends GameElement{
    #width = undefined
    set width(newWidth) {
        if (isNaN(newWidth)) {
            throw new TypeError("Width value has to be a number!")
        }
        if (newWidth <= 0) {
            throw new Error("Width has to be larger than 0!")
        }
        let percent = 50
        if (this.scaleElement && this.handleElement) {
            percent = this.getPercentValue()
            this.scaleElement.setLine(new Point(-newWidth/2,0),new Point(newWidth/2,0))
        }
        this.#width = newWidth
        if (this.scaleElement && this.handleElement) {
            const value = (percent * (this.max - this.min)) + this.min
            this.setValue(value,false)
        }
    }
    get width() {
        return this.#width
    }
    #color = undefined
    set color(newColor){
        if (newColor === "random") {
            this.#color = randomColor()
        } else {
            this.#color = newColor
        }
        if (this.scaleElement && this.handleElement) {
            this.scaleElement.stroke = this.#color
            this.handleElement.fill = this.#color
        }
    }
    get color() {
        return this.#color
    }
    #onChange = []
    get onChange() {return [...this.#onChange]}

    #bounds = {
        min: 0,
        max: 10
    }
    get max() {return this.bounds.max}
    get min() {return this.bounds.min}
    get bounds() {
        return {
            min: this.#bounds.min,
            max: this.#bounds.max
        }
    }


    constructor(center,attrs={}) {
        super(center,[],attrs);
        this.width = attrs.width || 100
        this.#onChange = attrs.onChange || []
        this.color = attrs.color || "red"

        this.floating = (attrs.floating === undefined) ? false : attrs.floating
        this.setBounds(
            attrs.min || 0,
            attrs.max || 10
        )

        this.scaleElement = this.createShape("line", {coords:[-this.width/2,0,this.width/2,0],level:-2, stroke:this.color})
        this.handleElement = this.createShape("rectangle",{width:10,height:20,level:-1,fill:this.color})

        this.draggable = true
        this.stationary = true

        function dragHandle(event) {
            const delta = this.shared.mousePos
                .rotateAround(this.center,-this.rotation)
                .subtract(this.center)
            const dx = Math.min(Math.max(-this.width/2,delta.x),this.width/2)
            const percent = Number.parseFloat(((dx + (this.width/2)) / this.width).toFixed(3))

            const value = (percent * (this.max - this.min)) + this.min

            this.setValue(value)
        }

        this.addOnDragListener(dragHandle)
    }

    /**
     * Returns percent value of the slider based on handle position
     * @returns {number}
     */
    getPercentValue() {
        return (this.handleElement.dx + (this.width/2)) / this.width
    }

    /**
     * Sets minimum and maximum bounds for slider
     * @param {number} min
     * @param {number} max
     */
    setBounds(min=0, max=10) {
        if (Number.isNaN(min) || Number.isNaN(max)) {
            throw new TypeError("Incorrect type for method setBounds()!")
        }
        if (min >= max) {
            throw new RangeError("Min value has to be larger than max value!")
        }
        this.#bounds.min = min
        this.#bounds.max = max
    }

    /**
     * Returns value of slider
     * @returns {number}
     */
    getValue() {
        const percent = this.getPercentValue()
        const value = (percent * (this.max - this.min)) + this.min

        if (this.floating) {
            return Number.parseFloat((value).toFixed(3))
        }
        return Math.round(value)
    }

    /**
     * Sets value of slider and updates position of the handle
     * @param {number} value Value to be set
     * @param {boolean} change False prevents triggering onChange events
     */
    setValue(value,change=true) {
        if (value < this.min || value > this.max) {
            throw new RangeError(`Value has to be between ${this.min} and ${this.max}!`)
        }

        if (!this.floating) {
            value = Math.round(value)
        }

        const percent = (value - this.min) / (this.max - this.min)

        this.handleElement.dx = (percent * this.width) - this.width/2

        if (change) {
            for (const callback of this.#onChange) {
                callback.call(this)
            }
        }
    }

    /**
     * Adds a listener to the array of listeners for OnChange
     * @param {function} callback function to be called
     */
    addOnChangeListener(callback) {
        this.#onChange.push(callback)
    }

    /**
     * Removes listener for the OnChange event
     * @param {function} callback function you want to remove
     */
    removeOnChangeListener(callback) {
        this.#onChange = this.#onChange.filter(item=>item!==callback)
    }

    getAttrs() {
        return Object.assign({
            width: this.width,
            color: this.color,
        },super.getAttrs())
    }

    /**
     * Returns copy of current instance.
     * @param {string} newName Name of the newly created instance. Names have to be unique.
     * @returns {GameRangeSlider} New instance with the same attributes.
     */
    copy(newName) {
        const attrs = this.getAttrs()
        if (newName === undefined) {
            attrs.name = (attrs.name === undefined) ? undefined : attrs.name + "_copy"
        } else {
            attrs.name = newName
        }
        return new GameRangeSlider(attrs.center,attrs)
    }
}

export {GameRangeSlider}