Source: components/atoms/SliderComponent.js

import dom from '../../DOM.js'

import Component from '../../Component.js'

/**
SliderComponent gives the user the ability to choose from a range of values by dragging a handle from side to side of a bar.

@example
* const sliderComponent = new SliderComponent()
* sliderComponent.addListener(SliderComponent.ValueChangeViaInputEvent, (eventName, valueFraction) => {
* 	console.log('New slider fraction (from 0 to 1)', valueFraction)
* })

@todo Allow min and max values other than 0 and 1
@todo Add granularity or "snapping" for example only allowing tenths.
*/
const SliderComponent = class extends Component {
	/**
	@param {DataObject} [dataObject=null]
	@param {Object} [options=null]
	*/
	constructor(dataObject = null, options = {}) {
		super(dataObject, options)
		this.addClass('slider-component')
		this.setName('SliderComponent')
		this._handleMouseMove = this._handleMouseMove.bind(this)
		this._handleMouseUp = this._handleMouseUp.bind(this)

		this._minValue = 0
		this._maxValue = 1
		this._value = null

		this._barWidth = 0.08 // Set in slider-component.kss
		this._handleWidth = 0.01 // Set in slider-component.kss
		this._handleY = 0
		this._handleXStart = 0
		this._handleXEnd = this._barWidth - this._handleWidth

		this._handleComponent = new Component().appendTo(this)
		this._handleComponent.addClass('slider-handle')
		this._handleComponent.setName('SliderHandle')

		this._barComponent = new Component().appendTo(this)
		this._barComponent.addClass('slider-bar')
		this._barComponent.setName('SliderBar')

		this.value = this._value

		this._pointerDown = false
		this._pointerStart = 0 // page x, y
		this._handleStart = 0 // left value

		// set up listeners for dragging of the handle
		this.listenTo('mousedown', this._handleComponent.dom, (ev) => {
			ev.preventDefault()
			this._pointerDown = true
			this._pointerStart = ev.pageX
			this._handleStart = Number.parseFloat(this._handleComponent.dom.style['left'] || '0')
			// We listen to both the handle and the base component because the browser often can't keep up
			this.listenTo('mousemove', this._handleComponent.dom, this._handleMouseMove)
			this.listenTo('mousemove', this.dom, this._handleMouseMove)
		})
		this.listenTo('mouseup', this._handleComponent.dom, this._handleMouseUp)
		this.listenTo('mouseup', this.dom, this._handleMouseUp)
		this.listenTo('mouseleave', this.dom, this._handleMouseUp)
	}

	/**
	@desc the current value
	@type {number}
	*/
	get value() {
		return this._value
	}

	/**
	@param {number} val - a number to set the slider value which will be clamped in the minValue and maxValue inclusive range
	*/
	set value(val) {
		this._value = Math.min(this._maxValue, Math.max(this._minValue, val))
		this._updateHandlePosition()
	}

	/**
	@return {number} The fraction between 0 and 1 inclusive that the current value is between the minValue and maxValue
	*/
	get valueFraction() {
		return this._value / (this._maxValue - this._minValue)
	}

	/**
	@param {number} fraction - a number between 0 and 1
	*/
	set valueFraction(fraction) {
		fraction = Math.min(1, Math.max(0, fraction))
		this.value = this._minValue + (this._maxValue - this._minValue) * fraction
	}

	get minValue() {
		return this._minValue
	}
	get maxValue() {
		return this._maxValue
	}

	/**
	@desc true if the user is moving the handle
	@type {boolean}
  */
	get userIsChanging() {
		return this._pointerDown
	}

	_handleMouseMove(ev) {
		const xChange = ev.pageX - this._pointerStart
		this._handleComponent.dom.style['left'] = this._handleStart + xChange + 'px'
	}

	_handleMouseUp(ev) {
		ev.preventDefault()
		if (this._pointerDown === false) return
		this._pointerDown = false
		this._handleComponent.dom.removeEventListener('mousemove', this._handleMouseMove)
		this.dom.removeEventListener('mousemove', this._handleMouseMove)
		this.valueFraction = this._getValueFractionFromHandlePosition(this._handleComponent.dom, this._barComponent.dom)
		this.trigger(SliderComponent.ValueChangeViaInputEvent, this.valueFraction)
	}

	_getValueFractionFromHandlePosition(handleDOM, barDOM) {
		const handleLeft = Number.parseFloat(handleDOM.style['left'] || '0')
		if (barDOM.clientWidth === 0 || handleDOM.clientWidth === 0) return 0
		if (handleLeft < 0) return 0
		if (handleLeft >= barDOM.clientWidth) return 1
		return handleLeft / (barDOM.clientWidth - handleDOM.clientWidth)
	}

	_updateHandlePosition() {
		const barWidth = this._barComponent.dom.clientWidth
		const handleWidth = this._handleComponent.dom.clientWidth
		if (barWidth === 0 || handleWidth === 0) return // Not on the page yet
		const startX = 0
		const endX = barWidth - handleWidth
		const x = startX + (endX - startX) * this.valueFraction
		this._handleComponent.dom.style['left'] = `${x}px`
	}
}

SliderComponent.ValueChangeViaInputEvent = Symbol('sc-value-changed-via-input')

export default SliderComponent
export { SliderComponent }