VisiOmatic web client

source

control/widget/Spinbox.js

/**
 #	This file part of:	VisiOmatic
 * @file Provide a number spinbox with adaptive step increment.

 * @author Emmanuel Bertin <bertin@cfht.hawaii.edu>
 * @copyright (c) 2015-2023 CNRS/IAP/CFHT/SorbonneU.
   Adapted from jQuery Mobile Framework plugin to provide number spinbox
   (original copyright notice follows).
   Copyright (c) JTSage
   CC 3.0 Attribution.  May be relicensed without permission/notification.
   https://github.com/jtsage/jquery-mobile-spinbox
 */
import {
	Evented,
	DomEvent,
	DomUtil,
	Util
} from 'leaflet';


export const Spinbox = Evented.extend( /** @lends Spinbox */ {
	options: {
		title: 'Enter value',
		dmin: undefined,
		dmax: undefined,
		step: undefined,
		initValue: undefined,
		repButton: true,
		clickEvent: 'click',
		instantUpdate: false,
		className: 'leaflet-spinbox'
	},

	/**
	 * Create a new spinbox widget.

	 * @extends leaflet.Evented
	 * @memberof module:control/widget/Spinbox.js
	 * @constructs
	 * @override
	 * @param {object} parent - Parent element.
	 * @param {object} [options] - Options.

	 * @param {string} [options.title='Enter value']
	   Title of the control.

	 * @param {number} [options.dmin]
	   Minimum value.

	 * @param {number} [options.dmax]
	   Maximum value.

	 * @param {number} [options.step]
	   Initial value increment between autorepeat updates.

	 * @param {number} [options.initValue]
	   Initial value.

	 * @param {boolean} [options.repButton=true]
	   Up/Down spin buttons have autorepeat?

	 * @param {boolean} [options.instantUpdate=false]
	   Return updated value at each autorepeat update?

	 * @param {string} [options.className]
	   Element class name for the widget.

	 * @returns {Spinbox} Spinbox widget element.
	 */
	initialize: function (parent, options) {
		options = Util.setOptions(this, options);
		const	_this = this,
			drag = this._drag = {
				startEvent: 'touchstart mousedown',
				stopEvent : 'touchend mouseup mouseout touchcancel',
				move      : false,
				start     : false,
				end       : false,
				pos       : false,
				target    : false,
				delta     : false,
				tmp       : false,
				cnt       : 0,
				step      : options.step,
				prec      : this._prec(options.step)
			},
			wrap = this._wrap = DomUtil.create(
				'div',
				options.className,
				parent
			),
			input = this._input = DomUtil.create(
				'input',
				options.className + '-input',
				wrap
			),
			down = this._down = DomUtil.create(
				'div',
				options.className + '-down',
				wrap
			),
			downIcon = this._downIcon = DomUtil.create(
				'div',
				options.className + '-down-icon',
				down
			),
			up = this._up = DomUtil.create(
				'div',
				options.className + '-up',
				wrap
			),
			upIcon = this._upIcon = DomUtil.create(
				'div',
				options.className + '-up-icon',
				up
			);

		input.type = 'number';
		input.step = 0.1;	// Tells input that decimal numbers are valid
		DomEvent
			.disableClickPropagation(wrap)
			.disableScrollPropagation(wrap);

		if (input.disabled === true) {
			options.disabled = true;
		}

		if (options.dmin === undefined) {
			options.dmin = - Number.MAX_VALUE;
		}
		if (options.dmax === undefined) {
			options.dmax = Number.MAX_VALUE;
		}
		if (options.step === undefined) {
			options.step = 1;
		}

		if (options.initValue === undefined) {
			options.initValue = (options.dmin + options.dmax) / 2.0;
		}

		this.value(options.initValue);

		input.title = options.title;
		down.title = 'Decrease number by ' + options.step;
		up.title = 'Increase number by ' + options.step;

		DomEvent.on(this._input, 'change', function () {
			this.value(Math.min(
				Math.max(parseFloat(this._input.value), this.options.dmin),
				this.options.dmax
			));
			this.fire('change');
		}, this);

		if (options.repButton === false) {
			DomEvent.on(down, options.clickEvent, function (e) {
				e.preventDefault();
				this._offset(e.currentTarget, -1);
			}, this);
			DomEvent.on(up, options.clickEvent, function (e) {
				e.preventDefault();
				this._offset(e.currentTarget, 1);
			}, this);
		} else {
			DomEvent.on(down, drag.startEvent, function (e) {
				input.blur();
				drag.move = true;
				drag.cnt = 0;
				drag.step = options.step;
				drag.prec = this._prec(drag.step);
				drag.delta = -1;
				this._offset(e.currentTarget, -1);
				if (!this.runButton) {
					drag.target = e.currentTarget;
					this.runButton = setTimeout(function () {
						_this._sboxRun();
					}, 500);
				}
			}, this);
			DomEvent.on(up, drag.startEvent, function (e) {
				input.blur();
				drag.move = true;
				drag.cnt = 0;
				drag.step = options.step;
				drag.prec = this._prec(drag.step);
				drag.delta = 1;
				this._offset(e.currentTarget, 1);
				if (!this.runButton) {
					drag.target = e.currentTarget;
					this.runButton = setTimeout(function () {
						_this._sboxRun();
					}, 500);
				}
			}, this);
			DomEvent.on(down, drag.stopEvent, function (e) {
				if (drag.move) {
					e.preventDefault();
					clearTimeout(this.runButton);
					this.runButton = false;
					drag.move = false;
					if (options.instantUpdate === false) {
						this.fire('change');
					}
				}
			}, this);
			DomEvent.on(up, drag.stopEvent, function (e) {
				if (drag.move) {
					e.preventDefault();
					clearTimeout(this.runButton);
					this.runButton = false;
					drag.move = false;
					if (options.instantUpdate === false) {
						this.fire('change');
					}
				}
			}, this);
		}
	
		if (options.disabled) {
			this.disable();
		}

		return wrap;
	},

	/**
	 * Return or set the current value of the spinbox.
	 * @param {number} [val]
	   If provided, set the spinbox value to that of `val`.

	 * @returns {number} Spinbox value.
	 */
	value: function (val) {
		if (val === undefined) {
			return parseFloat(this._input.value);
		}
		else {
			this._input.value = Number(val.toPrecision(8));
			return this;
		}
	},

	/**
	 * Return or set the current increment value of the spinbox.
	 * @param {number} [val]
	   If provided, set the spinbox increment value to that of `val`.

	 * @returns {number} Spinbox increment value.
	 */
	step: function (val) {
		if (val === undefined) {
			return this.options.step;
		}
		else {
			this.options.step = val;
			return this;
		}
	},

	/**
	 * Disable the spinbox widget.
	 */
	disable: function () {
		// Disable the element
		const	cname = 'disabled';

		this._input.disabled = true;
		this._input.blur();
		DomUtil.addClass(this._wrap, cname);
		DomUtil.addClass(this._down, cname);
		DomUtil.addClass(this._up, cname);
		this.options.disabled = true;
	},

	/**
	 * Enable the spinbox widget.
	 */
	enable: function () {
		// Enable the element
		const	cname = 'disabled';

		this._input.disabled = false;
		DomUtil.removeClass(this._wrap, cname);
		DomUtil.removeClass(this._down, cname);
		DomUtil.removeClass(this._up, cname);
		this.options.disabled = false;
	},

	/**
	 * Run one step of the spinbox.
	 * @private
	 */
	_sboxRun: function () {
		const	_this = this,
			options = this.options,
			drag = this._drag;

		let	timer = 150;

		if (drag.cnt === 20) {
			timer = 50;
			drag.step = 10.0 * options.step;
			drag.prec = this._prec(drag.step);
		} else if (drag.cnt === 40) {
			timer = 10;
			drag.step = 100.0 * options.step;
			drag.prec = this._prec(drag.step);
		} else if (drag.cnt === 60) {
			drag.step = 1000.0 * options.step;
			drag.prec = this._prec(drag.step);
		} else if (drag.cnt === 80) {
			drag.step = 10000.0 * options.step;
			drag.prec = this._prec(drag.step);
		}
		drag.didRun = true;
		this._offset(this, drag.delta);
		drag.cnt++;
		this.runButton = setTimeout(function () {
			_this._sboxRun();
		}, timer);
	},

	/**
	 * Set spinbox value precision.
	 * @private
	 * @param {number} step
	   Increment value.

	 * @returns {number} Number of decimals.
	 */
	_prec: function (step) {
		const	dprec = -0.4342944 * Math.log(step);
		return dprec > 0.0 ? Math.ceil(dprec) : 0;
	},

	/**
	 * Increment or decrement spinbox value.
	 * @private
	 * @param {object} target
	   Element that triggered the action.
	 * @param {number} direction
	   Step direction (-1 or +1).
	 */
	_offset: function (obj, direction) {
		const	options = this.options,
			input = this._input,
			drag = this._drag;

		if (!this.disabled) {
			if (direction < 1) {
				var	tmp = (parseFloat(input.value) - drag.step).toFixed(
					drag.prec
				);
				if (tmp >= options.dmin) {
					input.value = tmp;
					if (options.instantUpdate === true) {
						this.fire('change');
					}
				}
			} else {
				var	tmp = (parseFloat(input.value) + drag.step).toFixed(
					drag.prec
				);
				if (tmp <= options.dmax) {
					input.value = tmp;
					if (options.instantUpdate === true) {
						this.fire('change');
					}
				}
			}
		}
	}
});

/**
 * Instantiate a spinbox widget.
 *
 * @function
 * @param {object} [parent] - Parent element.
 * @param {object} [options] - Options: see {@link Spinbox}
 * @returns {Spinbox} Spinbox widget element.

 * @example
   ...
   const elem = DomUtil.create('div', 'myelement', divParent);
   const spinElem = Spinbox(elem, {initValue: 42.0, step: 2.0});
 */
export const spinbox = function (parent, options) {
	return new Spinbox(parent, options);
};