text: /**
   * 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 $;
  }
  