341 lines
11 KiB
JavaScript
341 lines
11 KiB
JavaScript
/* eslint-disable unicorn/prefer-math-trunc */
|
|
/* eslint-disable no-bitwise */
|
|
/* eslint-disable unicorn/prefer-ternary */
|
|
import {findPos} from './Util.js';
|
|
/**
|
|
* Whether a value is `null` or `undefined`.
|
|
* @param {any} val
|
|
* @returns {boolean}
|
|
*/
|
|
const isNullish = (val) => {
|
|
return val === null || val === undefined;
|
|
};
|
|
/**
|
|
* Encapsulate slider functionality for the ColorMap and ColorBar -
|
|
* could be useful to use a jQuery UI draggable for this with certain extensions.
|
|
* @memberof module:jPicker
|
|
*/
|
|
export default class Slider {
|
|
/**
|
|
* @param {external:jQuery} bar
|
|
* @param {module:jPicker.SliderOptions} options
|
|
*/
|
|
constructor (bar, options) {
|
|
const that = this;
|
|
/**
|
|
* Fire events on the supplied `context`
|
|
* @param {module:jPicker.JPickerInit} context
|
|
* @returns {void}
|
|
*/
|
|
function fireChangeEvents (context) {
|
|
changeEvents.forEach((changeEvent) => {
|
|
changeEvent.call(that, that, context);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Bind the mousedown to the bar not the arrow for quick snapping to the clicked location.
|
|
* @param {external:jQuery.Event} e
|
|
* @returns {void}
|
|
*/
|
|
function mouseDown (e) {
|
|
const off = findPos(bar);
|
|
offset = {l: off.left | 0, t: off.top | 0};
|
|
clearTimeout(timeout);
|
|
// using setTimeout for visual updates - once the style is updated the browser will re-render internally allowing the next Javascript to run
|
|
timeout = setTimeout(function () {
|
|
setValuesFromMousePosition.call(that, e);
|
|
}, 0);
|
|
// Bind mousemove and mouseup event to the document so it responds when dragged of of the bar - we will unbind these when on mouseup to save processing
|
|
document.addEventListener('mousemove', mouseMove);
|
|
document.addEventListener('mouseup', mouseUp);
|
|
e.preventDefault(); // don't try to select anything or drag the image to the desktop
|
|
}
|
|
/**
|
|
* Set the values as the mouse moves.
|
|
* @param {external:jQuery.Event} e
|
|
* @returns {false}
|
|
*/
|
|
function mouseMove (e) {
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(function () {
|
|
setValuesFromMousePosition.call(that, e);
|
|
}, 0);
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
return false;
|
|
}
|
|
/**
|
|
* Unbind the document events - they aren't needed when not dragging.
|
|
* @param {external:jQuery.Event} e
|
|
* @returns {false}
|
|
*/
|
|
function mouseUp (e) {
|
|
document.removeEventListener('mousemove', mouseMove);
|
|
document.removeEventListener('mouseup', mouseUp);
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Calculate mouse position and set value within the current range.
|
|
* @param {Event} e
|
|
* @returns {void}
|
|
*/
|
|
function setValuesFromMousePosition (e) {
|
|
const barW = bar.w, // local copies for YUI compressor
|
|
barH = bar.h;
|
|
let locX = e.pageX - offset.l,
|
|
locY = e.pageY - offset.t;
|
|
// keep the arrow within the bounds of the bar
|
|
if (locX < 0) locX = 0;
|
|
else if (locX > barW) locX = barW;
|
|
if (locY < 0) locY = 0;
|
|
else if (locY > barH) locY = barH;
|
|
val.call(that, 'xy', {
|
|
x: ((locX / barW) * rangeX) + minX,
|
|
y: ((locY / barH) * rangeY) + minY
|
|
});
|
|
}
|
|
/**
|
|
*
|
|
* @returns {void}
|
|
*/
|
|
function draw () {
|
|
const
|
|
barW = bar.w,
|
|
barH = bar.h,
|
|
arrowW = arrow.w,
|
|
arrowH = arrow.h;
|
|
let arrowOffsetX = 0,
|
|
arrowOffsetY = 0;
|
|
setTimeout(function () {
|
|
if (rangeX > 0) { // range is greater than zero
|
|
// constrain to bounds
|
|
if (x === maxX) arrowOffsetX = barW;
|
|
else arrowOffsetX = ((x / rangeX) * barW) | 0;
|
|
}
|
|
if (rangeY > 0) { // range is greater than zero
|
|
// constrain to bounds
|
|
if (y === maxY) arrowOffsetY = barH;
|
|
else arrowOffsetY = ((y / rangeY) * barH) | 0;
|
|
}
|
|
// if arrow width is greater than bar width, center arrow and prevent horizontal dragging
|
|
if (arrowW >= barW) arrowOffsetX = (barW >> 1) - (arrowW >> 1); // number >> 1 - superfast bitwise divide by two and truncate (move bits over one bit discarding lowest)
|
|
else arrowOffsetX -= arrowW >> 1;
|
|
// if arrow height is greater than bar height, center arrow and prevent vertical dragging
|
|
if (arrowH >= barH) arrowOffsetY = (barH >> 1) - (arrowH >> 1);
|
|
else arrowOffsetY -= arrowH >> 1;
|
|
// set the arrow position based on these offsets
|
|
arrow.style.left = arrowOffsetX + 'px';
|
|
arrow.style.top = arrowOffsetY + 'px';
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get or set a value.
|
|
* @param {?("xy"|"x"|"y")} name
|
|
* @param {module:math.XYObject} value
|
|
* @param {module:jPicker.Slider} context
|
|
* @returns {module:math.XYObject|Float|void}
|
|
*/
|
|
function val (name, value, context) {
|
|
const set = value !== undefined;
|
|
if (!set) {
|
|
if (isNullish(name)) name = 'xy';
|
|
switch (name.toLowerCase()) {
|
|
case 'x': return x;
|
|
case 'y': return y;
|
|
case 'xy':
|
|
default: return {x, y};
|
|
}
|
|
}
|
|
if (!isNullish(context) && context === that) return undefined;
|
|
let changed = false;
|
|
|
|
let newX, newY;
|
|
if (isNullish(name)) name = 'xy';
|
|
switch (name.toLowerCase()) {
|
|
case 'x':
|
|
newX = (value && ((value.x && value.x | 0) || value | 0)) || 0;
|
|
break;
|
|
case 'y':
|
|
newY = (value && ((value.y && value.y | 0) || value | 0)) || 0;
|
|
break;
|
|
case 'xy':
|
|
default:
|
|
newX = (value && value.x && value.x | 0) || 0;
|
|
newY = (value && value.y && value.y | 0) || 0;
|
|
break;
|
|
}
|
|
if (!isNullish(newX)) {
|
|
if (newX < minX) newX = minX;
|
|
else if (newX > maxX) newX = maxX;
|
|
if (x !== newX) {
|
|
x = newX;
|
|
changed = true;
|
|
}
|
|
}
|
|
if (!isNullish(newY)) {
|
|
if (newY < minY) newY = minY;
|
|
else if (newY > maxY) newY = maxY;
|
|
if (y !== newY) {
|
|
y = newY;
|
|
changed = true;
|
|
}
|
|
}
|
|
changed && fireChangeEvents.call(that, context || that);
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* @typedef {PlainObject} module:jPicker.MinMaxRangeX
|
|
* @property {Float} minX
|
|
* @property {Float} maxX
|
|
* @property {Float} rangeX
|
|
*/
|
|
/**
|
|
* @typedef {PlainObject} module:jPicker.MinMaxRangeY
|
|
* @property {Float} minY
|
|
* @property {Float} maxY
|
|
* @property {Float} rangeY
|
|
*/
|
|
/**
|
|
* @typedef {module:jPicker.MinMaxRangeY|module:jPicker.MinMaxRangeX} module:jPicker.MinMaxRangeXY
|
|
*/
|
|
|
|
/**
|
|
*
|
|
* @param {"minx"|"maxx"|"rangex"|"miny"|"maxy"|"rangey"|"all"} name
|
|
* @param {module:jPicker.MinMaxRangeXY} value
|
|
* @returns {module:jPicker.MinMaxRangeXY|module:jPicker.MinMaxRangeX|module:jPicker.MinMaxRangeY|void}
|
|
*/
|
|
function range (name, value) {
|
|
const set = value !== undefined;
|
|
if (!set) {
|
|
if (isNullish(name)) name = 'all';
|
|
switch (name.toLowerCase()) {
|
|
case 'minx': return minX;
|
|
case 'maxx': return maxX;
|
|
case 'rangex': return {minX, maxX, rangeX};
|
|
case 'miny': return minY;
|
|
case 'maxy': return maxY;
|
|
case 'rangey': return {minY, maxY, rangeY};
|
|
case 'all':
|
|
default: return {minX, maxX, rangeX, minY, maxY, rangeY};
|
|
}
|
|
}
|
|
let // changed = false,
|
|
newMinX,
|
|
newMaxX,
|
|
newMinY,
|
|
newMaxY;
|
|
if (isNullish(name)) name = 'all';
|
|
switch (name.toLowerCase()) {
|
|
case 'minx':
|
|
newMinX = (value && ((value.minX && value.minX | 0) || value | 0)) || 0;
|
|
break;
|
|
case 'maxx':
|
|
newMaxX = (value && ((value.maxX && value.maxX | 0) || value | 0)) || 0;
|
|
break;
|
|
case 'rangex':
|
|
newMinX = (value && value.minX && value.minX | 0) || 0;
|
|
newMaxX = (value && value.maxX && value.maxX | 0) || 0;
|
|
break;
|
|
case 'miny':
|
|
newMinY = (value && ((value.minY && value.minY | 0) || value | 0)) || 0;
|
|
break;
|
|
case 'maxy':
|
|
newMaxY = (value && ((value.maxY && value.maxY | 0) || value | 0)) || 0;
|
|
break;
|
|
case 'rangey':
|
|
newMinY = (value && value.minY && value.minY | 0) || 0;
|
|
newMaxY = (value && value.maxY && value.maxY | 0) || 0;
|
|
break;
|
|
case 'all':
|
|
default:
|
|
newMinX = (value && value.minX && value.minX | 0) || 0;
|
|
newMaxX = (value && value.maxX && value.maxX | 0) || 0;
|
|
newMinY = (value && value.minY && value.minY | 0) || 0;
|
|
newMaxY = (value && value.maxY && value.maxY | 0) || 0;
|
|
break;
|
|
}
|
|
|
|
if (!isNullish(newMinX) && minX !== newMinX) {
|
|
minX = newMinX;
|
|
rangeX = maxX - minX;
|
|
}
|
|
if (!isNullish(newMaxX) && maxX !== newMaxX) {
|
|
maxX = newMaxX;
|
|
rangeX = maxX - minX;
|
|
}
|
|
if (!isNullish(newMinY) && minY !== newMinY) {
|
|
minY = newMinY;
|
|
rangeY = maxY - minY;
|
|
}
|
|
if (!isNullish(newMaxY) && maxY !== newMaxY) {
|
|
maxY = newMaxY;
|
|
rangeY = maxY - minY;
|
|
}
|
|
return undefined;
|
|
}
|
|
/**
|
|
* @param {GenericCallback} callback
|
|
* @returns {void}
|
|
*/
|
|
function bind (callback) { // eslint-disable-line promise/prefer-await-to-callbacks
|
|
if (typeof callback === 'function') changeEvents.push(callback);
|
|
}
|
|
/**
|
|
* @param {GenericCallback} callback
|
|
* @returns {void}
|
|
*/
|
|
function unbind (callback) { // eslint-disable-line promise/prefer-await-to-callbacks
|
|
if (typeof callback !== 'function') return;
|
|
let i;
|
|
while ((i = changeEvents.includes(callback))) changeEvents.splice(i, 1);
|
|
}
|
|
/**
|
|
*
|
|
* @returns {void}
|
|
*/
|
|
function destroy () {
|
|
// unbind all possible events and null objects
|
|
document.removeEventListener('mousemove', mouseMove);
|
|
document.removeEventListener('mouseup', mouseUp);
|
|
bar.removeEventListener('mousedown', mouseDown);
|
|
bar = null;
|
|
arrow = null;
|
|
changeEvents = null;
|
|
}
|
|
let offset,
|
|
timeout,
|
|
x = 0,
|
|
y = 0,
|
|
minX = 0,
|
|
maxX = 100,
|
|
rangeX = 100,
|
|
minY = 0,
|
|
maxY = 100,
|
|
rangeY = 100,
|
|
arrow = bar.querySelector('img'), // the arrow image to drag
|
|
changeEvents = [];
|
|
Object.assign(that, {
|
|
val,
|
|
range,
|
|
bind,
|
|
unbind,
|
|
destroy
|
|
});
|
|
// initialize this control
|
|
arrow.src = options.arrow && options.arrow.image;
|
|
arrow.w = (options.arrow && options.arrow.width) || arrow.width();
|
|
arrow.h = (options.arrow && options.arrow.height) || arrow.height();
|
|
bar.w = (options.map && options.map.width) || bar.width();
|
|
bar.h = (options.map && options.map.height) || bar.height();
|
|
bar.addEventListener('mousedown', mouseDown);
|
|
bind.call(that, draw);
|
|
}
|
|
}
|