| /** | 
|  * 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.<br />Fast-repeat for keys and live updating as you type. | 
|  * | v1.8 | 12 Jan 2010 | Benjamin Thomas | Fixes for mouseout behavior.<br />Added smallStep | 
|  * | v1.9 | 20 May 2018 | Brett Zamir | Avoid SVGEdit dependency via `stateObj` config;<br />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 ($) { | 
|   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` | 
|   */ | 
|   /** | 
|   * @callback module:jQuerySpinButton.ValueCallback | 
|   * @param {external:jQuery} thisArg Value of `this` | 
|   * @param {Float} value Value that was changed | 
|   */ | 
|   /** | 
|    * @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 | 
|    * @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 || {}; | 
|     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 $; | 
| } |