Source: jgraduate/jQuery.jGraduate.js

/**
 * jGraduate 0.4
 *
 * jQuery Plugin for a gradient picker
 *
 * @module jGraduate
 * @copyright 2010 Jeff Schiller {@link http://blog.codedread.com/}, 2010 Alexis Deveria {@link http://a.deveria.com/}
 *
 * @license Apache-2.0
 * @example
 * // The Paint object is described below.
 * $.jGraduate.Paint(); // constructs a 'none' color
 * @example $.jGraduate.Paint({copy: o}); // creates a copy of the paint o
 * @example $.jGraduate.Paint({hex: '#rrggbb'}); // creates a solid color paint with hex = "#rrggbb"
 * @example $.jGraduate.Paint({linearGradient: o, a: 50}); // creates a linear gradient paint with opacity=0.5
 * @example $.jGraduate.Paint({radialGradient: o, a: 7}); // creates a radial gradient paint with opacity=0.07
 * @example $.jGraduate.Paint({hex: '#rrggbb', linearGradient: o}); // throws an exception?
*/

/**
  * The jQuery namespace.
  * @external jQuery
*/
/**
 * The jQuery plugin namespace.
 * @namespace {PlainObject} fn
 * @memberof external:jQuery
 * @see {@link http://learn.jquery.com/plugins/|jQuery Plugins}
 */

const ns = {
  svg: 'http://www.w3.org/2000/svg',
  xlink: 'http://www.w3.org/1999/xlink'
};

if (!window.console) {
  window.console = {
    log (str) { /* */ },
    dir (str) { /* */ }
  };
}

/**
* Adds {@link external:jQuery.jGraduate.Paint},
* {@link external:jQuery.fn.jGraduateDefaults},
* {@link external:jQuery.fn.jGraduate}.
* @function module:jGraduate.jGraduate
* @param {external:jQuery} $ The jQuery instance to wrap
* @returns {external:jQuery}
*/
export default function jQueryPluginJGraduate ($) {
  if (!$.loadingStylesheets) {
    $.loadingStylesheets = [];
  }
  const stylesheet = 'jgraduate/css/jGraduate.css';
  if (!$.loadingStylesheets.includes(stylesheet)) {
    $.loadingStylesheets.push(stylesheet);
  }

  /**
  * @typedef {PlainObject} module:jGraduate.jGraduatePaintOptions
  * @param {Float} [alpha]
  * @param {module:jGraduate~Paint} [copy] Copy paint object
  * @param {SVGLinearGradientElement} [linearGradient]
  * @param {SVGRadialGradientElement} [radialGradient]
  * @param {string} [solidColor]
  */

  /**
  * @memberof module:jGraduate~
  */
  class Paint {
    /**
     * @param {module:jGraduate.jGraduatePaintOptions} [opt]
    */
    constructor (opt) {
      const options = opt || {};
      this.alpha = isNaN(options.alpha) ? 100 : options.alpha;
      // copy paint object
      if (options.copy) {
        /**
         * @name module:jGraduate~Paint#type
         * @type {"none"|"solidColor"|"linearGradient"|"radialGradient"}
         */
        this.type = options.copy.type;
        /**
         * Represents opacity (0-100)
         * @name module:jGraduate~Paint#alpha
         * @type {Float}
         */
        this.alpha = options.copy.alpha;
        /**
         * Represents #RRGGBB hex of color
         * @name module:jGraduate~Paint#solidColor
         * @type {string}
         */
        this.solidColor = null;
        /**
         * @name module:jGraduate~Paint#linearGradient
         * @type {SVGLinearGradientElement}
         */
        this.linearGradient = null;
        /**
         * @name module:jGraduate~Paint#radialGradient
         * @type {SVGRadialGradientElement}
         */
        this.radialGradient = null;

        switch (this.type) {
        case 'none':
          break;
        case 'solidColor':
          this.solidColor = options.copy.solidColor;
          break;
        case 'linearGradient':
          this.linearGradient = options.copy.linearGradient.cloneNode(true);
          break;
        case 'radialGradient':
          this.radialGradient = options.copy.radialGradient.cloneNode(true);
          break;
        }
      // create linear gradient paint
      } else if (options.linearGradient) {
        this.type = 'linearGradient';
        this.solidColor = null;
        this.radialGradient = null;
        this.linearGradient = options.linearGradient.cloneNode(true);
      // create linear gradient paint
      } else if (options.radialGradient) {
        this.type = 'radialGradient';
        this.solidColor = null;
        this.linearGradient = null;
        this.radialGradient = options.radialGradient.cloneNode(true);
      // create solid color paint
      } else if (options.solidColor) {
        this.type = 'solidColor';
        this.solidColor = options.solidColor;
      // create empty paint
      } else {
        this.type = 'none';
        this.solidColor = null;
        this.linearGradient = null;
        this.radialGradient = null;
      }
    }
  }
  /**
  * @namespace {PlainObject} jGraduate
  * @memberof external:jQuery
  */
  $.jGraduate = /** @lends external:jQuery.jGraduate */ {
    /**
    * @class external:jQuery.jGraduate.Paint
    * @see module:jGraduate~Paint
    */
    Paint
  };

  // JSDoc doesn't show this as belonging to our `module:jGraduate.Options` type,
  //   so we use `@see`
  /**
  * @namespace {module:jGraduate.Options} jGraduateDefaults
  * @memberof external:jQuery.fn
  */
  $.fn.jGraduateDefaults = /** @lends external:jQuery.fn.jGraduateDefaults */ {
    /**
    * Creates an object with a 'none' color
    * @type {external:jQuery.jGraduate.Paint}
    * @see module:jGraduate.Options
    */
    paint: new $.jGraduate.Paint(),
    /**
    * @namespace
    */
    window: {
      /**
      * @type {string}
      * @see module:jGraduate.Options
      */
      pickerTitle: 'Drag markers to pick a paint'
    },
    /**
    * @namespace
    */
    images: {
      /**
      * @type {string}
      * @see module:jGraduate.Options
      */
      clientPath: 'images/'
    },
    /**
    * @type {string}
    * @see module:jGraduate.Options
    */
    newstop: 'inverse' // same, inverse, black, white
  };

  const isGecko = navigator.userAgent.includes('Gecko/');

  /**
  * @typedef {PlainObject<string, string>} module:jGraduate.Attrs
  */
  /**
  * @param {SVGElement} elem
  * @param {module:jGraduate.Attrs} attrs
  * @returns {void}
  */
  function setAttrs (elem, attrs) {
    if (isGecko) {
      Object.entries(attrs).forEach(([aname, val]) => {
        elem.setAttribute(aname, val);
      });
    } else {
      Object.entries(attrs).forEach(([aname, val]) => {
        const prop = elem[aname];
        if (prop && prop.constructor === 'SVGLength') {
          prop.baseVal.value = val;
        } else {
          elem.setAttribute(aname, val);
        }
      });
    }
  }

  /**
  * @param {string} name
  * @param {module:jGraduate.Attrs} attrs
  * @param {Element} newparent
  * @returns {SVGElement}
  */
  function mkElem (name, attrs, newparent) {
    const elem = document.createElementNS(ns.svg, name);
    setAttrs(elem, attrs);
    if (newparent) {
      newparent.append(elem);
    }
    return elem;
  }

  /**
  * @typedef {PlainObject} module:jGraduate.ColorOpac Object may have one or both values
  * @property {string} [color] #Hex color
  * @property {Float} [opac] 0-1
  */
  /**
  * @typedef {PlainObject} module:jGraduate.Options
  * @property {module:jGraduate~Paint} [paint] A Paint object object describing the paint to display initially; defaults to a new instance without options (defaults to opaque white)
  * @property {external:Window} [window]
  * @property {string} [window.pickerTitle='Drag markers to pick a paint']
  * @property {PlainObject} [images]
  * @property {string} [images.clientPath='images/']
  * @property {"same"|"inverse"|"black"|"white"|module:jGraduate.ColorOpac} [newstop="inverse"]
  */

  /**
  * @callback external:jQuery.fn.jGraduate.OkCallback
  * @param {external:jQuery.jGraduate.Paint} paint
  * @returns {void}
  */
  /**
  * @callback external:jQuery.fn.jGraduate.CancelCallback
  * @returns {void}
  */

  /**
  * @function external:jQuery.fn.jGraduate
  * @param {module:jGraduate.Options} [options]
  * @param {external:jQuery.fn.jGraduate.OkCallback} [okCallback] Called with a Paint object when Ok is pressed
  * @param {external:jQuery.fn.jGraduate.CancelCallback} [cancelCallback] Called with no arguments when Cancel is pressed
  * @returns {external:jQuery}
  */
  $.fn.jGraduate = function (options, okCallback, cancelCallback) {
    return this.each(function () {
      const $this = $(this),
        $settings = $.extend(true, {}, $.fn.jGraduateDefaults, options || {}),
        id = $this.attr('id'),
        idref = '#' + $this.attr('id') + ' ';

      if (!idref) {
        /* await */ $.alert('Container element must have an id attribute to maintain unique id strings for sub-elements.');
        return;
      }

      const okClicked = function () {
        switch ($this.paint.type) {
        case 'radialGradient':
          $this.paint.linearGradient = null;
          break;
        case 'linearGradient':
          $this.paint.radialGradient = null;
          break;
        case 'solidColor':
          $this.paint.radialGradient = $this.paint.linearGradient = null;
          break;
        }
        typeof $this.okCallback === 'function' && $this.okCallback($this.paint);
        $this.hide();
      };
      const cancelClicked = function () {
        typeof $this.cancelCallback === 'function' && $this.cancelCallback();
        $this.hide();
      };

      $.extend(
        true,
        $this,
        // public properties, methods, and callbacks
        {
          // make a copy of the incoming paint
          paint: new $.jGraduate.Paint({copy: $settings.paint}),
          okCallback: typeof okCallback === 'function' ? okCallback : null,
          cancelCallback: typeof cancelCallback === 'function' ? cancelCallback : null
        }
      );

      let // pos = $this.position(),
        color = null;
      const $win = $(window);

      if ($this.paint.type === 'none') {
        $this.paint = new $.jGraduate.Paint({solidColor: 'ffffff'});
      }

      $this.addClass('jGraduate_Picker');
      $this.html(
        '<ul class="jGraduate_tabs">' +
          '<li class="jGraduate_tab_color jGraduate_tab_current" data-type="col">Solid Color</li>' +
          '<li class="jGraduate_tab_lingrad" data-type="lg">Linear Gradient</li>' +
          '<li class="jGraduate_tab_radgrad" data-type="rg">Radial Gradient</li>' +
        '</ul>' +
        '<div class="jGraduate_colPick"></div>' +
        '<div class="jGraduate_gradPick"></div>' +
        '<div class="jGraduate_LightBox"></div>' +
        '<div id="' + id + '_jGraduate_stopPicker" class="jGraduate_stopPicker"></div>'
      );
      const colPicker = $(idref + '> .jGraduate_colPick');
      const gradPicker = $(idref + '> .jGraduate_gradPick');

      gradPicker.html(
        '<div id="' + id + '_jGraduate_Swatch" class="jGraduate_Swatch">' +
          '<h2 class="jGraduate_Title">' + $settings.window.pickerTitle + '</h2>' +
          '<div id="' + id + '_jGraduate_GradContainer" class="jGraduate_GradContainer"></div>' +
          '<div id="' + id + '_jGraduate_StopSlider" class="jGraduate_StopSlider"></div>' +
        '</div>' +
        '<div class="jGraduate_Form jGraduate_Points jGraduate_lg_field">' +
          '<div class="jGraduate_StopSection">' +
            '<label class="jGraduate_Form_Heading">Begin Point</label>' +
            '<div class="jGraduate_Form_Section">' +
              '<label>x:</label>' +
              '<input type="text" id="' + id + '_jGraduate_x1" size="3" title="Enter starting x value between 0.0 and 1.0"/>' +
              '<label>y:</label>' +
              '<input type="text" id="' + id + '_jGraduate_y1" size="3" title="Enter starting y value between 0.0 and 1.0"/>' +
            '</div>' +
          '</div>' +
          '<div class="jGraduate_StopSection">' +
            '<label class="jGraduate_Form_Heading">End Point</label>' +
            '<div class="jGraduate_Form_Section">' +
              '<label>x:</label>' +
              '<input type="text" id="' + id + '_jGraduate_x2" size="3" title="Enter ending x value between 0.0 and 1.0"/>' +
              '<label>y:</label>' +
              '<input type="text" id="' + id + '_jGraduate_y2" size="3" title="Enter ending y value between 0.0 and 1.0"/>' +
            '</div>' +
          '</div>' +
        '</div>' +
        '<div class="jGraduate_Form jGraduate_Points jGraduate_rg_field">' +
          '<div class="jGraduate_StopSection">' +
            '<label class="jGraduate_Form_Heading">Center Point</label>' +
            '<div class="jGraduate_Form_Section">' +
              '<label>x:</label>' +
              '<input type="text" id="' + id + '_jGraduate_cx" size="3" title="Enter x value between 0.0 and 1.0"/>' +
              '<label>y:</label>' +
              '<input type="text" id="' + id + '_jGraduate_cy" size="3" title="Enter y value between 0.0 and 1.0"/>' +
            '</div>' +
          '</div>' +
          '<div class="jGraduate_StopSection">' +
            '<label class="jGraduate_Form_Heading">Focal Point</label>' +
            '<div class="jGraduate_Form_Section">' +
              '<label>Match center: <input type="checkbox" checked="checked" id="' + id + '_jGraduate_match_ctr"/></label><br/>' +
              '<label>x:</label>' +
              '<input type="text" id="' + id + '_jGraduate_fx" size="3" title="Enter x value between 0.0 and 1.0"/>' +
              '<label>y:</label>' +
              '<input type="text" id="' + id + '_jGraduate_fy" size="3" title="Enter y value between 0.0 and 1.0"/>' +
            '</div>' +
          '</div>' +
        '</div>' +
        '<div class="jGraduate_StopSection jGraduate_SpreadMethod">' +
          '<label class="jGraduate_Form_Heading">Spread method</label>' +
          '<div class="jGraduate_Form_Section">' +
            '<select class="jGraduate_spreadMethod">' +
              '<option value=pad selected>Pad</option>' +
              '<option value=reflect>Reflect</option>' +
              '<option value=repeat>Repeat</option>' +
            '</select>' +
          '</div>' +
        '</div>' +
        '<div class="jGraduate_Form">' +
          '<div class="jGraduate_Slider jGraduate_RadiusField jGraduate_rg_field">' +
            '<label class="prelabel">Radius:</label>' +
            '<div id="' + id + '_jGraduate_Radius" class="jGraduate_SliderBar jGraduate_Radius" title="Click to set radius">' +
              '<img id="' + id + '_jGraduate_RadiusArrows" class="jGraduate_RadiusArrows" src="' + $settings.images.clientPath + 'rangearrows2.gif">' +
            '</div>' +
            '<label><input type="text" id="' + id + '_jGraduate_RadiusInput" size="3" value="100"/>%</label>' +
          '</div>' +
          '<div class="jGraduate_Slider jGraduate_EllipField jGraduate_rg_field">' +
            '<label class="prelabel">Ellip:</label>' +
            '<div id="' + id + '_jGraduate_Ellip" class="jGraduate_SliderBar jGraduate_Ellip" title="Click to set Ellip">' +
              '<img id="' + id + '_jGraduate_EllipArrows" class="jGraduate_EllipArrows" src="' + $settings.images.clientPath + 'rangearrows2.gif">' +
            '</div>' +
            '<label><input type="text" id="' + id + '_jGraduate_EllipInput" size="3" value="0"/>%</label>' +
          '</div>' +
          '<div class="jGraduate_Slider jGraduate_AngleField jGraduate_rg_field">' +
            '<label class="prelabel">Angle:</label>' +
            '<div id="' + id + '_jGraduate_Angle" class="jGraduate_SliderBar jGraduate_Angle" title="Click to set Angle">' +
              '<img id="' + id + '_jGraduate_AngleArrows" class="jGraduate_AngleArrows" src="' + $settings.images.clientPath + 'rangearrows2.gif">' +
            '</div>' +
            '<label><input type="text" id="' + id + '_jGraduate_AngleInput" size="3" value="0"/>deg</label>' +
          '</div>' +
          '<div class="jGraduate_Slider jGraduate_OpacField">' +
            '<label class="prelabel">Opac:</label>' +
            '<div id="' + id + '_jGraduate_Opac" class="jGraduate_SliderBar jGraduate_Opac" title="Click to set Opac">' +
              '<img id="' + id + '_jGraduate_OpacArrows" class="jGraduate_OpacArrows" src="' + $settings.images.clientPath + 'rangearrows2.gif">' +
            '</div>' +
            '<label><input type="text" id="' + id + '_jGraduate_OpacInput" size="3" value="100"/>%</label>' +
          '</div>' +
        '</div>' +
        '<div class="jGraduate_OkCancel">' +
          '<input type="button" id="' + id + '_jGraduate_Ok" class="jGraduate_Ok" value="OK"/>' +
          '<input type="button" id="' + id + '_jGraduate_Cancel" class="jGraduate_Cancel" value="Cancel"/>' +
        '</div>'
      );

      // --------------
      // Set up all the SVG elements (the gradient, stops and rectangle)
      const MAX = 256,
        MARGINX = 0,
        MARGINY = 0,
        // STOP_RADIUS = 15 / 2,
        SIZEX = MAX - 2 * MARGINX,
        SIZEY = MAX - 2 * MARGINY;

      const attrInput = {};

      const SLIDERW = 145;
      $('.jGraduate_SliderBar').width(SLIDERW);

      const container = $('#' + id + '_jGraduate_GradContainer')[0];

      const svg = mkElem('svg', {
        id: id + '_jgraduate_svg',
        width: MAX,
        height: MAX,
        xmlns: ns.svg
      }, container);

      // This wasn't working as designed
      // let curType;
      // curType = curType || $this.paint.type;

      // if we are sent a gradient, import it
      let curType = $this.paint.type;

      let grad = $this.paint[curType];
      let curGradient = grad;

      const gradalpha = $this.paint.alpha;

      const isSolid = curType === 'solidColor';

      // Make any missing gradients
      switch (curType) {
      case 'solidColor':
        // fall through
      case 'linearGradient':
        if (!isSolid) {
          curGradient.id = id + '_lg_jgraduate_grad';
          grad = curGradient = svg.appendChild(curGradient); // .cloneNode(true));
        }
        mkElem('radialGradient', {
          id: id + '_rg_jgraduate_grad'
        }, svg);
        if (curType === 'linearGradient') { break; }
        // fall through
      case 'radialGradient':
        if (!isSolid) {
          curGradient.id = id + '_rg_jgraduate_grad';
          grad = curGradient = svg.appendChild(curGradient); // .cloneNode(true));
        }
        mkElem('linearGradient', {
          id: id + '_lg_jgraduate_grad'
        }, svg);
      }

      let stopGroup; // eslint-disable-line prefer-const
      if (isSolid) {
        grad = curGradient = $('#' + id + '_lg_jgraduate_grad')[0];
        color = $this.paint[curType];
        mkStop(0, '#' + color, 1);

        const type = typeof $settings.newstop;

        if (type === 'string') {
          switch ($settings.newstop) {
          case 'same':
            mkStop(1, '#' + color, 1);
            break;

          case 'inverse': {
            // Invert current color for second stop
            let inverted = '';
            for (let i = 0; i < 6; i += 2) {
              // const ch = color.substr(i, 2);
              let inv = (255 - parseInt(color.substr(i, 2), 16)).toString(16);
              if (inv.length < 2) inv = 0 + inv;
              inverted += inv;
            }
            mkStop(1, '#' + inverted, 1);
            break;
          } case 'white':
            mkStop(1, '#ffffff', 1);
            break;

          case 'black':
            mkStop(1, '#000000', 1);
            break;
          }
        } else if (type === 'object') {
          const opac = ('opac' in $settings.newstop) ? $settings.newstop.opac : 1;
          mkStop(1, ($settings.newstop.color || '#' + color), opac);
        }
      }

      const x1 = parseFloat(grad.getAttribute('x1') || 0.0),
        y1 = parseFloat(grad.getAttribute('y1') || 0.0),
        x2 = parseFloat(grad.getAttribute('x2') || 1.0),
        y2 = parseFloat(grad.getAttribute('y2') || 0.0);

      const cx = parseFloat(grad.getAttribute('cx') || 0.5),
        cy = parseFloat(grad.getAttribute('cy') || 0.5),
        fx = parseFloat(grad.getAttribute('fx') || cx),
        fy = parseFloat(grad.getAttribute('fy') || cy);

      const previewRect = mkElem('rect', {
        id: id + '_jgraduate_rect',
        x: MARGINX,
        y: MARGINY,
        width: SIZEX,
        height: SIZEY,
        fill: 'url(#' + id + '_jgraduate_grad)',
        'fill-opacity': gradalpha / 100
      }, svg);

      // stop visuals created here
      const beginCoord = $('<div/>').attr({
        class: 'grad_coord jGraduate_lg_field',
        title: 'Begin Stop'
      }).text(1).css({
        top: y1 * MAX,
        left: x1 * MAX
      }).data('coord', 'start').appendTo(container);

      const endCoord = beginCoord.clone().text(2).css({
        top: y2 * MAX,
        left: x2 * MAX
      }).attr('title', 'End stop').data('coord', 'end').appendTo(container);

      const centerCoord = $('<div/>').attr({
        class: 'grad_coord jGraduate_rg_field',
        title: 'Center stop'
      }).text('C').css({
        top: cy * MAX,
        left: cx * MAX
      }).data('coord', 'center').appendTo(container);

      const focusCoord = centerCoord.clone().text('F').css({
        top: fy * MAX,
        left: fx * MAX,
        display: 'none'
      }).attr('title', 'Focus point').data('coord', 'focus').appendTo(container);

      focusCoord[0].id = id + '_jGraduate_focusCoord';

      // const coords = $(idref + ' .grad_coord');

      // $(container).hover(function () {
      //   coords.animate({
      //     opacity: 1
      //   }, 500);
      // }, function () {
      //   coords.animate({
      //     opacity: .2
      //   }, 500);
      // });

      let showFocus;
      $.each(['x1', 'y1', 'x2', 'y2', 'cx', 'cy', 'fx', 'fy'], function (i, attr) {
        const isRadial = isNaN(attr[1]);

        let attrval = curGradient.getAttribute(attr);
        if (!attrval) {
          // Set defaults
          if (isRadial) {
            // For radial points
            attrval = '0.5';
          } else {
            // Only x2 is 1
            attrval = attr === 'x2' ? '1.0' : '0.0';
          }
        }

        attrInput[attr] = $('#' + id + '_jGraduate_' + attr)
          .val(attrval)
          .change(function () {
            // TODO: Support values < 0 and > 1 (zoomable preview?)
            if (isNaN(parseFloat(this.value)) || this.value < 0) {
              this.value = 0.0;
            } else if (this.value > 1) {
              this.value = 1.0;
            }

            if (!(attr[0] === 'f' && !showFocus)) {
              if ((isRadial && curType === 'radialGradient') || (!isRadial && curType === 'linearGradient')) {
                curGradient.setAttribute(attr, this.value);
              }
            }

            const $elem = isRadial
              ? attr[0] === 'c' ? centerCoord : focusCoord
              : attr[1] === '1' ? beginCoord : endCoord;

            const cssName = attr.includes('x') ? 'left' : 'top';

            $elem.css(cssName, this.value * MAX);
          }).change();
      });

      /**
       *
       * @param {Float} n
       * @param {Float|string} colr
       * @param {Float} opac
       * @param {boolean} [sel]
       * @param {SVGStopElement} [stopElem]
       * @returns {SVGStopElement}
       */
      function mkStop (n, colr, opac, sel, stopElem) {
        const stop = stopElem || mkElem('stop', {
          'stop-color': colr,
          'stop-opacity': opac,
          offset: n
        }, curGradient);
        if (stopElem) {
          colr = stopElem.getAttribute('stop-color');
          opac = stopElem.getAttribute('stop-opacity');
          n = stopElem.getAttribute('offset');
        } else {
          curGradient.append(stop);
        }
        if (opac === null) opac = 1;

        const pickerD = 'M-6.2,0.9c3.6-4,6.7-4.3,6.7-12.4c-0.2,7.9,3.1,8.8,6.5,12.4c3.5,3.8,2.9,9.6,0,12.3c-3.1,2.8-10.4,2.7-13.2,0C-9.6,9.9-9.4,4.4-6.2,0.9z';

        const pathbg = mkElem('path', {
          d: pickerD,
          fill: 'url(#jGraduate_trans)',
          transform: 'translate(' + (10 + n * MAX) + ', 26)'
        }, stopGroup);

        const path = mkElem('path', {
          d: pickerD,
          fill: colr,
          'fill-opacity': opac,
          transform: 'translate(' + (10 + n * MAX) + ', 26)',
          stroke: '#000',
          'stroke-width': 1.5
        }, stopGroup);

        $(path).mousedown(function (e) {
          selectStop(this);
          drag = curStop;
          $win.mousemove(dragColor).mouseup(remDrags);
          stopOffset = stopMakerDiv.offset();
          e.preventDefault();
          return false;
        }).data('stop', stop).data('bg', pathbg).dblclick(function () {
          $('div.jGraduate_LightBox').show();
          const colorhandle = this; // eslint-disable-line consistent-this
          let stopOpacity = Number(stop.getAttribute('stop-opacity')) || 1;
          let stopColor = stop.getAttribute('stop-color') || 1;
          let thisAlpha = (parseFloat(stopOpacity) * 255).toString(16);
          while (thisAlpha.length < 2) { thisAlpha = '0' + thisAlpha; }
          colr = stopColor.substr(1) + thisAlpha;
          $('#' + id + '_jGraduate_stopPicker').css({left: 100, bottom: 15}).jPicker({
            window: {title: 'Pick the start color and opacity for the gradient'},
            images: {clientPath: $settings.images.clientPath},
            color: {active: colr, alphaSupport: true}
          }, function (clr, arg2) {
            stopColor = clr.val('hex') ? ('#' + clr.val('hex')) : 'none';
            stopOpacity = clr.val('a') !== null ? clr.val('a') / 256 : 1;
            colorhandle.setAttribute('fill', stopColor);
            colorhandle.setAttribute('fill-opacity', stopOpacity);
            stop.setAttribute('stop-color', stopColor);
            stop.setAttribute('stop-opacity', stopOpacity);
            $('div.jGraduate_LightBox').hide();
            $('#' + id + '_jGraduate_stopPicker').hide();
          }, null, function () {
            $('div.jGraduate_LightBox').hide();
            $('#' + id + '_jGraduate_stopPicker').hide();
          });
        });

        $(curGradient).find('stop').each(function () {
          const curS = $(this);
          if (Number(this.getAttribute('offset')) > n) {
            if (!colr) {
              const newcolor = this.getAttribute('stop-color');
              const newopac = this.getAttribute('stop-opacity');
              stop.setAttribute('stop-color', newcolor);
              path.setAttribute('fill', newcolor);
              stop.setAttribute('stop-opacity', newopac === null ? 1 : newopac);
              path.setAttribute('fill-opacity', newopac === null ? 1 : newopac);
            }
            curS.before(stop);
            return false;
          }
          return true;
        });
        if (sel) selectStop(path);
        return stop;
      }

      /**
      *
      * @returns {void}
      */
      function remStop () {
        delStop.setAttribute('display', 'none');
        const path = $(curStop);
        const stop = path.data('stop');
        const bg = path.data('bg');
        $([curStop, stop, bg]).remove();
      }

      const stopMakerDiv = $('#' + id + '_jGraduate_StopSlider');

      let stops, curStop, drag;

      const delStop = mkElem('path', {
        d: 'm9.75,-6l-19.5,19.5m0,-19.5l19.5,19.5',
        fill: 'none',
        stroke: '#D00',
        'stroke-width': 5,
        display: 'none'
      }, undefined); // stopMakerSVG);

      /**
      * @param {Element} item
      * @returns {void}
      */
      function selectStop (item) {
        if (curStop) curStop.setAttribute('stroke', '#000');
        item.setAttribute('stroke', 'blue');
        curStop = item;
        //   stops = $('stop');
        //   opac_select.val(curStop.attr('fill-opacity') || 1);
        //   root.append(delStop);
      }

      let stopOffset;

      /**
      *
      * @returns {void}
      */
      function remDrags () {
        $win.unbind('mousemove', dragColor);
        if (delStop.getAttribute('display') !== 'none') {
          remStop();
        }
        drag = null;
      }

      let scaleX = 1, scaleY = 1, angle = 0;

      let cX = cx;
      let cY = cy;
      /**
      *
      * @returns {void}
      */
      function xform () {
        const rot = angle ? 'rotate(' + angle + ',' + cX + ',' + cY + ') ' : '';
        if (scaleX === 1 && scaleY === 1) {
          curGradient.removeAttribute('gradientTransform');
          // $('#ang').addClass('dis');
        } else {
          const x = -cX * (scaleX - 1);
          const y = -cY * (scaleY - 1);
          curGradient.setAttribute('gradientTransform', rot + 'translate(' + x + ',' + y + ') scale(' + scaleX + ',' + scaleY + ')');
          // $('#ang').removeClass('dis');
        }
      }

      /**
      * @param {Event} evt
      * @returns {void}
      */
      function dragColor (evt) {
        let x = evt.pageX - stopOffset.left;
        const y = evt.pageY - stopOffset.top;
        x = x < 10
          ? 10
          : x > MAX + 10
            ? MAX + 10
            : x;

        const xfStr = 'translate(' + x + ', 26)';
        if (y < -60 || y > 130) {
          delStop.setAttribute('display', 'block');
          delStop.setAttribute('transform', xfStr);
        } else {
          delStop.setAttribute('display', 'none');
        }

        drag.setAttribute('transform', xfStr);
        $.data(drag, 'bg').setAttribute('transform', xfStr);
        const stop = $.data(drag, 'stop');
        const sX = (x - 10) / MAX;

        stop.setAttribute('offset', sX);

        let last = 0;
        $(curGradient).find('stop').each(function (i) {
          const cur = this.getAttribute('offset');
          const t = $(this);
          if (cur < last) {
            t.prev().before(t);
            stops = $(curGradient).find('stop');
          }
          last = cur;
        });
      }

      const stopMakerSVG = mkElem('svg', {
        width: '100%',
        height: 45
      }, stopMakerDiv[0]);

      const transPattern = mkElem('pattern', {
        width: 16,
        height: 16,
        patternUnits: 'userSpaceOnUse',
        id: 'jGraduate_trans'
      }, stopMakerSVG);

      const transImg = mkElem('image', {
        width: 16,
        height: 16
      }, transPattern);

      const bgImage = $settings.images.clientPath + 'map-opacity.png';

      transImg.setAttributeNS(ns.xlink, 'xlink:href', bgImage);

      $(stopMakerSVG).click(function (evt) {
        stopOffset = stopMakerDiv.offset();
        const {target} = evt;
        if (target.tagName === 'path') return;
        let x = evt.pageX - stopOffset.left - 8;
        x = x < 10 ? 10 : x > MAX + 10 ? MAX + 10 : x;
        mkStop(x / MAX, 0, 0, true);
        evt.stopPropagation();
      });

      $(stopMakerSVG).mouseover(function () {
        stopMakerSVG.append(delStop);
      });

      stopGroup = mkElem('g', {}, stopMakerSVG);

      mkElem('line', {
        x1: 10,
        y1: 15,
        x2: MAX + 10,
        y2: 15,
        'stroke-width': 2,
        stroke: '#000'
      }, stopMakerSVG);

      const spreadMethodOpt = gradPicker.find('.jGraduate_spreadMethod').change(function () {
        curGradient.setAttribute('spreadMethod', $(this).val());
      });

      // handle dragging the stop around the swatch
      let draggingCoord = null;

      const onCoordDrag = function (evt) {
        let x = evt.pageX - offset.left;
        let y = evt.pageY - offset.top;

        // clamp stop to the swatch
        x = x < 0 ? 0 : x > MAX ? MAX : x;
        y = y < 0 ? 0 : y > MAX ? MAX : y;

        draggingCoord.css('left', x).css('top', y);

        // calculate stop offset
        const fracx = x / SIZEX;
        const fracy = y / SIZEY;

        const type = draggingCoord.data('coord');
        const grd = curGradient;

        switch (type) {
        case 'start':
          attrInput.x1.val(fracx);
          attrInput.y1.val(fracy);
          grd.setAttribute('x1', fracx);
          grd.setAttribute('y1', fracy);
          break;
        case 'end':
          attrInput.x2.val(fracx);
          attrInput.y2.val(fracy);
          grd.setAttribute('x2', fracx);
          grd.setAttribute('y2', fracy);
          break;
        case 'center':
          attrInput.cx.val(fracx);
          attrInput.cy.val(fracy);
          grd.setAttribute('cx', fracx);
          grd.setAttribute('cy', fracy);
          cX = fracx;
          cY = fracy;
          xform();
          break;
        case 'focus':
          attrInput.fx.val(fracx);
          attrInput.fy.val(fracy);
          grd.setAttribute('fx', fracx);
          grd.setAttribute('fy', fracy);
          xform();
        }

        evt.preventDefault();
      };

      const onCoordUp = function () {
        draggingCoord = null;
        $win.unbind('mousemove', onCoordDrag).unbind('mouseup', onCoordUp);
      };

      // Linear gradient
      // (function () {

      stops = curGradient.getElementsByTagNameNS(ns.svg, 'stop');

      let numstops = stops.length;
      // if there are not at least two stops, then
      if (numstops < 2) {
        while (numstops < 2) {
          curGradient.append(document.createElementNS(ns.svg, 'stop'));
          ++numstops;
        }
        stops = curGradient.getElementsByTagNameNS(ns.svg, 'stop');
      }

      for (let i = 0; i < numstops; i++) {
        mkStop(0, 0, 0, 0, stops[i]);
      }

      spreadMethodOpt.val(curGradient.getAttribute('spreadMethod') || 'pad');

      let offset;

      // No match, so show focus point
      showFocus = false;

      previewRect.setAttribute('fill-opacity', gradalpha / 100);

      $('#' + id + ' div.grad_coord').mousedown(function (evt) {
        evt.preventDefault();
        draggingCoord = $(this);
        // const sPos = draggingCoord.offset();
        offset = draggingCoord.parent().offset();
        $win.mousemove(onCoordDrag).mouseup(onCoordUp);
      });

      // bind GUI elements
      $('#' + id + '_jGraduate_Ok').bind('click', function () {
        $this.paint.type = curType;
        $this.paint[curType] = curGradient.cloneNode(true);
        $this.paint.solidColor = null;
        okClicked();
      });
      $('#' + id + '_jGraduate_Cancel').bind('click', function (paint) {
        cancelClicked();
      });

      if (curType === 'radialGradient') {
        if (showFocus) {
          focusCoord.show();
        } else {
          focusCoord.hide();
          attrInput.fx.val('');
          attrInput.fy.val('');
        }
      }

      $('#' + id + '_jGraduate_match_ctr')[0].checked = !showFocus;

      let lastfx, lastfy;

      $('#' + id + '_jGraduate_match_ctr').change(function () {
        showFocus = !this.checked;
        focusCoord.toggle(showFocus);
        attrInput.fx.val('');
        attrInput.fy.val('');
        const grd = curGradient;
        if (!showFocus) {
          lastfx = grd.getAttribute('fx');
          lastfy = grd.getAttribute('fy');
          grd.removeAttribute('fx');
          grd.removeAttribute('fy');
        } else {
          const fX = lastfx || 0.5;
          const fY = lastfy || 0.5;
          grd.setAttribute('fx', fX);
          grd.setAttribute('fy', fY);
          attrInput.fx.val(fX);
          attrInput.fy.val(fY);
        }
      });

      stops = curGradient.getElementsByTagNameNS(ns.svg, 'stop');
      numstops = stops.length;
      // if there are not at least two stops, then
      if (numstops < 2) {
        while (numstops < 2) {
          curGradient.append(document.createElementNS(ns.svg, 'stop'));
          ++numstops;
        }
        stops = curGradient.getElementsByTagNameNS(ns.svg, 'stop');
      }

      let slider;

      const setSlider = function (e) {
        const {offset: {left}} = slider;
        const div = slider.parent;
        let x = (e.pageX - left - parseInt(div.css('border-left-width')));
        if (x > SLIDERW) x = SLIDERW;
        if (x <= 0) x = 0;
        const posx = x - 5;
        x /= SLIDERW;

        switch (slider.type) {
        case 'radius':
          x = (x * 2) ** 2.5;
          if (x > 0.98 && x < 1.02) x = 1;
          if (x <= 0.01) x = 0.01;
          curGradient.setAttribute('r', x);
          break;
        case 'opacity':
          $this.paint.alpha = parseInt(x * 100);
          previewRect.setAttribute('fill-opacity', x);
          break;
        case 'ellip':
          scaleX = 1;
          scaleY = 1;
          if (x < 0.5) {
            x /= 0.5; // 0.001
            scaleX = x <= 0 ? 0.01 : x;
          } else if (x > 0.5) {
            x /= 0.5; // 2
            x = 2 - x;
            scaleY = x <= 0 ? 0.01 : x;
          }
          xform();
          x -= 1;
          if (scaleY === x + 1) {
            x = Math.abs(x);
          }
          break;
        case 'angle':
          x -= 0.5;
          angle = x *= 180;
          xform();
          x /= 100;
          break;
        }
        slider.elem.css({'margin-left': posx});
        x = Math.round(x * 100);
        slider.input.val(x);
      };

      let ellipVal = 0, angleVal = 0;

      if (curType === 'radialGradient') {
        const tlist = curGradient.gradientTransform.baseVal;
        if (tlist.numberOfItems === 2) {
          const t = tlist.getItem(0);
          const s = tlist.getItem(1);
          if (t.type === 2 && s.type === 3) {
            const m = s.matrix;
            if (m.a !== 1) {
              ellipVal = Math.round(-(1 - m.a) * 100);
            } else if (m.d !== 1) {
              ellipVal = Math.round((1 - m.d) * 100);
            }
          }
        } else if (tlist.numberOfItems === 3) {
          // Assume [R][T][S]
          const r = tlist.getItem(0);
          const t = tlist.getItem(1);
          const s = tlist.getItem(2);

          if (r.type === 4 &&
            t.type === 2 &&
            s.type === 3
          ) {
            angleVal = Math.round(r.angle);
            const m = s.matrix;
            if (m.a !== 1) {
              ellipVal = Math.round(-(1 - m.a) * 100);
            } else if (m.d !== 1) {
              ellipVal = Math.round((1 - m.d) * 100);
            }
          }
        }
      }

      const sliders = {
        radius: {
          handle: '#' + id + '_jGraduate_RadiusArrows',
          input: '#' + id + '_jGraduate_RadiusInput',
          val: (curGradient.getAttribute('r') || 0.5) * 100
        },
        opacity: {
          handle: '#' + id + '_jGraduate_OpacArrows',
          input: '#' + id + '_jGraduate_OpacInput',
          val: $this.paint.alpha || 100
        },
        ellip: {
          handle: '#' + id + '_jGraduate_EllipArrows',
          input: '#' + id + '_jGraduate_EllipInput',
          val: ellipVal
        },
        angle: {
          handle: '#' + id + '_jGraduate_AngleArrows',
          input: '#' + id + '_jGraduate_AngleInput',
          val: angleVal
        }
      };

      $.each(sliders, function (type, data) {
        const handle = $(data.handle);
        handle.mousedown(function (evt) {
          const parent = handle.parent();
          slider = {
            type,
            elem: handle,
            input: $(data.input),
            parent,
            offset: parent.offset()
          };
          $win.mousemove(dragSlider).mouseup(stopSlider);
          evt.preventDefault();
        });

        $(data.input).val(data.val).change(function () {
          const isRad = curType === 'radialGradient';
          let val = Number(this.value);
          let xpos = 0;
          switch (type) {
          case 'radius':
            if (isRad) curGradient.setAttribute('r', val / 100);
            xpos = (((val / 100) ** (1 / 2.5)) / 2) * SLIDERW;
            break;

          case 'opacity':
            $this.paint.alpha = val;
            previewRect.setAttribute('fill-opacity', val / 100);
            xpos = val * (SLIDERW / 100);
            break;

          case 'ellip':
            scaleX = scaleY = 1;
            if (val === 0) {
              xpos = SLIDERW * 0.5;
              break;
            }
            if (val > 99.5) val = 99.5;
            if (val > 0) {
              scaleY = 1 - (val / 100);
            } else {
              scaleX = -(val / 100) - 1;
            }

            xpos = SLIDERW * ((val + 100) / 2) / 100;
            if (isRad) xform();
            break;

          case 'angle':
            angle = val;
            xpos = angle / 180;
            xpos += 0.5;
            xpos *= SLIDERW;
            if (isRad) xform();
          }
          if (xpos > SLIDERW) {
            xpos = SLIDERW;
          } else if (xpos < 0) {
            xpos = 0;
          }
          handle.css({'margin-left': xpos - 5});
        }).change();
      });

      const dragSlider = function (evt) {
        setSlider(evt);
        evt.preventDefault();
      };

      const stopSlider = function (evt) {
        $win.unbind('mousemove', dragSlider).unbind('mouseup', stopSlider);
        slider = null;
      };

      // --------------
      let thisAlpha = ($this.paint.alpha * 255 / 100).toString(16);
      while (thisAlpha.length < 2) { thisAlpha = '0' + thisAlpha; }
      thisAlpha = thisAlpha.split('.')[0];
      color = $this.paint.solidColor === 'none' ? '' : $this.paint.solidColor + thisAlpha;

      if (!isSolid) {
        color = stops[0].getAttribute('stop-color');
      }

      // This should be done somewhere else, probably
      $.extend($.fn.jPicker.defaults.window, {
        alphaSupport: true, effects: {type: 'show', speed: 0}
      });

      colPicker.jPicker(
        {
          window: {title: $settings.window.pickerTitle},
          images: {clientPath: $settings.images.clientPath},
          color: {active: color, alphaSupport: true}
        },
        function (clr) {
          $this.paint.type = 'solidColor';
          $this.paint.alpha = clr.val('ahex') ? Math.round((clr.val('a') / 255) * 100) : 100;
          $this.paint.solidColor = clr.val('hex') ? clr.val('hex') : 'none';
          $this.paint.radialGradient = null;
          okClicked();
        },
        null,
        function () { cancelClicked(); }
      );

      const tabs = $(idref + ' .jGraduate_tabs li');
      tabs.click(function () {
        tabs.removeClass('jGraduate_tab_current');
        $(this).addClass('jGraduate_tab_current');
        $(idref + ' > div').hide();
        const type = $(this).attr('data-type');
        /* const container = */ $(idref + ' .jGraduate_gradPick').show();
        if (type === 'rg' || type === 'lg') {
          // Show/hide appropriate fields
          $('.jGraduate_' + type + '_field').show();
          $('.jGraduate_' + (type === 'lg' ? 'rg' : 'lg') + '_field').hide();

          $('#' + id + '_jgraduate_rect')[0].setAttribute('fill', 'url(#' + id + '_' + type + '_jgraduate_grad)');

          // Copy stops

          curType = type === 'lg' ? 'linearGradient' : 'radialGradient';

          $('#' + id + '_jGraduate_OpacInput').val($this.paint.alpha).change();

          const newGrad = $('#' + id + '_' + type + '_jgraduate_grad')[0];

          if (curGradient !== newGrad) {
            const curStops = $(curGradient).find('stop');
            $(newGrad).empty().append(curStops);
            curGradient = newGrad;
            const sm = spreadMethodOpt.val();
            curGradient.setAttribute('spreadMethod', sm);
          }
          showFocus = type === 'rg' && curGradient.getAttribute('fx') !== null && !(cx === fx && cy === fy);
          $('#' + id + '_jGraduate_focusCoord').toggle(showFocus);
          if (showFocus) {
            $('#' + id + '_jGraduate_match_ctr')[0].checked = false;
          }
        } else {
          $(idref + ' .jGraduate_gradPick').hide();
          $(idref + ' .jGraduate_colPick').show();
        }
      });
      $(idref + ' > div').hide();
      tabs.removeClass('jGraduate_tab_current');
      let tab;
      switch ($this.paint.type) {
      case 'linearGradient':
        tab = $(idref + ' .jGraduate_tab_lingrad');
        break;
      case 'radialGradient':
        tab = $(idref + ' .jGraduate_tab_radgrad');
        break;
      default:
        tab = $(idref + ' .jGraduate_tab_color');
        break;
      }
      $this.show();

      // jPicker will try to show after a 0ms timeout, so need to fire this after that
      setTimeout(() => {
        tab.addClass('jGraduate_tab_current').click();
      }, 10);
    });
  };
  return $;
}