Source: editor/components/jgraduate/jQuery.jGraduate.js

/* eslint-disable no-loop-func */
/* eslint-disable prefer-destructuring */
/* eslint-disable no-unsanitized/property */
/**
 * @file 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?
*/
import Paint from './paint.js';
import { jPickerDefaults, jPickerMethod } from './jQuery.jPicker.js';
import { findPos } from './Util.js';

/**
 * @todo JFH: This jQuery plugin was adapted to work within a Web Component.
 * We have to rewrite it as a pure webcomponent.
*/

/**
  * 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 () { /* empty fn */ },
    dir () { /* empty fn */ }
  };
}

/**
* 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 ($) {
/* eslint-disable jsdoc/require-property */
/**
* @namespace {PlainObject} jGraduate
* @memberof external:jQuery
*/
export const jGraduate = /** @lends external:jQuery.jGraduate */ {
  /* eslint-enable jsdoc/require-property */
  /**
  * @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
*/
export const 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}
*/
export function jGraduateMethod (elem, options, okCallback, cancelCallback, i18next) {
  const $this = elem;
  const $settings = Object.assign({}, jGraduateDefaults, options || {});
  const id = $this.getAttribute('id');
  const idref = '#' + $this.getAttribute('id') + ' ';

  if (!idref) {
    // eslint-disable-next-line no-alert
    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.style.display = 'none';
  };
  const cancelClicked = function () {
    typeof $this.cancelCallback === 'function' && $this.cancelCallback();
    $this.style.display = 'none';
  };
  Object.assign($this,     {
    // 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.classList.add('jGraduate_Picker');
  // eslint-disable-next-line no-unsanitized/property
  $this.innerHTML = `<ul class="jGraduate_tabs">
      <li class="jGraduate_tab_color jGraduate_tab_current" id="jGraduate_tab_color" data-type="col">${i18next.t('config.jgraduate_solid_color')}</li>
      <li class="jGraduate_tab_lingrad" id="jGraduate_tab_lingrad" data-type="lg">${i18next.t('config.jgraduate_linear_gradient')}</li>
      <li class="jGraduate_tab_radgrad" id="jGraduate_tab_radgrad" data-type="rg">${i18next.t('config.jgraduate_radial_gradient')}</li>
    </ul>
    <div class="jGraduate_colPick" id="jGraduate_colPick"></div>
    <div class="jGraduate_gradPick" id="jGraduate_gradPick"></div>
    <div class="jGraduate_LightBox" id="jGraduate_LightBox"></div>
    <div id="${id}_jGraduate_stopPicker" class="jGraduate_stopPicker"></div>`;
  /* JFH !!!! */
  const colPicker = $this.querySelector('#jGraduate_colPick');
  const gradPicker = $this.querySelector('#jGraduate_gradPick');
  // eslint-disable-next-line no-unsanitized/property
  const 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">${i18next.t('config.jgraduate_begin_point')}</label>
          <div class="jGraduate_Form_Section">
            <label>x:</label>
              <input type="text" id="${id}_jGraduate_x1" size="3" title="${i18next.t('config.jgraduate_enter_starting_x')}"/>
            <label>y:</label>
            <input type="text" id="${id}_jGraduate_y1" size="3" title="${i18next.t('config.jgraduate_enter_starting_y')}"/>
          </div>
        </div>
        <div class="jGraduate_StopSection">
          <label class="jGraduate_Form_Heading">${i18next.t('config.jgraduate_end_point')}</label>
          <div class="jGraduate_Form_Section">
            <label>x:</label>
            <input type="text" id="${id}_jGraduate_x2" size="3" title="${i18next.t('config.jgraduate_enter_ending_x')}"/>
            <label>y:</label>
            <input type="text" id="${id}_jGraduate_y2" size="3" title="${i18next.t('config.jgraduate_enter_ending_y')}"/>
          </div>
        </div>
      </div>
      <div class="jGraduate_Form jGraduate_Points jGraduate_rg_field">
        <div class="jGraduate_StopSection">
          <label class="jGraduate_Form_Heading">${i18next.t('config.jgraduate_center_point')}</label>
          <div class="jGraduate_Form_Section">
            <label>x:</label>
            <input type="text" id="${id}_jGraduate_cx" size="3" title="${i18next.t('config.jgraduate_enter_value_x')}"/>
            <label>y:</label>
            <input type="text" id="${id}_jGraduate_cy" size="3" title="${i18next.t('config.jgraduate_enter_value_y')}"/>
          </div>
        </div>
        <div class="jGraduate_StopSection">
          <label class="jGraduate_Form_Heading">${i18next.t('config.jgraduate_focal_point')}</label>
          <div class="jGraduate_Form_Section">
            <label>${i18next.t('config.jgraduate_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="${i18next.t('config.jgraduate_enter_focal_x')}"/>
            <label>y:</label>
            <input type="text" id="${id}_jGraduate_fy" size="3" title="${i18next.t('config.jgraduate_enter_focal_y')}"/>
          </div>
        </div>
      </div>
      <div class="jGraduate_StopSection jGraduate_SpreadMethod">
        <label class="jGraduate_Form_Heading">${i18next.t('config.jgraduate_spread_method')}</label>
        <div class="jGraduate_Form_Section">
          <select class="jGraduate_spreadMethod" id="jGraduate_spreadMethod">
            <option value=pad selected>${i18next.t('properties.jgraduate_pad')}</option>
            <option value=reflect>${i18next.t('properties.jgraduate_reflect')}</option>
            <option value=repeat>${i18next.t('properties.jgraduate_repeat')}</option>
          </select>
        </div>
      </div>
      <div class="jGraduate_Form">
        <div class="jGraduate_Slider jGraduate_RadiusField jGraduate_rg_field">
          <label class="prelabel">${i18next.t('config.jgraduate_radius')}</label>
          <div id="${id}_jGraduate_Radius" class="jGraduate_SliderBar jGraduate_Radius" title="${i18next.t('config.jgraduate_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">${i18next.t('config.jgraduate_ellip')}</label>
          <div id="${id}_jGraduate_Ellip" class="jGraduate_SliderBar jGraduate_Ellip" title="${i18next.t('config.jgraduate_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">${i18next.t('config.jgraduate_angle')}</label>
          <div id="${id}_jGraduate_Angle" class="jGraduate_SliderBar jGraduate_Angle" title="${i18next.t('config.jgraduate_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"/>${i18next.t('config.jgraduate_deg')}</label>
        </div>
        <div class="jGraduate_Slider jGraduate_OpacField">
          <label class="prelabel">${i18next.t('config.jgraduate_opac')}</label>
          <div id="${id}_jGraduate_Opac" class="jGraduate_SliderBar jGraduate_Opac" title="${i18next.t('config.jgraduate_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="${i18next.t("common.ok")}"/>
        <input type="button" id="${id}_jGraduate_Cancel" class="jGraduate_Cancel" value="${i18next.t("common.cancel")}"/>
      </div>`;
  const div = document.createElement('div');
  div.innerHTML = html;
  while (div.children.length > 0) {
    gradPicker.appendChild(div.children[0]);
  }
  /* eslint-enable max-len */
  // --------------
  // Set up all the SVG elements (the gradient, stops and rectangle)
  const MAX = 256;
  const MARGINX = 0;
  const MARGINY = 0;
  // STOP_RADIUS = 15 / 2,
  const SIZEX = MAX - 2 * MARGINX;
  const SIZEY = MAX - 2 * MARGINY;

  const attrInput = {};

  const SLIDERW = 145;
  const JQSliderBars = $this.querySelectorAll('.jGraduate_SliderBar');
  for (const JQSliderBar of JQSliderBars) {
    JQSliderBar.style.width = SLIDERW + 'px';
  }
  // JFH !!!!!!
  const container = $this.querySelector('#' + id + '_jGraduate_GradContainer');

  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);
    }
    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);
    }
    mkElem('linearGradient', {
      id: id + '_lg_jgraduate_grad'
    }, svg);
  }

  let stopGroup; // eslint-disable-line prefer-const
  if (isSolid) {
    // JFH !!!!!!!!
    grad = curGradient = $this.querySelector('#' + id + '_lg_jgraduate_grad');
    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 - Number.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 = Number.parseFloat(grad.getAttribute('x1') || 0.0);
  const y1 = Number.parseFloat(grad.getAttribute('y1') || 0.0);
  const x2 = Number.parseFloat(grad.getAttribute('x2') || 1.0);
  const y2 = Number.parseFloat(grad.getAttribute('y2') || 0.0);

  const cx = Number.parseFloat(grad.getAttribute('cx') || 0.5);
  const cy = Number.parseFloat(grad.getAttribute('cy') || 0.5);
  const fx = Number.parseFloat(grad.getAttribute('fx') || cx);
  const fy = Number.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 = document.createElement('div');
  beginCoord.setAttribute('class', 'grad_coord jGraduate_lg_field');
  beginCoord.setAttribute('title', 'Begin Stop');
  beginCoord.textContent = 1;
  beginCoord.style.top = y1 * MAX;
  beginCoord.style.left = x1 * MAX;
  beginCoord.dataset.coord = 'start';
  container.appendChild(beginCoord);

  const endCoord = document.createElement('div');
  endCoord.setAttribute('class', 'grad_coord jGraduate_lg_field');
  endCoord.setAttribute('title', 'End stop');
  endCoord.textContent = 2;
  endCoord.style.top = y2 * MAX;
  endCoord.style.left = x2 * MAX;
  endCoord.dataset.coord = 'end';
  container.appendChild(endCoord);

  const centerCoord = document.createElement('div');
  centerCoord.setAttribute('class', 'grad_coord jGraduate_rg_field');
  centerCoord.setAttribute('title', 'Center stop');
  centerCoord.textContent = 'C';
  centerCoord.style.top = cy * MAX;
  centerCoord.style.left = cx * MAX;
  centerCoord.dataset.coord = 'center';
  container.appendChild(centerCoord);

  const focusCoord = document.createElement('div');
  focusCoord.setAttribute('class', 'grad_coord jGraduate_rg_field');
  focusCoord.setAttribute('title', 'Focus point');
  focusCoord.textContent = 'F';
  focusCoord.style.top = fy * MAX;
  focusCoord.style.left = fx * MAX;
  focusCoord.style.display = 'none';
  focusCoord.dataset.coord = 'focus';
  focusCoord.setAttribute('id', id + '_jGraduate_focusCoord');
  container.appendChild(focusCoord);

  let showFocus;
  const onAttrChangeHandler = (e, attr, isRadial) => {
    // TODO: Support values < 0 and > 1 (zoomable preview?)
    if (isNaN(Number.parseFloat(e.target.value)) || e.target.value < 0) {
      e.target.value = 0.0;
    } else if (e.target.value > 1) {
      e.target.value = 1.0;
    }

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

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

    if (attr.includes('x') === 'left') {
      $elem.style.left = e.target.value * MAX;
    } else if (attr.includes('x') === 'top') {
      $elem.style.top = e.target.value * MAX;
    }
  };
  for (const [ , attr ] of [ 'x1', 'y1', 'x2', 'y2', 'cx', 'cy', 'fx', 'fy' ].entries()) {
    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] = $this.querySelector('#' + id + '_jGraduate_' + attr);
    attrInput[attr].value = attrval;
    attrInput[attr].addEventListener('change', (evt) => onAttrChangeHandler(evt, attr, isRadial));
    attrInput[attr].dispatchEvent(new Event('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', {
      id: 'jq_stop_' + Math.floor((Math.random() * 10000) + 1),
      '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.appendChild(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', {
      id: 'jq_pathbg_' + Math.floor((Math.random() * 10000) + 1),
      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.addEventListener('mousedown', function (e) {
      selectStop(this);
      drag = curStop;
      $win.addEventListener('mousemove', dragColor);
      $win.addEventListener('mouseup', remDrags);
      stopOffset = findPos(stopMakerDiv);
      e.preventDefault();
      return false;
    });
    path.dataset.stop = stop.getAttribute('id');
    path.dataset.bg = pathbg.getAttribute('id');
    path.addEventListener('dblclick', function () {
      $this.querySelector('#jGraduate_LightBox').style.display = 'block';
      const colorhandle = this;
      let stopOpacity = Number(stop.getAttribute('stop-opacity')) || 1;
      let stopColor = stop.getAttribute('stop-color') || 1;
      let thisAlpha = (Number.parseFloat(stopOpacity) * 255).toString(16);
      while (thisAlpha.length < 2) { thisAlpha = '0' + thisAlpha; }
      colr = stopColor.substr(1) + thisAlpha;
      const jqPickerElem = $this.querySelector('#' + id + '_jGraduate_stopPicker');
      jqPickerElem.style.left = '100px';
      jqPickerElem.style.bottom = '15px';
      jPickerMethod(jqPickerElem, {
        window: { title: 'Pick the start color and opacity for the gradient' },
        images: { clientPath: $settings.images.clientPath },
        color: { active: colr, alphaSupport: true }
      }, function (clr) {
        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);
        $this.querySelector('#jGraduate_LightBox').style.display = 'none';
        $this.querySelector('#' + id + '_jGraduate_stopPicker').style.display = 'none';
      }, null, function () {
        $this.querySelector('#jGraduate_LightBox').style.display = 'none';
        $this.querySelector('#' + id + '_jGraduate_stopPicker').style.display = 'none';
      },
      i18next
      );
    });
    const jqStopEls = curGradient.querySelectorAll('stop');
    for (const jqStopEl of jqStopEls) {
      const curS = jqStopEl;
      if (Number(jqStopEl.getAttribute('offset')) > n) {
        if (!colr) {
          const newcolor = jqStopEl.getAttribute('stop-color');
          const newopac = jqStopEl.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.insertAdjacentElement('beforebegin', stop);
        // curS.before(stop);
        // return false;
      }
      // return true;
    }
    if (sel) selectStop(path);
    return stop;
  }

  /**
  *
  * @returns {void}
  */
  function remStop () {
    delStop.setAttribute('display', 'none');
    const path = curStop;
    delete path.dataset.stop;
    delete path.dataset.bg;
    curStop.parentNode.removeChild(curStop);
  }

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

  let stops; let curStop; let 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;
  }

  let stopOffset;

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

  let scaleX = 1; let scaleY = 1; let 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');
    } else {
      const x = -cX * (scaleX - 1);
      const y = -cY * (scaleY - 1);
      curGradient.setAttribute(
        'gradientTransform',
        rot + 'translate(' + x + ',' + y + ') scale(' +
          scaleX + ',' + scaleY + ')'
      );
    }
  }

  /**
  * @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);
    const jqpgpath = $this.querySelector('#'+drag.dataset.bg);
    jqpgpath.setAttribute('transform', xfStr);
    const stop = $this.querySelector('#'+drag.dataset.stop);
    const sX = (x - 10) / MAX;

    stop.setAttribute('offset', sX);

    let last = 0;
    const jqStopElems = curGradient.querySelectorAll('stop');
    [].forEach.call(jqStopElems, function (jqStopElem) {
      const cur = jqStopElem.getAttribute('offset');
      const t = jqStopElem;
      if (cur < last) {
        t.previousElementSibling.insertAdjacentElement('beforebegin', t);
        stops = curGradient.querySelectorAll('stop');
      }
      last = cur;
    });
  }

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

  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.addEventListener('click', function (evt) {
    stopOffset = findPos(stopMakerDiv);
    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.addEventListener('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.querySelector('#jGraduate_spreadMethod');
  spreadMethodOpt.addEventListener('change', function () {
    curGradient.setAttribute('spreadMethod', this.value);
  });

  // 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.style.left = x + 'px';
    draggingCoord.style.top = y + 'px';

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

    const type = draggingCoord.dataset.coord;
    const grd = curGradient;

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

    evt.preventDefault();
  };

  const onCoordUp = function () {
    draggingCoord = null;
    $win.removeEventListener('mousemove', onCoordDrag);
    $win.removeEventListener('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.setAttribute('value', curGradient.getAttribute('spreadMethod') || 'pad');

  let offset;

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

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

  const JQGradCoords = $this.querySelectorAll('#' + id + ' div.grad_coord');
  const onMouseDownGradCoords = (e) => {
    e.preventDefault();
    draggingCoord = e.target;
    offset = findPos(draggingCoord.parentNode);
    $win.addEventListener('mousemove', onCoordDrag);
    $win.addEventListener('mouseup', onCoordUp);
  };
  for (const JQGradCoord of JQGradCoords) {
    JQGradCoord.addEventListener('mousedown', onMouseDownGradCoords);
  }

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

  if (curType === 'radialGradient') {
    if (showFocus) {
      focusCoord.style.display = 'block';
    } else {
      focusCoord.style.display = 'none';
      attrInput.fx.value = '';
      attrInput.fy.value = '';
    }
  }

  $this.querySelector('#' + id + '_jGraduate_match_ctr').checked = !showFocus;

  let lastfx; let lastfy;
  const onMatchCtrHandler = (e) => {
    showFocus = !e.target.checked;
    if (showFocus) {
      focusCoord.style.display = 'block';
    } else {
      focusCoord.style.display = 'none';
    }
    attrInput.fx.value = '';
    attrInput.fy.value = '';
    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.value = fX;
      attrInput.fy.value = fY;
    }
  };
  $this.querySelector('#' + id + '_jGraduate_match_ctr').addEventListener('change', onMatchCtrHandler);
  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 divi = slider.parent;
    let x = (e.pageX - left - Number.parseInt(getComputedStyle(divi, null).getPropertyValue('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 = Number.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.style.marginLeft = posx + 'px';
    x = Math.round(x * 100);
    slider.input.value = x;
  };

  let ellipVal = 0; let 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
    }
  };
  for (const [ , [ type, data ] ] of Object.entries(Object.entries(sliders))) {
    const handle = $this.querySelector(data.handle);
    const sInput = $this.querySelector(data.input);
    handle.addEventListener('mousedown', function (evt) {
      const parent = handle.parentNode;
      slider = {
        type,
        elem: handle,
        input: sInput,
        parent,
        offset: findPos(parent)
      };
      $win.addEventListener('mousemove', dragSlider);
      $win.addEventListener('mouseup', stopSlider);
      evt.preventDefault();
    });
    sInput.value = data.val;
    sInput.addEventListener('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.style.marginLeft = (xpos - 5) + 'px';
    });
    sInput.dispatchEvent(new Event('change'));
  }

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

  const stopSlider = function () {
    $win.removeEventListener('mousemove', dragSlider);
    $win.removeEventListener('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
  Object.assign(jPickerDefaults.window, {
    alphaSupport: true, effects: { type: 'show', speed: 0 }
  });

  jPickerMethod(
    colPicker,
    {
      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(); },
    i18next
  );

  // JFH !!!!
  const tabs = $this.querySelectorAll('.jGraduate_tabs li');
  const onTabsClickHandler = (e) => {
    for (const tab of tabs) {
      tab.classList.remove('jGraduate_tab_current');
    }
    e.target.classList.add('jGraduate_tab_current');
    const innerDivs = $this.querySelectorAll(idref + ' > div');
    [].forEach.call(innerDivs, function (innerDiv) {
      innerDiv.style.display = 'none';
    });
    const type = e.target.dataset.type;
    gradPicker.style.display = 'block';
    if (type === 'rg' || type === 'lg') {
      const tFileds = $this.querySelectorAll('.jGraduate_' + type + '_field');
      [].forEach.call(tFileds, function (tFiled) {
        tFiled.style.display = 'block';
      });
      const t1Fileds = $this.querySelectorAll('.jGraduate_' + (type === 'lg' ? 'rg' : 'lg') + '_field');
      [].forEach.call(t1Fileds, function (tFiled) {
        tFiled.style.display = 'none';
      });
      $this.querySelectorAll('#' + id + '_jgraduate_rect')[0]
        .setAttribute('fill', 'url(#' + id + '_' + type + '_jgraduate_grad)');
      curType = type === 'lg' ? 'linearGradient' : 'radialGradient';
      const jOpacInput = $this.querySelector('#' + id + '_jGraduate_OpacInput');
      jOpacInput.value = $this.paint.alpha;
      jOpacInput.dispatchEvent(new Event('change'));
      const newGrad = $this.querySelectorAll('#' + id + '_' + type + '_jgraduate_grad')[0];
      if (curGradient !== newGrad) {
        const curStops = curGradient.querySelectorAll('stop');
        while (newGrad.firstChild) {
          newGrad.removeChild(newGrad.firstChild);
        }
        [].forEach.call(curStops, function (curS) {
          newGrad.appendChild(curS);
        });
        curGradient = newGrad;
        const sm = spreadMethodOpt.getAttribute('value');
        curGradient.setAttribute('spreadMethod', sm);
      }
      showFocus = type === 'rg' && curGradient.getAttribute('fx') !== null && !(cx === fx && cy === fy);
      const jQfocusCoord = $this.querySelectorAll('#' + id + '_jGraduate_focusCoord');
      if (jQfocusCoord[0].style.display === 'none') {
        jQfocusCoord[0].style.display = 'block';
      } else {
        jQfocusCoord[0].style.display = 'none';
      }
      if (showFocus) {
        $this.querySelectorAll('#' + id + '_jGraduate_match_ctr')[0].checked = false;
      }
    } else {
      gradPicker.style.display = 'none';
      colPicker.style.display = 'block';
    }
  };
  for (const tab of tabs) {
    tab.addEventListener('click', onTabsClickHandler);
  }
  const innerDivs = $this.querySelectorAll(idref + ' > div');
  [].forEach.call(innerDivs, function (innerDiv) {
    innerDiv.style.display = 'none';
  });
  for (const tab of tabs) {
    tab.classList.remove('jGraduate_tab_current');
  }
  let tab;
  switch ($this.paint.type) {
  case 'linearGradient':
    tab = $this.querySelector(idref + ' .jGraduate_tab_lingrad');
    break;
  case 'radialGradient':
    tab = $this.querySelector(idref + ' .jGraduate_tab_radgrad');
    break;
  default:
    tab = $this.querySelector(idref + ' .jGraduate_tab_color');
    break;
  }
  $this.style.display = 'block';

  // jPicker will try to show after a 0ms timeout, so need to fire this after that
  setTimeout(() => {
    tab.classList.add('jGraduate_tab_current');
    tab.dispatchEvent(new Event('click'));
  }, 10);
}