/** * SpinButton control. * * Adds bells and whistles to any ordinary textbox to * make it look and feel like a SpinButton Control. * * Supplies {@link external:jQuery.fn.SpinButton} (and also {@link external:jQuery.loadingStylesheets}). * * Originally written by George Adamson, Software Unity (george.jquery@softwareunity.com) August 2006: * - Added min/max options. * - Added step size option. * - Added bigStep (page up/down) option. * * Modifications made by Mark Gibson, (mgibson@designlinks.net) September 2006: * - Converted to jQuery plugin. * - Allow limited or unlimited min/max values. * - Allow custom class names, and add class to input element. * - Removed global vars. * - Reset (to original or through config) when invalid value entered. * - Repeat whilst holding mouse button down (with initial pause, like keyboard repeat). * - Support mouse wheel in Firefox. * - Fix double click in IE. * - Refactored some code and renamed some vars. * * Modifications by Jeff Schiller, June 2009: * - provide callback function for when the value changes based on the following * {@link https://www.mail-archive.com/jquery-en@googlegroups.com/msg36070.html}. * * Modifications by Jeff Schiller, July 2009: * - improve styling for widget in Opera. * - consistent key-repeat handling cross-browser. * * Modifications by Alexis Deveria, October 2009: * - provide "stepfunc" callback option to allow custom function to run when changing a value. * - Made adjustValue(0) only run on certain keyup events, not all. * * Tested in IE6, Opera9, Firefox 1.5. * * | Version | Date | Author | Notes | * |---------|------|--------|-------| * | v1.0 | 11 Aug 2006 | George Adamson | First release | * | v1.1 | Aug 2006 | George Adamson | Minor enhancements | * | v1.2 | 27 Sep 2006 | Mark Gibson | Major enhancements | * | v1.3a | 28 Sep 2006 | George Adamson | Minor enhancements | * | v1.4 | 18 Jun 2009 | Jeff Schiller | Added callback function | * | v1.5 | 06 Jul 2009 | Jeff Schiller | Fixes for Opera. | * | v1.6 | 13 Oct 2009 | Alexis Deveria | Added stepfunc function | * | v1.7 | 21 Oct 2009 | Alexis Deveria | Minor fixes.
Fast-repeat for keys and live updating as you type. | * | v1.8 | 12 Jan 2010 | Benjamin Thomas | Fixes for mouseout behavior.
Added smallStep | * | v1.9 | 20 May 2018 | Brett Zamir | Avoid SVGEdit dependency via `stateObj` config;
convert to ES6 module | * . * * @module jQuerySpinButton * @example // Create group of settings to initialise spinbutton(s). (Optional) const myOptions = { min: 0, // Set lower limit. max: 100, // Set upper limit. step: 1, // Set increment size. smallStep: 0.5, // Set shift-click increment size. stateObj: {tool_scale: 1}, // Object to allow passing in live-updating scale spinClass: mySpinBtnClass, // CSS class to style the spinbutton. (Class also specifies url of the up/down button image.) upClass: mySpinUpClass, // CSS class for style when mouse over up button. downClass: mySpinDnClass // CSS class for style when mouse over down button. }; $(function () { // Initialise INPUT element(s) as SpinButtons: (passing options if desired) $('#myInputElement').SpinButton(myOptions); }); */ /** * @function module:jQuerySpinButton.jQuerySpinButton * @param {external:jQuery} $ The jQuery object to which to add the plug-in * @returns {external:jQuery} */ export default function jQueryPluginSpinButton ($) { if (!$.loadingStylesheets) { $.loadingStylesheets = []; } const stylesheet = 'spinbtn/jQuery.SpinButton.css'; if (!$.loadingStylesheets.includes(stylesheet)) { $.loadingStylesheets.push(stylesheet); } /** * @callback module:jQuerySpinButton.StepCallback * @param {external:jQuery} thisArg Value of `this` * @param {Float} i Value to adjust * @returns {Float} */ /** * @callback module:jQuerySpinButton.ValueCallback * @param {external:jQuery.fn.SpinButton} thisArg Spin Button; check its `value` to see how it was changed. * @returns {void} */ /** * @typedef {PlainObject} module:jQuerySpinButton.SpinButtonConfig * @property {Float} min Set lower limit * @property {Float} max Set upper limit. * @property {Float} step Set increment size. * @property {module:jQuerySpinButton.StepCallback} stepfunc Custom function to run when changing a value; called with `this` of object and the value to adjust and returns a float. * @property {module:jQuerySpinButton.ValueCallback} callback Called after value adjusted (with `this` of object) * @property {Float} smallStep Set shift-click increment size. * @property {PlainObject} stateObj Object to allow passing in live-updating scale * @property {Float} stateObj.tool_scale * @property {string} spinClass CSS class to style the spinbutton. (Class also specifies url of the up/down button image.) * @property {string} upClass CSS class for style when mouse over up button. * @property {string} downClass CSS class for style when mouse over down button. * @property {Float} page Value to be adjusted on page up/page down * @property {Float} reset Reset value when invalid value entered * @property {Float} delay Millisecond delay * @property {Float} interval Millisecond interval */ /** * @function external:jQuery.fn.SpinButton * @param {module:jQuerySpinButton.SpinButtonConfig} cfg * @returns {external:jQuery} */ $.fn.SpinButton = function (cfg) { cfg = cfg || {}; /** * * @param {Element} el * @param {"offsetLeft"|"offsetTop"} prop * @returns {Integer} */ function coord (el, prop) { const b = document.body; let c = el[prop]; while ((el = el.offsetParent) && (el !== b)) { if (!$.browser.msie || (el.currentStyle.position !== 'relative')) { c += el[prop]; } } return c; } return this.each(function () { this.repeating = false; // Apply specified options or defaults: // (Ought to refactor this some day to use $.extend() instead) this.spinCfg = { // min: cfg.min ? Number(cfg.min) : null, // max: cfg.max ? Number(cfg.max) : null, min: !isNaN(parseFloat(cfg.min)) ? Number(cfg.min) : null, // Fixes bug with min:0 max: !isNaN(parseFloat(cfg.max)) ? Number(cfg.max) : null, step: cfg.step ? Number(cfg.step) : 1, stepfunc: cfg.stepfunc || false, page: cfg.page ? Number(cfg.page) : 10, upClass: cfg.upClass || 'up', downClass: cfg.downClass || 'down', reset: cfg.reset || this.value, delay: cfg.delay ? Number(cfg.delay) : 500, interval: cfg.interval ? Number(cfg.interval) : 100, _btn_width: 20, _direction: null, _delay: null, _repeat: null, callback: cfg.callback || null }; // if a smallStep isn't supplied, use half the regular step this.spinCfg.smallStep = cfg.smallStep || this.spinCfg.step / 2; this.adjustValue = function (i) { let v; if (isNaN(this.value)) { v = this.spinCfg.reset; } else if (typeof this.spinCfg.stepfunc === 'function') { v = this.spinCfg.stepfunc(this, i); } else { // weirdest JavaScript bug ever: 5.1 + 0.1 = 5.199999999 v = Number((Number(this.value) + Number(i)).toFixed(5)); } if (this.spinCfg.min !== null) { v = Math.max(v, this.spinCfg.min); } if (this.spinCfg.max !== null) { v = Math.min(v, this.spinCfg.max); } this.value = v; if (typeof this.spinCfg.callback === 'function') { this.spinCfg.callback(this); } }; $(this) .addClass(cfg.spinClass || 'spin-button') .mousemove(function (e) { // Determine which button mouse is over, or not (spin direction): const x = e.pageX || e.x; const y = e.pageY || e.y; const el = e.target; const scale = cfg.stateObj.tool_scale || 1; const height = $(el).height() / 2; const direction = (x > coord(el, 'offsetLeft') + el.offsetWidth * scale - this.spinCfg._btn_width) ? ((y < coord(el, 'offsetTop') + height * scale) ? 1 : -1) : 0; if (direction !== this.spinCfg._direction) { // Style up/down buttons: switch (direction) { case 1: // Up arrow: $(this).removeClass(this.spinCfg.downClass).addClass(this.spinCfg.upClass); break; case -1: // Down arrow: $(this).removeClass(this.spinCfg.upClass).addClass(this.spinCfg.downClass); break; default: // Mouse is elsewhere in the textbox $(this).removeClass(this.spinCfg.upClass).removeClass(this.spinCfg.downClass); } // Set spin direction: this.spinCfg._direction = direction; } }) .mouseout(function () { // Reset up/down buttons to their normal appearance when mouse moves away: $(this).removeClass(this.spinCfg.upClass).removeClass(this.spinCfg.downClass); this.spinCfg._direction = null; window.clearInterval(this.spinCfg._repeat); window.clearTimeout(this.spinCfg._delay); }) .mousedown(function (e) { if (e.button === 0 && this.spinCfg._direction !== 0) { // Respond to click on one of the buttons: const stepSize = e.shiftKey ? this.spinCfg.smallStep : this.spinCfg.step; const adjust = () => { this.adjustValue(this.spinCfg._direction * stepSize); }; adjust(); // Initial delay before repeating adjustment this.spinCfg._delay = window.setTimeout(() => { adjust(); // Repeat adjust at regular intervals this.spinCfg._repeat = window.setInterval(adjust, this.spinCfg.interval); }, this.spinCfg.delay); } }) .mouseup(function (e) { // Cancel repeating adjustment window.clearInterval(this.spinCfg._repeat); window.clearTimeout(this.spinCfg._delay); }) .dblclick(function (e) { if ($.browser.msie) { this.adjustValue(this.spinCfg._direction * this.spinCfg.step); } }) .keydown(function (e) { // Respond to up/down arrow keys. switch (e.keyCode) { case 38: this.adjustValue(this.spinCfg.step); break; // Up case 40: this.adjustValue(-this.spinCfg.step); break; // Down case 33: this.adjustValue(this.spinCfg.page); break; // PageUp case 34: this.adjustValue(-this.spinCfg.page); break; // PageDown } }) /* http://unixpapa.com/js/key.html describes the current state-of-affairs for key repeat events: - Safari 3.1 changed their model so that keydown is reliably repeated going forward - Firefox and Opera still only repeat the keypress event, not the keydown */ .keypress(function (e) { if (this.repeating) { // Respond to up/down arrow keys. switch (e.keyCode) { case 38: this.adjustValue(this.spinCfg.step); break; // Up case 40: this.adjustValue(-this.spinCfg.step); break; // Down case 33: this.adjustValue(this.spinCfg.page); break; // PageUp case 34: this.adjustValue(-this.spinCfg.page); break; // PageDown } // we always ignore the first keypress event (use the keydown instead) } else { this.repeating = true; } }) // clear the 'repeating' flag .keyup(function (e) { this.repeating = false; switch (e.keyCode) { case 38: // Up case 40: // Down case 33: // PageUp case 34: // PageDown case 13: this.adjustValue(0); break; // Enter/Return } }) .bind('mousewheel', function (e) { // Respond to mouse wheel in IE. (It returns up/dn motion in multiples of 120) if (e.wheelDelta >= 120) { this.adjustValue(this.spinCfg.step); } else if (e.wheelDelta <= -120) { this.adjustValue(-this.spinCfg.step); } e.preventDefault(); }) .change(function (e) { this.adjustValue(0); }); if (this.addEventListener) { // Respond to mouse wheel in Firefox this.addEventListener('DOMMouseScroll', function (e) { if (e.detail > 0) { this.adjustValue(-this.spinCfg.step); } else if (e.detail < 0) { this.adjustValue(this.spinCfg.step); } e.preventDefault(); }); } }); }; return $; }