import Widget from "ui/core/widget";
import Constraint from "util/constraint";
import ConstraintSpec from "util/constraint-def";
/**
* Class representing an Numberbox widget.
* @class
* @implements {Widget}
*/
class Numberbox extends Widget {
/**
* @constructor
* @param {object} container - DOM container for the widget.
* @param {object} [o] - Options.
* @param {number} [o.minVal=null] - Minimum value.
* @param {number} [o.maxVal=null] - Maximum value.
* @param {number} [o.precision=0] - Number of decimal places to use.
* @param {string} [o.backgroundColor="#282828"] - The background color.
* @param {string} [o.fontColor="#aaa"] - The font color.
* @param {string} [o.fontSize="12px"] - The font size.
* @param {string} [o.fontFamily="Arial"] - The font family.
* @param {string} [o.appendString=""] - String to append to the value when displaying (i.e. " Hz").
*/
constructor(container, o) {
super(container, o);
return this;
}
/* ============================================================================================== */
/* PUBLIC API
/* ============================================================================================== */
/**
* Returns the current value.
* @public @override
* @returns {number} - Current value.
*/
getVal() {
return this.state.val;
}
/**
* Sets the current value.
* Same as setVal(), but will not cause an observer callback trigger.
* @public @override
* @param {number} newVal - The new value.
*/
setInternalVal(newVal) {
this.setInternalState({ val: newVal });
}
/**
* Sets the current value.
* Same as setInternalVal(), but will cause an observer callback trigger.
* @public @override
* @param {number} newVal - The new value.
*/
setVal(newVal) {
this.setState({ val: newVal });
}
/**
* Returns a string representation of the value.
* @returns {string} - String representation of the value.
*/
toString() {
return this.state.val.toFixed(this.o.precision);
}
/* ============================================================================================== */
/* INITIALIZATION METHODS
/* ============================================================================================== */
/**
* Initializes the options.
* @private @override
*/
_initOptions(o) {
// set the defaults
this.o = {
minVal: 0,
maxVal: 127,
precision: 4,
quantizeInterval: 1,
backgroundColor: "#282828",
fontColor: "#ccc",
fontSize: "12px",
fontFamily: "Arial",
textSelectBackgroundColor: "#f00",
appendString: "",
mouseSensitivity: 0.01,
mouseFineSensitivity: 0.001 // Fine sensitivity is used when shift key is held
};
// override defaults with provided options
super._initOptions(o);
}
/**
* Initializes state constraints.
* @private @override
*/
_initStateConstraints() {
const _this = this;
let valConstraintDef = {};
if (this.o.minVal !== null) {
valConstraintDef.minVal = this.o.minVal;
}
if (this.o.maxVal !== null) {
valConstraintDef.maxVal = this.o.maxVal;
}
this.stateConstraints = new ConstraintSpec({
val: new Constraint(valConstraintDef)
});
}
/**
* Initializes the state.
* @private @override
*/
_initState() {
this.state = {
val: 0
};
}
/**
* Initializes the svg elements.
* @private @override
*/
_initSvgEls() {
const _this = this;
this.svgEls = {
panel: document.createElementNS(_this.SVG_NS, "rect"),
textUnderlay: document.createElementNS(_this.SVG_NS, "rect"),
text: document.createElementNS(_this.SVG_NS, "text"),
cursor: document.createElementNS(_this.SVG_NS, "rect"),
overlay: document.createElementNS(_this.SVG_NS, "rect")
};
this.svgEls.text.setAttribute("alignment-baseline", "middle");
this.svgEls.text.setAttribute("text-anchor", "middle");
this.svg.addEventListener("mouseover", function() {
_this.svg.style.cursor = "text";
});
this.svgEls.textUnderlay.setAttribute("fill", "transparent");
this.svgEls.cursor.setAttribute("fill", "rgba(0,0,0,0)");
this.svgEls.cursor.setAttribute("stroke", "rgba(0,0,0,0)");
this._appendSvgEls();
this._update();
}
/**
* Initializes mouse and touch event handlers.
* @private @override
*/
_initHandlers() {
const _this = this;
let x0 = 0;
let y0 = 0;
let yD = 0;
let newVal = _this.getState().val;
let charNum;
let charBRect;
let power;
let prevTouchTime = Date.now();
this.handlers = {
touch: function touch(ev) {
ev.preventDefault();
ev.stopPropagation();
y0 = ev.clientY;
x0 = ev.clientX;
charNum = _this._getSelectedCharNumber(x0, y0);
power = _this._getPowerOfSelectedDigit(charNum);
document.addEventListener("mousemove", _this.handlers.move);
document.addEventListener("touchmove", _this.handlers.move);
_this.svg.addEventListener("mouseup", _this.handlers.kbdEdit);
_this.svg.addEventListener("touchend", _this.handlers.kbdEdit);
},
move: function move(ev) {
ev.preventDefault();
ev.stopPropagation();
let clientX = ev.clientX;
let clientY = ev.clientY;
yD = y0 - clientY;
let newVal = _this.getVal() + (yD * Math.pow(10, power) * _this.o.mouseSensitivity);
_this.setState({ val: newVal });
_this.svg.removeEventListener("mouseup", _this.handlers.kbdEdit);
_this.svg.removeEventListener("touchend", _this.handlers.kbdEdit);
document.addEventListener("mouseup", _this.handlers.release);
document.addEventListener("touchend", _this.handlers.release);
},
// Edit the value by typing on a keyboard
kbdEdit: function kbdEdit(ev) {
ev.preventDefault();
ev.stopPropagation();
_this.svg.removeEventListener("mouseup", _this.handlers.kbdEdit);
_this.svg.removeEventListener("touchend", _this.handlers.kbdEdit);
document.removeEventListener("mousemove", _this.handlers.move);
document.removeEventListener("touchmove", _this.handlers.move);
charNum = _this._getSelectedCharNumber(ev.clientX, ev.clientY);
charBRect = _this.svgEls.text.getExtentOfChar(Math.min(charNum, _this.svgEls.text.length - 1));
let editStr = _this.toString();
// If the click is past the mid-point of the character, we select the next char, bounded by the length of the string
if (ev.clientX > ((charBRect.x + (charBRect.x + charBRect.width)) * 0.55)) {
charNum = charNum + 1;
}
_this.svgEls.text.textContent = _this._editText(editStr, charNum);
},
release: function release(ev) {
ev.preventDefault();
ev.stopPropagation();
document.removeEventListener("mousemove", _this.handlers.move);
document.removeEventListener("touchmove", _this.handlers.move);
}
};
this.svg.addEventListener("mousedown", _this.handlers.touch);
this.svg.addEventListener("touchstart", _this.handlers.touch);
}
/**
* Updates (redraws) components based on state.
* @private @override
*/
_update() {
const _this = this;
this.svgEls.text.textContent = this.toString() + this.o.appendString;
let panelWidth = _this._getWidth();
let panelHeight = _this._getHeight();
let textWidth = this.svgEls.text.getBoundingClientRect().width;
let textHeight = this.svgEls.text.getBoundingClientRect().height;
this.svgEls.panel.setAttribute("fill", _this.o.backgroundColor);
this.svgEls.panel.setAttribute("width", panelWidth);
this.svgEls.panel.setAttribute("height", panelHeight);
this.svgEls.text.setAttribute("x", panelWidth / 2);
this.svgEls.text.setAttribute("y", panelHeight / 2);
this.svgEls.text.setAttribute("fill", _this.o.fontColor);
this.svgEls.overlay.setAttribute("fill", "transparent");
this.svgEls.overlay.setAttribute("width", _this._getWidth());
this.svgEls.overlay.setAttribute("height", _this._getHeight());
}
/* ============================================================================================== */
/* INTERNAL FUNCTIONALITY METHODS
/* ============================================================================================== */
/**
* Function called when 'select all' is invoked.
*/
_editSelectAll(ev) {
const _this = this;
ev.preventDefault();
ev.stopPropagation();
this.svgEls.text.textContent = this.toString();
let textBRect = _this.svgEls.text.getBoundingClientRect();
let svgBRect = _this.svg.getBoundingClientRect();
_this.svgEls.textUnderlay.setAttribute("fill", "#f00");
_this.svgEls.textUnderlay.setAttribute("x", textBRect.x - svgBRect.x);
_this.svgEls.textUnderlay.setAttribute("y", textBRect.y - svgBRect.y);
_this.svgEls.textUnderlay.setAttribute("width", textBRect.width);
_this.svgEls.textUnderlay.setAttribute("height", textBRect.height);
document.addEventListener("keydown", makeEdit);
document.addEventListener("mousedown", finishEditing);
document.addEventListener("touchstart", finishEditing);
function makeEdit(ev) {
let key = ev.key;
let str = "";
switch(key) {
case "-":
case "1": case "2": case "3": case "4": case "5":
case "6": case "7": case "8": case "9": case ".":
str = str + key;
case "Backspace":
case "Delete":
finishEditing(ev);
_this._editText(str, 1);
break;
case "Enter":
case "Escape":
finishEditing(ev);
break;
default:
break;
}
}
// Finish editing
function finishEditing(ev) {
ev.preventDefault();
ev.stopPropagation();
document.removeEventListener("keydown", makeEdit);
document.removeEventListener("mousedown", finishEditing);
document.removeEventListener("touchstart", finishEditing);
_this.svgEls.textUnderlay.setAttribute("fill", "transparent");
}
}
_editText(str, charNum) {
const _this = this;
let prevTime = Date.now();
this.svg.removeEventListener("mousedown", _this.handlers.touch);
this.svg.removeEventListener("touchstart", _this.handlers.touch);
this.svg.addEventListener("mousedown", checkDoubleTap);
this.svg.addEventListener("touchstart", checkDoubleTap);
this.svgEls.text.textContent = str;
let showCursorTimeoutID = null;
let hideCursorTimeoutID = null;
positionCursor();
document.addEventListener("keydown", makeEdit);
document.addEventListener("mousedown", finishEditing);
document.addEventListener("touchstart", finishEditing);
function makeEdit(ev) {
let key = ev.key;
switch(key) {
case "Backspace":
str = deletePrev();
positionCursor();
break;
case "Delete":
str = deleteNext();
positionCursor();
break;
case "ArrowLeft":
moveLeft();
positionCursor();
break;
case "ArrowRight":
moveRight();
positionCursor();
break;
case "ArrowUp":
increment();
positionCursor();
break;
case "ArrowDown":
decrement();
positionCursor();
break;
case "-":
str = insertMinus();
positionCursor();
break;
case "1": case "2": case "3": case "4": case "5":
case "6": case "7": case "8": case "9": case ".":
str = insertChar(key);
positionCursor();
break;
case "Enter":
case "Escape":
console.log("Enter or escape");
finishEditing(ev);
break;
default:
break;
}
_this.svgEls.text.textContent = str;
}
// Check if the gesture is a double-tap
function checkDoubleTap(ev) {
if ((Date.now() - prevTime) < 250) {
finishEditing(ev);
_this._editSelectAll(ev);
} else {
finishEditing(ev);
_this.handlers.touch(ev);
}
prevTime = Date.now();
}
// Delete previous character
function deletePrev() {
str = str.substring(0, charNum - 1) + str.substr(charNum);
charNum--;
return str;
}
// Delete next character
function deleteNext() {
str = str.substring(0, charNum) + str.substr(charNum + 1);
return str;
}
// Move cursor left
function moveLeft() {
charNum = Math.max(0, charNum - 1);
}
// Move cursor right
function moveRight() {
charNum = Math.min(str.length, charNum + 1);
}
// Increment current character
function increment() {
let power = _this._getPowerOfSelectedDigit(charNum);
str = (parseFloat(str) + Math.pow(10, power))
.toFixed(_this.o.precision);
}
// Decrement current character
function decrement() {
let power = _this._getPowerOfSelectedDigit(charNum);
str = (parseFloat(str) - Math.pow(10, power))
.toFixed(_this.o.precision);
}
// Insert minus sign
function insertMinus() {
if (charNum === 0) {
str = "-" + str;
charNum++;
}
return str;
}
// Insert a charactor
function insertChar(key) {
str = str.substring(0, charNum) + key + str.substr(charNum);
charNum++;
return str;
}
// Position the cursor
function positionCursor() {
if (showCursorTimeoutID !== null) {
clearTimeout(showCursorTimeoutID);
showCursorTimeoutID = null;
}
if (hideCursorTimeoutID !== null) {
clearTimeout(hideCursorTimeoutID);
hideCursorTimeoutID = null;
}
_this.svgEls.text.textContent = str;
let charBRect = _this.svgEls.text.getExtentOfChar(Math.min(charNum, (str.length - 1)));
_this.svgEls.cursor.setAttribute("height", charBRect.height);
if (charNum == str.length) {
let charEndPos = _this.svgEls.text.getEndPositionOfChar(str.length - 1);
_this.svgEls.cursor.setAttribute("x", charBRect.x + charBRect.width);
} else {
_this.svgEls.cursor.setAttribute("x", charBRect.x - 0.5);
}
_this.svgEls.cursor.setAttribute("y", charBRect.y);
_this.svgEls.cursor.setAttribute("width", 1);
showCursor();
}
// Show the cursor
function showCursor() {
_this.svgEls.cursor.setAttribute("stroke", _this.o.fontColor);
if (hideCursorTimeoutID !== null) {
window.clearTimeout(hideCursorTimeoutID);
hideCursorTimeoutID = null;
}
hideCursorTimeoutID = window.setTimeout(hideCursor, 500);
}
// Hide the cursor
function hideCursor() {
_this.svgEls.cursor.setAttribute("stroke", "rgba(0,0,0,0)");
if (showCursorTimeoutID !== null) {
window.clearTimeout(showCursorTimeoutID);
showCursorTimeoutID = null;
}
showCursorTimeoutID = window.setTimeout(showCursor, 500);
}
// Finish editing
function finishEditing(ev) {
ev.preventDefault();
ev.stopPropagation();
document.removeEventListener("keydown", makeEdit);
document.removeEventListener("mousedown", finishEditing);
document.removeEventListener("touchstart", finishEditing);
if (showCursorTimeoutID !== null) {
window.clearTimeout(showCursorTimeoutID);
showCursorTimeoutID = null;
}
if (hideCursorTimeoutID !== null) {
window.clearTimeout(hideCursorTimeoutID);
hideCursorTimeoutID = null;
}
_this.svgEls.cursor.setAttribute("stroke", "rgba(0,0,0,0)");
_this.svgEls.cursor.setAttribute("fill", "rgba(0,0,0,0)");
_this.svg.removeEventListener("mousedown", checkDoubleTap);
_this.svg.removeEventListener("touchstart", checkDoubleTap);
_this.svg.addEventListener("mousedown", _this.handlers.touch);
_this.svg.addEventListener("touchstart", _this.handlers.touch);
if (ev.target === _this.svgEls.overlay) {
let charNum = _this._getSelectedCharNumber(ev.clientX, ev.clientY);
let charBRect = _this.svgEls.text.getExtentOfChar(Math.min(charNum, _this.svgEls.text.length - 1));
let editStr = str
// If the click is past the mid-point of the character, we select the next char, bounded by the length of the string
if (ev.clientX > ((charBRect.x + (charBRect.x + charBRect.width)) * 0.55)) {
charNum = charNum + 1;
}
_this.svgEls.text.textContent = _this._editText(editStr, charNum);
} else {
_this.setVal(parseFloat(str));
}
}
return str;
}
/**
* Returns the number of the selected character in the text box based on the client mouse x and y position.
* @private
* @param {number} clientX
* @param {number} clientY
* @returns {number} - Index of the selected digit.
*/
_getSelectedCharNumber(clientX, clientY) {
let svgBRect = this.svg.getBoundingClientRect();
let textBRect = this.svgEls.text.getBoundingClientRect();
let numChars = this.svgEls.text.getNumberOfChars();
let charNum = 0;
if (clientX <= textBRect.x) {
charNum = 0;
} else if (clientX >= (textBRect.x + textBRect.width)) {
charNum = numChars - 1;
} else {
let svgX = clientX - svgBRect.x;
let svgY = clientY - svgBRect.y;
let svgPoint = this.svg.createSVGPoint();
svgPoint.x = svgX;
svgPoint.y = svgY;
charNum = this.svgEls.text.getCharNumAtPosition(svgPoint);
}
// if we selected the "minus" sign of a negative number, select the first digit instead
if (this.getVal() < 0 && charNum == 0) {
charNum = 1;
}
return charNum;
}
/**
* Returns the power of the selected digit.
* @private
* @param {number} charNum - The index of the selected digit.
* @returns {number} - Power of the selected digit.
*/
_getPowerOfSelectedDigit(charNum) {
let power;
let precision = this.o.precision;
let numChars = this.svgEls.text.getNumberOfChars();
if (precision > 0) {
// if the digit selected is to the left of the decimal point
if ((numChars - charNum) > (this.o.precision + 1)) {
power = ((numChars - (precision + 1)) - charNum) - 1;
} else {
power = -1 * ((precision + 1) - (numChars - charNum));
}
} else {
power = (numChars - charNum) - 1;
}
return power;
}
}
export default Numberbox;