df2profiler map drawer - Dead Frontier 2

a simple map drawer for df2profiler.com by clicking on the map to draw color on area

// ==UserScript==
// @name         df2profiler map drawer - Dead Frontier 2
// @name:zh-TW   df2profiler 地圖繪製器 - 死亡邊境 2
// @namespace    http://tampermonkey.net/
// @version      0.5.0
// @description  a simple map drawer for df2profiler.com by clicking on the map to draw color on area
// @description:zh-TW 透過點擊地圖來繪製顏色的簡易地圖繪製器
// @author       Archer_Wn
// @match        https://df2profiler.com/gamemap/
// @icon         https://www.google.com/s2/favicons?sz=64&domain=df2profiler.com
// @grant        none
// @license      GPL-3.0
// ==/UserScript==

// Options
let color = "rgba(255, 255, 0, 0.3)"; // default color for drawing
const pvpColor = "rgba(255, 0, 0, 0.4)"; // color for pvp area
const outpostColor = "rgba(0, 255, 0, 0.4)"; // color for outpost area
const chunkSize = 6; // echo chunk have (chunkSize * chunkSize) cells

// Main
let mouseDown = false;
let drawingMode = false;
let drewCells = [];
function onPickerInput(value) {
  color = value;
}

function onSavedMap() {
  const tbody = document.querySelector("#map tbody");

  const backgroundImgUrl = window
    .getComputedStyle(document.querySelector("#map"))
    .getPropertyValue("background-image");
  const canvas = document.createElement("canvas");
  canvas.width = tbody.clientWidth;
  canvas.height = tbody.clientHeight;
  const ctx = canvas.getContext("2d");
  const img = new Image();
  img.src = backgroundImgUrl.slice(5, -2);

  img.onload = () => {
    // draw background image
    ctx.drawImage(img, 0, 0, canvas.width, canvas.height);

    // draw pvp area
    const pvpCells = tbody.querySelectorAll(".pvpZone");
    pvpCells.forEach((cell) => {
      const rect = cell.getBoundingClientRect();
      const x = rect.left - tbody.getBoundingClientRect().left;
      const y = rect.top - tbody.getBoundingClientRect().top;
      ctx.fillStyle = pvpColor;
      ctx.fillRect(x, y, rect.width, rect.height);
    });

    // draw outpost area
    const outpostCells = tbody.querySelectorAll(".outpost");
    outpostCells.forEach((cell) => {
      const rect = cell.getBoundingClientRect();
      const x = rect.left - tbody.getBoundingClientRect().left;
      const y = rect.top - tbody.getBoundingClientRect().top;
      ctx.fillStyle = outpostColor;
      ctx.fillRect(x, y, rect.width, rect.height);
    });

    // draw area
    const cells = tbody.querySelectorAll("td");
    cells.forEach((cell) => {
      if (cell.style.backgroundColor === "") return;
      const rect = cell.getBoundingClientRect();
      const x = rect.left - tbody.getBoundingClientRect().left;
      const y = rect.top - tbody.getBoundingClientRect().top;
      ctx.fillStyle = cell.style.backgroundColor;
      ctx.fillRect(x, y, rect.width, rect.height);
    });

    // draw grid (6x6 cells)
    const td = tbody.querySelector("td");
    const chunkWidth = td.clientWidth * chunkSize;
    const chunkHeight = td.clientHeight * chunkSize;
    ctx.strokeStyle = "white";
    ctx.lineWidth = 1;
    ctx.beginPath();
    for (let x = chunkWidth; x < canvas.width; x += chunkWidth) {
      ctx.moveTo(x, 0);
      ctx.lineTo(x, canvas.height);
    }
    for (let y = chunkHeight; y < canvas.height; y += chunkHeight) {
      ctx.moveTo(0, y);
      ctx.lineTo(canvas.width, y);
    }
    ctx.stroke();

    // download image
    const a = document.createElement("a");
    a.href = canvas.toDataURL("image/png");
    a.download = "map.png";
    a.click();
  };
}

(function () {
  "use strict";

  const tbody = document.querySelector("#map tbody");
  tbody.addEventListener("click", (e) => {
    const cell = e.target.closest("td");
    if (!cell) return;

    if (drawingMode) {
      cell.style.backgroundColor = color;
    } else {
      cell.style.backgroundColor = "";
    }
  });
  tbody.addEventListener("mousedown", (e) => {
    mouseDown = true;
    const cell = e.target.closest("td");
    if (!cell) return;
    if (cell.style.backgroundColor === "") {
      drawingMode = true;
    } else {
      drawingMode = false;
    }
  });
  tbody.addEventListener("mousemove", (e) => {
    if (!mouseDown) return;
    const cell = e.target.closest("td");
    if (!cell) return;
    if (drewCells.includes(cell)) return;

    if (drawingMode) {
      cell.style.backgroundColor = color;
    } else {
      cell.style.backgroundColor = "";
    }
    drewCells.push(cell);
  });
  tbody.addEventListener("mouseup", (e) => {
    mouseDown = false;
    drewCells = [];
  });

  const navbarLinks = document.querySelector("#navbar-links");

  // color picker (rgba)
  const colorPicker = document.createElement("input");
  colorPicker.value = color;
  colorPicker.style.padding = "0 0.5rem";
  colorPicker.style.margin = "0.3rem 1rem";
  colorPicker.style.borderRadius =
    navbarLinks.getBoundingClientRect().height / 2 + "px";
  colorPicker.style.marginLeft = "auto";
  colorPicker.style.cursor = "pointer";
  colorPicker.setAttribute("data-jscolor", {});
  colorPicker.addEventListener("input", function () {
    onPickerInput(this.jscolor.toRGBAString());
  });
  navbarLinks.appendChild(colorPicker);

  // screenshot button
  const screenshotBtn = document.createElement("button");
  screenshotBtn.textContent = "Screenshot Map";
  screenshotBtn.style.padding = "0 1rem";
  screenshotBtn.style.margin = "0.3rem 0rem";
  screenshotBtn.style.borderRadius =
    navbarLinks.getBoundingClientRect().height / 2 + "px";
  screenshotBtn.style.cursor = "pointer";
  navbarLinks.appendChild(screenshotBtn);
  screenshotBtn.addEventListener("click", onSavedMap);
})();

/**
 * jscolor - JavaScript Color Picker
 *
 * @link    http://jscolor.com
 * @license For open source use: GPLv3
 *          For commercial use: JSColor Commercial License
 * @author  Jan Odvarko - East Desire
 * @version 2.5.2
 *
 * See usage examples at http://jscolor.com/examples/
 */

(function (global, factory) {
  "use strict";

  if (typeof module === "object" && typeof module.exports === "object") {
    // Export jscolor as a module
    module.exports = global.document
      ? factory(global)
      : function (win) {
          if (!win.document) {
            throw new Error("jscolor needs a window with document");
          }
          return factory(win);
        };
    return;
  }

  // Default use (no module export)
  factory(global);
})(typeof window !== "undefined" ? window : this, function (window) {
  // BEGIN factory

  // BEGIN jscolor code

  "use strict";

  var jscolor = (function () {
    // BEGIN jscolor

    var jsc = {
      initialized: false,

      instances: [], // created instances of jscolor

      readyQueue: [], // functions waiting to be called after init

      register: function () {
        if (typeof window !== "undefined" && window.document) {
          if (window.document.readyState !== "loading") {
            jsc.pub.init();
          } else {
            window.document.addEventListener(
              "DOMContentLoaded",
              jsc.pub.init,
              false
            );
          }
        }
      },

      installBySelector: function (selector, rootNode) {
        rootNode = rootNode ? jsc.node(rootNode) : window.document;
        if (!rootNode) {
          throw new Error("Missing root node");
        }

        var elms = rootNode.querySelectorAll(selector);

        // for backward compatibility with DEPRECATED installation/configuration using className
        var matchClass = new RegExp(
          "(^|\\s)(" + jsc.pub.lookupClass + ")(\\s*(\\{[^}]*\\})|\\s|$)",
          "i"
        );

        for (var i = 0; i < elms.length; i += 1) {
          if (elms[i].jscolor && elms[i].jscolor instanceof jsc.pub) {
            continue; // jscolor already installed on this element
          }

          if (
            elms[i].type !== undefined &&
            elms[i].type.toLowerCase() == "color" &&
            jsc.isColorAttrSupported
          ) {
            continue; // skips inputs of type 'color' if supported by the browser
          }

          var dataOpts, m;

          if (
            (dataOpts = jsc.getDataAttr(elms[i], "jscolor")) !== null ||
            (elms[i].className && (m = elms[i].className.match(matchClass))) // installation using className (DEPRECATED)
          ) {
            var targetElm = elms[i];

            var optsStr = "";
            if (dataOpts !== null) {
              optsStr = dataOpts;
            } else if (m) {
              // installation using className (DEPRECATED)
              console.warn(
                'Installation using class name is DEPRECATED. Use data-jscolor="" attribute instead.' +
                  jsc.docsRef
              );
              if (m[4]) {
                optsStr = m[4];
              }
            }

            var opts = null;
            if (optsStr.trim()) {
              try {
                opts = jsc.parseOptionsStr(optsStr);
              } catch (e) {
                console.warn(e + "\n" + optsStr);
              }
            }

            try {
              new jsc.pub(targetElm, opts);
            } catch (e) {
              console.warn(e);
            }
          }
        }
      },

      parseOptionsStr: function (str) {
        var opts = null;

        try {
          opts = JSON.parse(str);
        } catch (eParse) {
          if (!jsc.pub.looseJSON) {
            throw new Error(
              "Could not parse jscolor options as JSON: " + eParse
            );
          } else {
            // loose JSON syntax is enabled -> try to evaluate the options string as JavaScript object
            try {
              opts = new Function(
                "var opts = (" +
                  str +
                  '); return typeof opts === "object" ? opts : {};'
              )();
            } catch (eEval) {
              throw new Error("Could not evaluate jscolor options: " + eEval);
            }
          }
        }
        return opts;
      },

      getInstances: function () {
        var inst = [];
        for (var i = 0; i < jsc.instances.length; i += 1) {
          // if the targetElement still exists, the instance is considered "alive"
          if (jsc.instances[i] && jsc.instances[i].targetElement) {
            inst.push(jsc.instances[i]);
          }
        }
        return inst;
      },

      createEl: function (tagName) {
        var el = window.document.createElement(tagName);
        jsc.setData(el, "gui", true);
        return el;
      },

      node: function (nodeOrSelector) {
        if (!nodeOrSelector) {
          return null;
        }

        if (typeof nodeOrSelector === "string") {
          // query selector
          var sel = nodeOrSelector;
          var el = null;
          try {
            el = window.document.querySelector(sel);
          } catch (e) {
            console.warn(e);
            return null;
          }
          if (!el) {
            console.warn("No element matches the selector: %s", sel);
          }
          return el;
        }

        if (jsc.isNode(nodeOrSelector)) {
          // DOM node
          return nodeOrSelector;
        }

        console.warn(
          "Invalid node of type %s: %s",
          typeof nodeOrSelector,
          nodeOrSelector
        );
        return null;
      },

      // See https://stackoverflow.com/questions/384286/
      isNode: function (val) {
        if (typeof Node === "object") {
          return val instanceof Node;
        }
        return (
          val &&
          typeof val === "object" &&
          typeof val.nodeType === "number" &&
          typeof val.nodeName === "string"
        );
      },

      nodeName: function (node) {
        if (node && node.nodeName) {
          return node.nodeName.toLowerCase();
        }
        return false;
      },

      removeChildren: function (node) {
        while (node.firstChild) {
          node.removeChild(node.firstChild);
        }
      },

      isTextInput: function (el) {
        return (
          el && jsc.nodeName(el) === "input" && el.type.toLowerCase() === "text"
        );
      },

      isButton: function (el) {
        if (!el) {
          return false;
        }
        var n = jsc.nodeName(el);
        return (
          n === "button" ||
          (n === "input" &&
            ["button", "submit", "reset"].indexOf(el.type.toLowerCase()) > -1)
        );
      },

      isButtonEmpty: function (el) {
        switch (jsc.nodeName(el)) {
          case "input":
            return !el.value || el.value.trim() === "";
          case "button":
            return el.textContent.trim() === "";
        }
        return null; // could not determine element's text
      },

      // See https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md
      isPassiveEventSupported: (function () {
        var supported = false;

        try {
          var opts = Object.defineProperty({}, "passive", {
            get: function () {
              supported = true;
            },
          });
          window.addEventListener("testPassive", null, opts);
          window.removeEventListener("testPassive", null, opts);
        } catch (e) {}

        return supported;
      })(),

      isColorAttrSupported: (function () {
        var elm = window.document.createElement("input");
        if (elm.setAttribute) {
          elm.setAttribute("type", "color");
          if (elm.type.toLowerCase() == "color") {
            return true;
          }
        }
        return false;
      })(),

      dataProp: "_data_jscolor",

      // usage:
      //   setData(obj, prop, value)
      //   setData(obj, {prop:value, ...})
      //
      setData: function () {
        var obj = arguments[0];

        if (arguments.length === 3) {
          // setting a single property
          var data = obj.hasOwnProperty(jsc.dataProp)
            ? obj[jsc.dataProp]
            : (obj[jsc.dataProp] = {});
          var prop = arguments[1];
          var value = arguments[2];

          data[prop] = value;
          return true;
        } else if (arguments.length === 2 && typeof arguments[1] === "object") {
          // setting multiple properties
          var data = obj.hasOwnProperty(jsc.dataProp)
            ? obj[jsc.dataProp]
            : (obj[jsc.dataProp] = {});
          var map = arguments[1];

          for (var prop in map) {
            if (map.hasOwnProperty(prop)) {
              data[prop] = map[prop];
            }
          }
          return true;
        }

        throw new Error("Invalid arguments");
      },

      // usage:
      //   removeData(obj, prop, [prop...])
      //
      removeData: function () {
        var obj = arguments[0];
        if (!obj.hasOwnProperty(jsc.dataProp)) {
          return true; // data object does not exist
        }
        for (var i = 1; i < arguments.length; i += 1) {
          var prop = arguments[i];
          delete obj[jsc.dataProp][prop];
        }
        return true;
      },

      getData: function (obj, prop, setDefault) {
        if (!obj.hasOwnProperty(jsc.dataProp)) {
          // data object does not exist
          if (setDefault !== undefined) {
            obj[jsc.dataProp] = {}; // create data object
          } else {
            return undefined; // no value to return
          }
        }
        var data = obj[jsc.dataProp];

        if (!data.hasOwnProperty(prop) && setDefault !== undefined) {
          data[prop] = setDefault;
        }
        return data[prop];
      },

      getDataAttr: function (el, name) {
        var attrName = "data-" + name;
        var attrValue = el.getAttribute(attrName);
        return attrValue;
      },

      setDataAttr: function (el, name, value) {
        var attrName = "data-" + name;
        el.setAttribute(attrName, value);
      },

      _attachedGroupEvents: {},

      attachGroupEvent: function (groupName, el, evnt, func) {
        if (!jsc._attachedGroupEvents.hasOwnProperty(groupName)) {
          jsc._attachedGroupEvents[groupName] = [];
        }
        jsc._attachedGroupEvents[groupName].push([el, evnt, func]);
        el.addEventListener(evnt, func, false);
      },

      detachGroupEvents: function (groupName) {
        if (jsc._attachedGroupEvents.hasOwnProperty(groupName)) {
          for (
            var i = 0;
            i < jsc._attachedGroupEvents[groupName].length;
            i += 1
          ) {
            var evt = jsc._attachedGroupEvents[groupName][i];
            evt[0].removeEventListener(evt[1], evt[2], false);
          }
          delete jsc._attachedGroupEvents[groupName];
        }
      },

      preventDefault: function (e) {
        if (e.preventDefault) {
          e.preventDefault();
        }
        e.returnValue = false;
      },

      triggerEvent: function (el, eventName, bubbles, cancelable) {
        if (!el) {
          return;
        }

        var ev = null;

        if (typeof Event === "function") {
          ev = new Event(eventName, {
            bubbles: bubbles,
            cancelable: cancelable,
          });
        } else {
          // IE
          ev = window.document.createEvent("Event");
          ev.initEvent(eventName, bubbles, cancelable);
        }

        if (!ev) {
          return false;
        }

        // so that we know that the event was triggered internally
        jsc.setData(ev, "internal", true);

        el.dispatchEvent(ev);
        return true;
      },

      triggerInputEvent: function (el, eventName, bubbles, cancelable) {
        if (!el) {
          return;
        }
        if (jsc.isTextInput(el)) {
          jsc.triggerEvent(el, eventName, bubbles, cancelable);
        }
      },

      eventKey: function (ev) {
        var keys = {
          9: "Tab",
          13: "Enter",
          27: "Escape",
        };
        if (typeof ev.code === "string") {
          return ev.code;
        } else if (
          ev.keyCode !== undefined &&
          keys.hasOwnProperty(ev.keyCode)
        ) {
          return keys[ev.keyCode];
        }
        return null;
      },

      strList: function (str) {
        if (!str) {
          return [];
        }
        return str.replace(/^\s+|\s+$/g, "").split(/\s+/);
      },

      // The className parameter (str) can only contain a single class name
      hasClass: function (elm, className) {
        if (!className) {
          return false;
        }
        if (elm.classList !== undefined) {
          return elm.classList.contains(className);
        }
        // polyfill
        return (
          -1 !=
          (" " + elm.className.replace(/\s+/g, " ") + " ").indexOf(
            " " + className + " "
          )
        );
      },

      // The className parameter (str) can contain multiple class names separated by whitespace
      addClass: function (elm, className) {
        var classNames = jsc.strList(className);

        if (elm.classList !== undefined) {
          for (var i = 0; i < classNames.length; i += 1) {
            elm.classList.add(classNames[i]);
          }
          return;
        }
        // polyfill
        for (var i = 0; i < classNames.length; i += 1) {
          if (!jsc.hasClass(elm, classNames[i])) {
            elm.className += (elm.className ? " " : "") + classNames[i];
          }
        }
      },

      // The className parameter (str) can contain multiple class names separated by whitespace
      removeClass: function (elm, className) {
        var classNames = jsc.strList(className);

        if (elm.classList !== undefined) {
          for (var i = 0; i < classNames.length; i += 1) {
            elm.classList.remove(classNames[i]);
          }
          return;
        }
        // polyfill
        for (var i = 0; i < classNames.length; i += 1) {
          var repl = new RegExp(
            "^\\s*" +
              classNames[i] +
              "\\s*|" +
              "\\s*" +
              classNames[i] +
              "\\s*$|" +
              "\\s+" +
              classNames[i] +
              "(\\s+)",
            "g"
          );
          elm.className = elm.className.replace(repl, "$1");
        }
      },

      getCompStyle: function (elm) {
        var compStyle = window.getComputedStyle
          ? window.getComputedStyle(elm)
          : elm.currentStyle;

        // Note: In Firefox, getComputedStyle returns null in a hidden iframe,
        // that's why we need to check if the returned value is non-empty
        if (!compStyle) {
          return {};
        }
        return compStyle;
      },

      // Note:
      //   Setting a property to NULL reverts it to the state before it was first set
      //   with the 'reversible' flag enabled
      //
      setStyle: function (elm, styles, important, reversible) {
        // using '' for standard priority (IE10 apparently doesn't like value undefined)
        var priority = important ? "important" : "";
        var origStyle = null;

        for (var prop in styles) {
          if (styles.hasOwnProperty(prop)) {
            var setVal = null;

            if (styles[prop] === null) {
              // reverting a property value

              if (!origStyle) {
                // get the original style object, but dont't try to create it if it doesn't exist
                origStyle = jsc.getData(elm, "origStyle");
              }
              if (origStyle && origStyle.hasOwnProperty(prop)) {
                // we have property's original value -> use it
                setVal = origStyle[prop];
              }
            } else {
              // setting a property value

              if (reversible) {
                if (!origStyle) {
                  // get the original style object and if it doesn't exist, create it
                  origStyle = jsc.getData(elm, "origStyle", {});
                }
                if (!origStyle.hasOwnProperty(prop)) {
                  // original property value not yet stored -> store it
                  origStyle[prop] = elm.style[prop];
                }
              }
              setVal = styles[prop];
            }

            if (setVal !== null) {
              elm.style.setProperty(prop, setVal, priority);
            }
          }
        }
      },

      appendCss: function (css) {
        var head = document.querySelector("head");
        var style = document.createElement("style");
        style.innerText = css;
        head.appendChild(style);
      },

      appendDefaultCss: function (css) {
        jsc.appendCss(
          [
            ".jscolor-wrap, .jscolor-wrap div, .jscolor-wrap canvas { " +
              "position:static; display:block; visibility:visible; overflow:visible; margin:0; padding:0; " +
              "border:none; border-radius:0; outline:none; z-index:auto; float:none; " +
              "width:auto; height:auto; left:auto; right:auto; top:auto; bottom:auto; min-width:0; min-height:0; max-width:none; max-height:none; " +
              "background:none; clip:auto; opacity:1; transform:none; box-shadow:none; box-sizing:content-box; " +
              "}",
            ".jscolor-wrap { clear:both; }",
            ".jscolor-wrap .jscolor-picker { position:relative; }",
            ".jscolor-wrap .jscolor-shadow { position:absolute; left:0; top:0; width:100%; height:100%; }",
            ".jscolor-wrap .jscolor-border { position:relative; }",
            ".jscolor-wrap .jscolor-palette { position:absolute; }",
            ".jscolor-wrap .jscolor-palette-sw { position:absolute; display:block; cursor:pointer; }",
            ".jscolor-wrap .jscolor-btn { position:absolute; overflow:hidden; white-space:nowrap; font:13px sans-serif; text-align:center; cursor:pointer; }",
          ].join("\n")
        );
      },

      hexColor: function (r, g, b) {
        return (
          "#" +
          (
            ("0" + Math.round(r).toString(16)).slice(-2) +
            ("0" + Math.round(g).toString(16)).slice(-2) +
            ("0" + Math.round(b).toString(16)).slice(-2)
          ).toUpperCase()
        );
      },

      hexaColor: function (r, g, b, a) {
        return (
          "#" +
          (
            ("0" + Math.round(r).toString(16)).slice(-2) +
            ("0" + Math.round(g).toString(16)).slice(-2) +
            ("0" + Math.round(b).toString(16)).slice(-2) +
            ("0" + Math.round(a * 255).toString(16)).slice(-2)
          ).toUpperCase()
        );
      },

      rgbColor: function (r, g, b) {
        return (
          "rgb(" +
          Math.round(r) +
          "," +
          Math.round(g) +
          "," +
          Math.round(b) +
          ")"
        );
      },

      rgbaColor: function (r, g, b, a) {
        return (
          "rgba(" +
          Math.round(r) +
          "," +
          Math.round(g) +
          "," +
          Math.round(b) +
          "," +
          Math.round((a === undefined || a === null ? 1 : a) * 100) / 100 +
          ")"
        );
      },

      linearGradient: (function () {
        function getFuncName() {
          var stdName = "linear-gradient";
          var prefixes = ["", "-webkit-", "-moz-", "-o-", "-ms-"];
          var helper = window.document.createElement("div");

          for (var i = 0; i < prefixes.length; i += 1) {
            var tryFunc = prefixes[i] + stdName;
            var tryVal = tryFunc + "(to right, rgba(0,0,0,0), rgba(0,0,0,0))";

            helper.style.background = tryVal;
            if (helper.style.background) {
              // CSS background successfully set -> function name is supported
              return tryFunc;
            }
          }
          return stdName; // fallback to standard 'linear-gradient' without vendor prefix
        }

        var funcName = getFuncName();

        return function () {
          return (
            funcName + "(" + Array.prototype.join.call(arguments, ", ") + ")"
          );
        };
      })(),

      setBorderRadius: function (elm, value) {
        jsc.setStyle(elm, { "border-radius": value || "0" });
      },

      setBoxShadow: function (elm, value) {
        jsc.setStyle(elm, { "box-shadow": value || "none" });
      },

      getElementPos: function (e, relativeToViewport) {
        var x = 0,
          y = 0;
        var rect = e.getBoundingClientRect();
        x = rect.left;
        y = rect.top;
        if (!relativeToViewport) {
          var viewPos = jsc.getViewPos();
          x += viewPos[0];
          y += viewPos[1];
        }
        return [x, y];
      },

      getElementSize: function (e) {
        return [e.offsetWidth, e.offsetHeight];
      },

      // get pointer's X/Y coordinates relative to viewport
      getAbsPointerPos: function (e) {
        var x = 0,
          y = 0;
        if (
          typeof e.changedTouches !== "undefined" &&
          e.changedTouches.length
        ) {
          // touch devices
          x = e.changedTouches[0].clientX;
          y = e.changedTouches[0].clientY;
        } else if (typeof e.clientX === "number") {
          x = e.clientX;
          y = e.clientY;
        }
        return { x: x, y: y };
      },

      // get pointer's X/Y coordinates relative to target element
      getRelPointerPos: function (e) {
        var target = e.target || e.srcElement;
        var targetRect = target.getBoundingClientRect();

        var x = 0,
          y = 0;

        var clientX = 0,
          clientY = 0;
        if (
          typeof e.changedTouches !== "undefined" &&
          e.changedTouches.length
        ) {
          // touch devices
          clientX = e.changedTouches[0].clientX;
          clientY = e.changedTouches[0].clientY;
        } else if (typeof e.clientX === "number") {
          clientX = e.clientX;
          clientY = e.clientY;
        }

        x = clientX - targetRect.left;
        y = clientY - targetRect.top;
        return { x: x, y: y };
      },

      getViewPos: function () {
        var doc = window.document.documentElement;
        return [
          (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0),
          (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0),
        ];
      },

      getViewSize: function () {
        var doc = window.document.documentElement;
        return [
          window.innerWidth || doc.clientWidth,
          window.innerHeight || doc.clientHeight,
        ];
      },

      // r: 0-255
      // g: 0-255
      // b: 0-255
      //
      // returns: [ 0-360, 0-100, 0-100 ]
      //
      RGB_HSV: function (r, g, b) {
        r /= 255;
        g /= 255;
        b /= 255;
        var n = Math.min(Math.min(r, g), b);
        var v = Math.max(Math.max(r, g), b);
        var m = v - n;
        if (m === 0) {
          return [null, 0, 100 * v];
        }
        var h =
          r === n
            ? 3 + (b - g) / m
            : g === n
            ? 5 + (r - b) / m
            : 1 + (g - r) / m;
        return [60 * (h === 6 ? 0 : h), 100 * (m / v), 100 * v];
      },

      // h: 0-360
      // s: 0-100
      // v: 0-100
      //
      // returns: [ 0-255, 0-255, 0-255 ]
      //
      HSV_RGB: function (h, s, v) {
        var u = 255 * (v / 100);

        if (h === null) {
          return [u, u, u];
        }

        h /= 60;
        s /= 100;

        var i = Math.floor(h);
        var f = i % 2 ? h - i : 1 - (h - i);
        var m = u * (1 - s);
        var n = u * (1 - s * f);
        switch (i) {
          case 6:
          case 0:
            return [u, n, m];
          case 1:
            return [n, u, m];
          case 2:
            return [m, u, n];
          case 3:
            return [m, n, u];
          case 4:
            return [n, m, u];
          case 5:
            return [u, m, n];
        }
      },

      parseColorString: function (str) {
        var ret = {
          rgba: null,
          format: null, // 'hex' | 'hexa' | 'rgb' | 'rgba'
        };

        var m;

        if ((m = str.match(/^\W*([0-9A-F]{3,8})\W*$/i))) {
          // HEX notation

          if (m[1].length === 8) {
            // 8-char notation (= with alpha)
            ret.format = "hexa";
            ret.rgba = [
              parseInt(m[1].slice(0, 2), 16),
              parseInt(m[1].slice(2, 4), 16),
              parseInt(m[1].slice(4, 6), 16),
              parseInt(m[1].slice(6, 8), 16) / 255,
            ];
          } else if (m[1].length === 6) {
            // 6-char notation
            ret.format = "hex";
            ret.rgba = [
              parseInt(m[1].slice(0, 2), 16),
              parseInt(m[1].slice(2, 4), 16),
              parseInt(m[1].slice(4, 6), 16),
              null,
            ];
          } else if (m[1].length === 3) {
            // 3-char notation
            ret.format = "hex";
            ret.rgba = [
              parseInt(m[1].charAt(0) + m[1].charAt(0), 16),
              parseInt(m[1].charAt(1) + m[1].charAt(1), 16),
              parseInt(m[1].charAt(2) + m[1].charAt(2), 16),
              null,
            ];
          } else {
            return false;
          }

          return ret;
        }

        if ((m = str.match(/^\W*rgba?\(([^)]*)\)\W*$/i))) {
          // rgb(...) or rgba(...) notation

          var par = m[1].split(",");
          var re = /^\s*(\d+|\d*\.\d+|\d+\.\d*)\s*$/;
          var mR, mG, mB, mA;
          if (
            par.length >= 3 &&
            (mR = par[0].match(re)) &&
            (mG = par[1].match(re)) &&
            (mB = par[2].match(re))
          ) {
            ret.format = "rgb";
            ret.rgba = [
              parseFloat(mR[1]) || 0,
              parseFloat(mG[1]) || 0,
              parseFloat(mB[1]) || 0,
              null,
            ];

            if (par.length >= 4 && (mA = par[3].match(re))) {
              ret.format = "rgba";
              ret.rgba[3] = parseFloat(mA[1]) || 0;
            }
            return ret;
          }
        }

        return false;
      },

      parsePaletteValue: function (mixed) {
        var vals = [];

        if (typeof mixed === "string") {
          // input is a string of space separated color values
          // rgb() and rgba() may contain spaces too, so let's find all color values by regex
          mixed.replace(
            /#[0-9A-F]{3}\b|#[0-9A-F]{6}([0-9A-F]{2})?\b|rgba?\(([^)]*)\)/gi,
            function (val) {
              vals.push(val);
            }
          );
        } else if (Array.isArray(mixed)) {
          // input is an array of color values
          vals = mixed;
        }

        // convert all values into uniform color format

        var colors = [];

        for (var i = 0; i < vals.length; i++) {
          var color = jsc.parseColorString(vals[i]);
          if (color) {
            colors.push(color);
          }
        }

        return colors;
      },

      containsTranparentColor: function (colors) {
        for (var i = 0; i < colors.length; i++) {
          var a = colors[i].rgba[3];
          if (a !== null && a < 1.0) {
            return true;
          }
        }
        return false;
      },

      isAlphaFormat: function (format) {
        switch (format.toLowerCase()) {
          case "hexa":
          case "rgba":
            return true;
        }
        return false;
      },

      // Canvas scaling for retina displays
      //
      // adapted from https://www.html5rocks.com/en/tutorials/canvas/hidpi/
      //
      scaleCanvasForHighDPR: function (canvas) {
        var dpr = window.devicePixelRatio || 1;
        canvas.width *= dpr;
        canvas.height *= dpr;
        var ctx = canvas.getContext("2d");
        ctx.scale(dpr, dpr);
      },

      genColorPreviewCanvas: function (
        color,
        separatorPos,
        specWidth,
        scaleForHighDPR
      ) {
        var sepW = Math.round(jsc.pub.previewSeparator.length);
        var sqSize = jsc.pub.chessboardSize;
        var sqColor1 = jsc.pub.chessboardColor1;
        var sqColor2 = jsc.pub.chessboardColor2;

        var cWidth = specWidth ? specWidth : sqSize * 2;
        var cHeight = sqSize * 2;

        var canvas = jsc.createEl("canvas");
        var ctx = canvas.getContext("2d");

        canvas.width = cWidth;
        canvas.height = cHeight;
        if (scaleForHighDPR) {
          jsc.scaleCanvasForHighDPR(canvas);
        }

        // transparency chessboard - background
        ctx.fillStyle = sqColor1;
        ctx.fillRect(0, 0, cWidth, cHeight);

        // transparency chessboard - squares
        ctx.fillStyle = sqColor2;
        for (var x = 0; x < cWidth; x += sqSize * 2) {
          ctx.fillRect(x, 0, sqSize, sqSize);
          ctx.fillRect(x + sqSize, sqSize, sqSize, sqSize);
        }

        if (color) {
          // actual color in foreground
          ctx.fillStyle = color;
          ctx.fillRect(0, 0, cWidth, cHeight);
        }

        var start = null;
        switch (separatorPos) {
          case "left":
            start = 0;
            ctx.clearRect(0, 0, sepW / 2, cHeight);
            break;
          case "right":
            start = cWidth - sepW;
            ctx.clearRect(cWidth - sepW / 2, 0, sepW / 2, cHeight);
            break;
        }
        if (start !== null) {
          ctx.lineWidth = 1;
          for (var i = 0; i < jsc.pub.previewSeparator.length; i += 1) {
            ctx.beginPath();
            ctx.strokeStyle = jsc.pub.previewSeparator[i];
            ctx.moveTo(0.5 + start + i, 0);
            ctx.lineTo(0.5 + start + i, cHeight);
            ctx.stroke();
          }
        }

        return {
          canvas: canvas,
          width: cWidth,
          height: cHeight,
        };
      },

      // if position or width is not set => fill the entire element (0%-100%)
      genColorPreviewGradient: function (color, position, width) {
        var params = [];

        if (position && width) {
          params = [
            "to " + { left: "right", right: "left" }[position],
            color + " 0%",
            color + " " + width + "px",
            "rgba(0,0,0,0) " + (width + 1) + "px",
            "rgba(0,0,0,0) 100%",
          ];
        } else {
          params = ["to right", color + " 0%", color + " 100%"];
        }

        return jsc.linearGradient.apply(this, params);
      },

      redrawPosition: function () {
        if (!jsc.picker || !jsc.picker.owner) {
          return; // picker is not shown
        }

        var thisObj = jsc.picker.owner;

        if (thisObj.container !== window.document.body) {
          jsc._drawPosition(thisObj, 0, 0, "relative", false);
        } else {
          var tp, vp;

          if (thisObj.fixed) {
            // Fixed elements are positioned relative to viewport,
            // therefore we can ignore the scroll offset
            tp = jsc.getElementPos(thisObj.targetElement, true); // target pos
            vp = [0, 0]; // view pos
          } else {
            tp = jsc.getElementPos(thisObj.targetElement); // target pos
            vp = jsc.getViewPos(); // view pos
          }

          var ts = jsc.getElementSize(thisObj.targetElement); // target size
          var vs = jsc.getViewSize(); // view size
          var pd = jsc.getPickerDims(thisObj);
          var ps = [pd.borderW, pd.borderH]; // picker outer size
          var a, b, c;
          switch (thisObj.position.toLowerCase()) {
            case "left":
              a = 1;
              b = 0;
              c = -1;
              break;
            case "right":
              a = 1;
              b = 0;
              c = 1;
              break;
            case "top":
              a = 0;
              b = 1;
              c = -1;
              break;
            default:
              a = 0;
              b = 1;
              c = 1;
              break;
          }
          var l = (ts[b] + ps[b]) / 2;

          // compute picker position
          if (!thisObj.smartPosition) {
            var pp = [tp[a], tp[b] + ts[b] - l + l * c];
          } else {
            var pp = [
              -vp[a] + tp[a] + ps[a] > vs[a]
                ? -vp[a] + tp[a] + ts[a] / 2 > vs[a] / 2 &&
                  tp[a] + ts[a] - ps[a] >= 0
                  ? tp[a] + ts[a] - ps[a]
                  : tp[a]
                : tp[a],
              -vp[b] + tp[b] + ts[b] + ps[b] - l + l * c > vs[b]
                ? -vp[b] + tp[b] + ts[b] / 2 > vs[b] / 2 &&
                  tp[b] + ts[b] - l - l * c >= 0
                  ? tp[b] + ts[b] - l - l * c
                  : tp[b] + ts[b] - l + l * c
                : tp[b] + ts[b] - l + l * c >= 0
                ? tp[b] + ts[b] - l + l * c
                : tp[b] + ts[b] - l - l * c,
            ];
          }

          var x = pp[a];
          var y = pp[b];
          var positionValue = thisObj.fixed ? "fixed" : "absolute";
          var contractShadow =
            (pp[0] + ps[0] > tp[0] || pp[0] < tp[0] + ts[0]) &&
            pp[1] + ps[1] < tp[1] + ts[1];

          jsc._drawPosition(thisObj, x, y, positionValue, contractShadow);
        }
      },

      _drawPosition: function (thisObj, x, y, positionValue, contractShadow) {
        var vShadow = contractShadow ? 0 : thisObj.shadowBlur; // px

        jsc.picker.wrap.style.position = positionValue;

        if (
          // To avoid unnecessary repositioning during scroll
          Math.round(parseFloat(jsc.picker.wrap.style.left)) !==
            Math.round(x) ||
          Math.round(parseFloat(jsc.picker.wrap.style.top)) !== Math.round(y)
        ) {
          jsc.picker.wrap.style.left = x + "px";
          jsc.picker.wrap.style.top = y + "px";
        }

        jsc.setBoxShadow(
          jsc.picker.boxS,
          thisObj.shadow
            ? new jsc.BoxShadow(
                0,
                vShadow,
                thisObj.shadowBlur,
                0,
                thisObj.shadowColor
              )
            : null
        );
      },

      getPickerDims: function (thisObj) {
        var w = 2 * thisObj.controlBorderWidth + thisObj.width;
        var h = 2 * thisObj.controlBorderWidth + thisObj.height;

        var sliderSpace =
          2 * thisObj.controlBorderWidth +
          2 * jsc.getControlPadding(thisObj) +
          thisObj.sliderSize;

        if (jsc.getSliderChannel(thisObj)) {
          w += sliderSpace;
        }
        if (thisObj.hasAlphaChannel()) {
          w += sliderSpace;
        }

        var pal = jsc.getPaletteDims(thisObj, w);

        if (pal.height) {
          h += pal.height + thisObj.padding;
        }
        if (thisObj.closeButton) {
          h +=
            2 * thisObj.controlBorderWidth +
            thisObj.padding +
            thisObj.buttonHeight;
        }

        var pW = w + 2 * thisObj.padding;
        var pH = h + 2 * thisObj.padding;

        return {
          contentW: w,
          contentH: h,
          paddedW: pW,
          paddedH: pH,
          borderW: pW + 2 * thisObj.borderWidth,
          borderH: pH + 2 * thisObj.borderWidth,
          palette: pal,
        };
      },

      getPaletteDims: function (thisObj, width) {
        var cols = 0,
          rows = 0,
          cellW = 0,
          cellH = 0,
          height = 0;
        var sampleCount = thisObj._palette ? thisObj._palette.length : 0;

        if (sampleCount) {
          cols = thisObj.paletteCols;
          rows = cols > 0 ? Math.ceil(sampleCount / cols) : 0;

          // color sample's dimensions (includes border)
          cellW = Math.max(
            1,
            Math.floor((width - (cols - 1) * thisObj.paletteSpacing) / cols)
          );
          cellH = thisObj.paletteHeight
            ? Math.min(thisObj.paletteHeight, cellW)
            : cellW;
        }

        if (rows) {
          height = rows * cellH + (rows - 1) * thisObj.paletteSpacing;
        }

        return {
          cols: cols,
          rows: rows,
          cellW: cellW,
          cellH: cellH,
          width: width,
          height: height,
        };
      },

      getControlPadding: function (thisObj) {
        return Math.max(
          thisObj.padding / 2,
          2 * thisObj.pointerBorderWidth +
            thisObj.pointerThickness -
            thisObj.controlBorderWidth
        );
      },

      getPadYChannel: function (thisObj) {
        switch (thisObj.mode.charAt(1).toLowerCase()) {
          case "v":
            return "v";
            break;
        }
        return "s";
      },

      getSliderChannel: function (thisObj) {
        if (thisObj.mode.length > 2) {
          switch (thisObj.mode.charAt(2).toLowerCase()) {
            case "s":
              return "s";
              break;
            case "v":
              return "v";
              break;
          }
        }
        return null;
      },

      // calls function specified in picker's property
      triggerCallback: function (thisObj, prop) {
        if (!thisObj[prop]) {
          return; // callback func not specified
        }
        var callback = null;

        if (typeof thisObj[prop] === "string") {
          // string with code
          try {
            callback = new Function(thisObj[prop]);
          } catch (e) {
            console.error(e);
          }
        } else {
          // function
          callback = thisObj[prop];
        }

        if (callback) {
          callback.call(thisObj);
        }
      },

      // Triggers a color change related event(s) on all picker instances.
      // It is possible to specify multiple events separated with a space.
      triggerGlobal: function (eventNames) {
        var inst = jsc.getInstances();
        for (var i = 0; i < inst.length; i += 1) {
          inst[i].trigger(eventNames);
        }
      },

      _pointerMoveEvent: {
        mouse: "mousemove",
        touch: "touchmove",
      },
      _pointerEndEvent: {
        mouse: "mouseup",
        touch: "touchend",
      },

      _pointerOrigin: null,

      onDocumentKeyUp: function (e) {
        if (["Tab", "Escape"].indexOf(jsc.eventKey(e)) !== -1) {
          if (jsc.picker && jsc.picker.owner) {
            jsc.picker.owner.tryHide();
          }
        }
      },

      onWindowResize: function (e) {
        jsc.redrawPosition();
      },

      onWindowScroll: function (e) {
        jsc.redrawPosition();
      },

      onParentScroll: function (e) {
        // hide the picker when one of the parent elements is scrolled
        if (jsc.picker && jsc.picker.owner) {
          jsc.picker.owner.tryHide();
        }
      },

      onDocumentMouseDown: function (e) {
        var target = e.target || e.srcElement;

        if (target.jscolor && target.jscolor instanceof jsc.pub) {
          // clicked targetElement -> show picker
          if (target.jscolor.showOnClick && !target.disabled) {
            target.jscolor.show();
          }
        } else if (jsc.getData(target, "gui")) {
          // clicked jscolor's GUI element
          var control = jsc.getData(target, "control");
          if (control) {
            // jscolor's control
            jsc.onControlPointerStart(
              e,
              target,
              jsc.getData(target, "control"),
              "mouse"
            );
          }
        } else {
          // mouse is outside the picker's controls -> hide the color picker!
          if (jsc.picker && jsc.picker.owner) {
            jsc.picker.owner.tryHide();
          }
        }
      },

      onPickerTouchStart: function (e) {
        var target = e.target || e.srcElement;

        if (jsc.getData(target, "control")) {
          jsc.onControlPointerStart(
            e,
            target,
            jsc.getData(target, "control"),
            "touch"
          );
        }
      },

      onControlPointerStart: function (e, target, controlName, pointerType) {
        var thisObj = jsc.getData(target, "instance");

        jsc.preventDefault(e);

        var registerDragEvents = function (doc, offset) {
          jsc.attachGroupEvent(
            "drag",
            doc,
            jsc._pointerMoveEvent[pointerType],
            jsc.onDocumentPointerMove(
              e,
              target,
              controlName,
              pointerType,
              offset
            )
          );
          jsc.attachGroupEvent(
            "drag",
            doc,
            jsc._pointerEndEvent[pointerType],
            jsc.onDocumentPointerEnd(e, target, controlName, pointerType)
          );
        };

        registerDragEvents(window.document, [0, 0]);

        if (window.parent && window.frameElement) {
          var rect = window.frameElement.getBoundingClientRect();
          var ofs = [-rect.left, -rect.top];
          registerDragEvents(window.parent.window.document, ofs);
        }

        var abs = jsc.getAbsPointerPos(e);
        var rel = jsc.getRelPointerPos(e);
        jsc._pointerOrigin = {
          x: abs.x - rel.x,
          y: abs.y - rel.y,
        };

        switch (controlName) {
          case "pad":
            // if the value slider is at the bottom, move it up
            if (
              jsc.getSliderChannel(thisObj) === "v" &&
              thisObj.channels.v === 0
            ) {
              thisObj.fromHSVA(null, null, 100, null);
            }
            jsc.setPad(thisObj, e, 0, 0);
            break;

          case "sld":
            jsc.setSld(thisObj, e, 0);
            break;

          case "asld":
            jsc.setASld(thisObj, e, 0);
            break;
        }
        thisObj.trigger("input");
      },

      onDocumentPointerMove: function (
        e,
        target,
        controlName,
        pointerType,
        offset
      ) {
        return function (e) {
          var thisObj = jsc.getData(target, "instance");
          switch (controlName) {
            case "pad":
              jsc.setPad(thisObj, e, offset[0], offset[1]);
              break;

            case "sld":
              jsc.setSld(thisObj, e, offset[1]);
              break;

            case "asld":
              jsc.setASld(thisObj, e, offset[1]);
              break;
          }
          thisObj.trigger("input");
        };
      },

      onDocumentPointerEnd: function (e, target, controlName, pointerType) {
        return function (e) {
          var thisObj = jsc.getData(target, "instance");
          jsc.detachGroupEvents("drag");

          // Always trigger changes AFTER detaching outstanding mouse handlers,
          // in case some color change that occured in user-defined onChange/onInput handler
          // intruded into current mouse events
          thisObj.trigger("input");
          thisObj.trigger("change");
        };
      },

      onPaletteSampleClick: function (e) {
        var target = e.currentTarget;
        var thisObj = jsc.getData(target, "instance");
        var color = jsc.getData(target, "color");

        // when format is flexible, use the original format of this color sample
        if (thisObj.format.toLowerCase() === "any") {
          thisObj._setFormat(color.format); // adapt format
          if (!jsc.isAlphaFormat(thisObj.getFormat())) {
            color.rgba[3] = 1.0; // when switching to a format that doesn't support alpha, set full opacity
          }
        }

        // if this color doesn't specify alpha, use alpha of 1.0 (if applicable)
        if (color.rgba[3] === null) {
          if (
            thisObj.paletteSetsAlpha === true ||
            (thisObj.paletteSetsAlpha === "auto" &&
              thisObj._paletteHasTransparency)
          ) {
            color.rgba[3] = 1.0;
          }
        }

        thisObj.fromRGBA.apply(thisObj, color.rgba);

        thisObj.trigger("input");
        thisObj.trigger("change");

        if (thisObj.hideOnPaletteClick) {
          thisObj.hide();
        }
      },

      setPad: function (thisObj, e, ofsX, ofsY) {
        var pointerAbs = jsc.getAbsPointerPos(e);
        var x =
          ofsX +
          pointerAbs.x -
          jsc._pointerOrigin.x -
          thisObj.padding -
          thisObj.controlBorderWidth;
        var y =
          ofsY +
          pointerAbs.y -
          jsc._pointerOrigin.y -
          thisObj.padding -
          thisObj.controlBorderWidth;

        var xVal = x * (360 / (thisObj.width - 1));
        var yVal = 100 - y * (100 / (thisObj.height - 1));

        switch (jsc.getPadYChannel(thisObj)) {
          case "s":
            thisObj.fromHSVA(xVal, yVal, null, null);
            break;
          case "v":
            thisObj.fromHSVA(xVal, null, yVal, null);
            break;
        }
      },

      setSld: function (thisObj, e, ofsY) {
        var pointerAbs = jsc.getAbsPointerPos(e);
        var y =
          ofsY +
          pointerAbs.y -
          jsc._pointerOrigin.y -
          thisObj.padding -
          thisObj.controlBorderWidth;
        var yVal = 100 - y * (100 / (thisObj.height - 1));

        switch (jsc.getSliderChannel(thisObj)) {
          case "s":
            thisObj.fromHSVA(null, yVal, null, null);
            break;
          case "v":
            thisObj.fromHSVA(null, null, yVal, null);
            break;
        }
      },

      setASld: function (thisObj, e, ofsY) {
        var pointerAbs = jsc.getAbsPointerPos(e);
        var y =
          ofsY +
          pointerAbs.y -
          jsc._pointerOrigin.y -
          thisObj.padding -
          thisObj.controlBorderWidth;
        var yVal = 1.0 - y * (1.0 / (thisObj.height - 1));

        if (yVal < 1.0) {
          // if format is flexible and the current format doesn't support alpha, switch to a suitable one
          var fmt = thisObj.getFormat();
          if (
            thisObj.format.toLowerCase() === "any" &&
            !jsc.isAlphaFormat(fmt)
          ) {
            thisObj._setFormat(fmt === "hex" ? "hexa" : "rgba");
          }
        }

        thisObj.fromHSVA(null, null, null, yVal);
      },

      createPadCanvas: function () {
        var ret = {
          elm: null,
          draw: null,
        };

        var canvas = jsc.createEl("canvas");
        var ctx = canvas.getContext("2d");

        var drawFunc = function (width, height, type) {
          canvas.width = width;
          canvas.height = height;

          ctx.clearRect(0, 0, canvas.width, canvas.height);

          var hGrad = ctx.createLinearGradient(0, 0, canvas.width, 0);
          hGrad.addColorStop(0 / 6, "#F00");
          hGrad.addColorStop(1 / 6, "#FF0");
          hGrad.addColorStop(2 / 6, "#0F0");
          hGrad.addColorStop(3 / 6, "#0FF");
          hGrad.addColorStop(4 / 6, "#00F");
          hGrad.addColorStop(5 / 6, "#F0F");
          hGrad.addColorStop(6 / 6, "#F00");

          ctx.fillStyle = hGrad;
          ctx.fillRect(0, 0, canvas.width, canvas.height);

          var vGrad = ctx.createLinearGradient(0, 0, 0, canvas.height);
          switch (type.toLowerCase()) {
            case "s":
              vGrad.addColorStop(0, "rgba(255,255,255,0)");
              vGrad.addColorStop(1, "rgba(255,255,255,1)");
              break;
            case "v":
              vGrad.addColorStop(0, "rgba(0,0,0,0)");
              vGrad.addColorStop(1, "rgba(0,0,0,1)");
              break;
          }
          ctx.fillStyle = vGrad;
          ctx.fillRect(0, 0, canvas.width, canvas.height);
        };

        ret.elm = canvas;
        ret.draw = drawFunc;

        return ret;
      },

      createSliderGradient: function () {
        var ret = {
          elm: null,
          draw: null,
        };

        var canvas = jsc.createEl("canvas");
        var ctx = canvas.getContext("2d");

        var drawFunc = function (width, height, color1, color2) {
          canvas.width = width;
          canvas.height = height;

          ctx.clearRect(0, 0, canvas.width, canvas.height);

          var grad = ctx.createLinearGradient(0, 0, 0, canvas.height);
          grad.addColorStop(0, color1);
          grad.addColorStop(1, color2);

          ctx.fillStyle = grad;
          ctx.fillRect(0, 0, canvas.width, canvas.height);
        };

        ret.elm = canvas;
        ret.draw = drawFunc;

        return ret;
      },

      createASliderGradient: function () {
        var ret = {
          elm: null,
          draw: null,
        };

        var canvas = jsc.createEl("canvas");
        var ctx = canvas.getContext("2d");

        var drawFunc = function (width, height, color) {
          canvas.width = width;
          canvas.height = height;

          ctx.clearRect(0, 0, canvas.width, canvas.height);

          var sqSize = canvas.width / 2;
          var sqColor1 = jsc.pub.chessboardColor1;
          var sqColor2 = jsc.pub.chessboardColor2;

          // dark gray background
          ctx.fillStyle = sqColor1;
          ctx.fillRect(0, 0, canvas.width, canvas.height);

          if (sqSize > 0) {
            // to avoid infinite loop
            for (var y = 0; y < canvas.height; y += sqSize * 2) {
              // light gray squares
              ctx.fillStyle = sqColor2;
              ctx.fillRect(0, y, sqSize, sqSize);
              ctx.fillRect(sqSize, y + sqSize, sqSize, sqSize);
            }
          }

          var grad = ctx.createLinearGradient(0, 0, 0, canvas.height);
          grad.addColorStop(0, color);
          grad.addColorStop(1, "rgba(0,0,0,0)");

          ctx.fillStyle = grad;
          ctx.fillRect(0, 0, canvas.width, canvas.height);
        };

        ret.elm = canvas;
        ret.draw = drawFunc;

        return ret;
      },

      BoxShadow: (function () {
        var BoxShadow = function (
          hShadow,
          vShadow,
          blur,
          spread,
          color,
          inset
        ) {
          this.hShadow = hShadow;
          this.vShadow = vShadow;
          this.blur = blur;
          this.spread = spread;
          this.color = color;
          this.inset = !!inset;
        };

        BoxShadow.prototype.toString = function () {
          var vals = [
            Math.round(this.hShadow) + "px",
            Math.round(this.vShadow) + "px",
            Math.round(this.blur) + "px",
            Math.round(this.spread) + "px",
            this.color,
          ];
          if (this.inset) {
            vals.push("inset");
          }
          return vals.join(" ");
        };

        return BoxShadow;
      })(),

      flags: {
        leaveValue: 1 << 0,
        leaveAlpha: 1 << 1,
        leavePreview: 1 << 2,
      },

      enumOpts: {
        format: ["auto", "any", "hex", "hexa", "rgb", "rgba"],
        previewPosition: ["left", "right"],
        mode: ["hsv", "hvs", "hs", "hv"],
        position: ["left", "right", "top", "bottom"],
        alphaChannel: ["auto", true, false],
        paletteSetsAlpha: ["auto", true, false],
      },

      deprecatedOpts: {
        // <old_option>: <new_option>  (<new_option> can be null)
        styleElement: "previewElement",
        onFineChange: "onInput",
        overwriteImportant: "forceStyle",
        closable: "closeButton",
        insetWidth: "controlBorderWidth",
        insetColor: "controlBorderColor",
        refine: null,
      },

      docsRef: " " + "See https://jscolor.com/docs/",

      //
      // Usage:
      // var myPicker = new JSColor(<targetElement> [, <options>])
      //
      // (constructor is accessible via both 'jscolor' and 'JSColor' name)
      //

      pub: function (targetElement, opts) {
        var THIS = this;

        if (!opts) {
          opts = {};
        }

        this.channels = {
          r: 255, // red [0-255]
          g: 255, // green [0-255]
          b: 255, // blue [0-255]
          h: 0, // hue [0-360]
          s: 0, // saturation [0-100]
          v: 100, // value (brightness) [0-100]
          a: 1.0, // alpha (opacity) [0.0 - 1.0]
        };

        // General options
        //
        this.format = "auto"; // 'auto' | 'any' | 'hex' | 'hexa' | 'rgb' | 'rgba' - Format of the input/output value
        this.value = undefined; // INITIAL color value in any supported format. To change it later, use method fromString(), fromHSVA(), fromRGBA() or channel()
        this.alpha = undefined; // INITIAL alpha value. To change it later, call method channel('A', <value>)
        this.random = false; // whether to randomize the initial color. Either true | false, or an array of ranges: [minV, maxV, minS, maxS, minH, maxH, minA, maxA]
        this.onChange = undefined; // called when color changes. Value can be either a function or a string with JS code.
        this.onInput = undefined; // called repeatedly as the color is being changed, e.g. while dragging a slider. Value can be either a function or a string with JS code.
        this.valueElement = undefined; // element that will be used to display and input the color value
        this.alphaElement = undefined; // element that will be used to display and input the alpha (opacity) value
        this.previewElement = undefined; // element that will preview the picked color using CSS background
        this.previewPosition = "left"; // 'left' | 'right' - position of the color preview in previewElement
        this.previewSize = 32; // (px) width of the color preview displayed in previewElement
        this.previewPadding = 8; // (px) space between color preview and content of the previewElement
        this.required = true; // whether the associated text input must always contain a color value. If false, the input can be left empty.
        this.hash = true; // whether to prefix the HEX color code with # symbol (only applicable for HEX format)
        this.uppercase = true; // whether to show the HEX color code in upper case (only applicable for HEX format)
        this.forceStyle = true; // whether to overwrite CSS style of the previewElement using !important flag

        // Color Picker options
        //
        this.width = 181; // width of the color spectrum (in px)
        this.height = 101; // height of the color spectrum (in px)
        this.mode = "HSV"; // 'HSV' | 'HVS' | 'HS' | 'HV' - layout of the color picker controls
        this.alphaChannel = "auto"; // 'auto' | true | false - if alpha channel is enabled, the alpha slider will be visible. If 'auto', it will be determined according to color format
        this.position = "bottom"; // 'left' | 'right' | 'top' | 'bottom' - position relative to the target element
        this.smartPosition = true; // automatically change picker position when there is not enough space for it
        this.showOnClick = true; // whether to show the picker when user clicks its target element
        this.hideOnLeave = true; // whether to automatically hide the picker when user leaves its target element (e.g. upon clicking the document)
        this.palette = []; // colors to be displayed in the palette, specified as an array or a string of space separated color values (in any supported format)
        this.paletteCols = 10; // number of columns in the palette
        this.paletteSetsAlpha = "auto"; // 'auto' | true | false - if true, palette colors that don't specify alpha will set alpha to 1.0
        this.paletteHeight = 16; // maximum height (px) of a row in the palette
        this.paletteSpacing = 4; // distance (px) between color samples in the palette
        this.hideOnPaletteClick = false; // when set to true, clicking the palette will also hide the color picker
        this.sliderSize = 16; // px
        this.crossSize = 8; // px
        this.closeButton = false; // whether to display the Close button
        this.closeText = "Close";
        this.buttonColor = "rgba(0,0,0,1)"; // CSS color
        this.buttonHeight = 18; // px
        this.padding = 12; // px
        this.backgroundColor = "rgba(255,255,255,1)"; // CSS color
        this.borderWidth = 1; // px
        this.borderColor = "rgba(187,187,187,1)"; // CSS color
        this.borderRadius = 8; // px
        this.controlBorderWidth = 1; // px
        this.controlBorderColor = "rgba(187,187,187,1)"; // CSS color
        this.shadow = true; // whether to display a shadow
        this.shadowBlur = 15; // px
        this.shadowColor = "rgba(0,0,0,0.2)"; // CSS color
        this.pointerColor = "rgba(76,76,76,1)"; // CSS color
        this.pointerBorderWidth = 1; // px
        this.pointerBorderColor = "rgba(255,255,255,1)"; // CSS color
        this.pointerThickness = 2; // px
        this.zIndex = 5000;
        this.container = undefined; // where to append the color picker (BODY element by default)

        // Experimental
        //
        this.minS = 0; // min allowed saturation (0 - 100)
        this.maxS = 100; // max allowed saturation (0 - 100)
        this.minV = 0; // min allowed value (brightness) (0 - 100)
        this.maxV = 100; // max allowed value (brightness) (0 - 100)
        this.minA = 0.0; // min allowed alpha (opacity) (0.0 - 1.0)
        this.maxA = 1.0; // max allowed alpha (opacity) (0.0 - 1.0)

        // Getter: option(name)
        // Setter: option(name, value)
        //         option({name:value, ...})
        //
        this.option = function () {
          if (!arguments.length) {
            throw new Error("No option specified");
          }

          if (arguments.length === 1 && typeof arguments[0] === "string") {
            // getting a single option
            try {
              return getOption(arguments[0]);
            } catch (e) {
              console.warn(e);
            }
            return false;
          } else if (
            arguments.length >= 2 &&
            typeof arguments[0] === "string"
          ) {
            // setting a single option
            try {
              if (!setOption(arguments[0], arguments[1])) {
                return false;
              }
            } catch (e) {
              console.warn(e);
              return false;
            }
            this.redraw(); // immediately redraws the picker, if it's displayed
            this.exposeColor(); // in case some preview-related or format-related option was changed
            return true;
          } else if (
            arguments.length === 1 &&
            typeof arguments[0] === "object"
          ) {
            // setting multiple options
            var opts = arguments[0];
            var success = true;
            for (var opt in opts) {
              if (opts.hasOwnProperty(opt)) {
                try {
                  if (!setOption(opt, opts[opt])) {
                    success = false;
                  }
                } catch (e) {
                  console.warn(e);
                  success = false;
                }
              }
            }
            this.redraw(); // immediately redraws the picker, if it's displayed
            this.exposeColor(); // in case some preview-related or format-related option was changed
            return success;
          }

          throw new Error("Invalid arguments");
        };

        // Getter: channel(name)
        // Setter: channel(name, value)
        //
        this.channel = function (name, value) {
          if (typeof name !== "string") {
            throw new Error("Invalid value for channel name: " + name);
          }

          if (value === undefined) {
            // getting channel value
            if (!this.channels.hasOwnProperty(name.toLowerCase())) {
              console.warn("Getting unknown channel: " + name);
              return false;
            }
            return this.channels[name.toLowerCase()];
          } else {
            // setting channel value
            var res = false;
            switch (name.toLowerCase()) {
              case "r":
                res = this.fromRGBA(value, null, null, null);
                break;
              case "g":
                res = this.fromRGBA(null, value, null, null);
                break;
              case "b":
                res = this.fromRGBA(null, null, value, null);
                break;
              case "h":
                res = this.fromHSVA(value, null, null, null);
                break;
              case "s":
                res = this.fromHSVA(null, value, null, null);
                break;
              case "v":
                res = this.fromHSVA(null, null, value, null);
                break;
              case "a":
                res = this.fromHSVA(null, null, null, value);
                break;
              default:
                console.warn("Setting unknown channel: " + name);
                return false;
            }
            if (res) {
              this.redraw(); // immediately redraws the picker, if it's displayed
              return true;
            }
          }

          return false;
        };

        // Triggers given input event(s) by:
        // - executing on<Event> callback specified as picker's option
        // - triggering standard DOM event listeners attached to the value element
        //
        // It is possible to specify multiple events separated with a space.
        //
        this.trigger = function (eventNames) {
          var evs = jsc.strList(eventNames);
          for (var i = 0; i < evs.length; i += 1) {
            var ev = evs[i].toLowerCase();

            // trigger a callback
            var callbackProp = null;
            switch (ev) {
              case "input":
                callbackProp = "onInput";
                break;
              case "change":
                callbackProp = "onChange";
                break;
            }
            if (callbackProp) {
              jsc.triggerCallback(this, callbackProp);
            }

            // trigger standard DOM event listeners on the value element
            jsc.triggerInputEvent(this.valueElement, ev, true, true);
          }
        };

        // h: 0-360
        // s: 0-100
        // v: 0-100
        // a: 0.0-1.0
        //
        this.fromHSVA = function (h, s, v, a, flags) {
          // null = don't change
          if (h === undefined) {
            h = null;
          }
          if (s === undefined) {
            s = null;
          }
          if (v === undefined) {
            v = null;
          }
          if (a === undefined) {
            a = null;
          }

          if (h !== null) {
            if (isNaN(h)) {
              return false;
            }
            this.channels.h = Math.max(0, Math.min(360, h));
          }
          if (s !== null) {
            if (isNaN(s)) {
              return false;
            }
            this.channels.s = Math.max(
              0,
              Math.min(100, this.maxS, s),
              this.minS
            );
          }
          if (v !== null) {
            if (isNaN(v)) {
              return false;
            }
            this.channels.v = Math.max(
              0,
              Math.min(100, this.maxV, v),
              this.minV
            );
          }
          if (a !== null) {
            if (isNaN(a)) {
              return false;
            }
            this.channels.a = this.hasAlphaChannel()
              ? Math.max(0, Math.min(1, this.maxA, a), this.minA)
              : 1.0; // if alpha channel is disabled, the color should stay 100% opaque
          }

          var rgb = jsc.HSV_RGB(
            this.channels.h,
            this.channels.s,
            this.channels.v
          );
          this.channels.r = rgb[0];
          this.channels.g = rgb[1];
          this.channels.b = rgb[2];

          this.exposeColor(flags);
          return true;
        };

        // r: 0-255
        // g: 0-255
        // b: 0-255
        // a: 0.0-1.0
        //
        this.fromRGBA = function (r, g, b, a, flags) {
          // null = don't change
          if (r === undefined) {
            r = null;
          }
          if (g === undefined) {
            g = null;
          }
          if (b === undefined) {
            b = null;
          }
          if (a === undefined) {
            a = null;
          }

          if (r !== null) {
            if (isNaN(r)) {
              return false;
            }
            r = Math.max(0, Math.min(255, r));
          }
          if (g !== null) {
            if (isNaN(g)) {
              return false;
            }
            g = Math.max(0, Math.min(255, g));
          }
          if (b !== null) {
            if (isNaN(b)) {
              return false;
            }
            b = Math.max(0, Math.min(255, b));
          }
          if (a !== null) {
            if (isNaN(a)) {
              return false;
            }
            this.channels.a = this.hasAlphaChannel()
              ? Math.max(0, Math.min(1, this.maxA, a), this.minA)
              : 1.0; // if alpha channel is disabled, the color should stay 100% opaque
          }

          var hsv = jsc.RGB_HSV(
            r === null ? this.channels.r : r,
            g === null ? this.channels.g : g,
            b === null ? this.channels.b : b
          );
          if (hsv[0] !== null) {
            this.channels.h = Math.max(0, Math.min(360, hsv[0]));
          }
          if (hsv[2] !== 0) {
            // fully black color stays black through entire saturation range, so let's not change saturation
            this.channels.s = Math.max(
              0,
              this.minS,
              Math.min(100, this.maxS, hsv[1])
            );
          }
          this.channels.v = Math.max(
            0,
            this.minV,
            Math.min(100, this.maxV, hsv[2])
          );

          // update RGB according to final HSV, as some values might be trimmed
          var rgb = jsc.HSV_RGB(
            this.channels.h,
            this.channels.s,
            this.channels.v
          );
          this.channels.r = rgb[0];
          this.channels.g = rgb[1];
          this.channels.b = rgb[2];

          this.exposeColor(flags);
          return true;
        };

        // DEPRECATED. Use .fromHSVA() instead
        //
        this.fromHSV = function (h, s, v, flags) {
          console.warn(
            "fromHSV() method is DEPRECATED. Using fromHSVA() instead." +
              jsc.docsRef
          );
          return this.fromHSVA(h, s, v, null, flags);
        };

        // DEPRECATED. Use .fromRGBA() instead
        //
        this.fromRGB = function (r, g, b, flags) {
          console.warn(
            "fromRGB() method is DEPRECATED. Using fromRGBA() instead." +
              jsc.docsRef
          );
          return this.fromRGBA(r, g, b, null, flags);
        };

        this.fromString = function (str, flags) {
          if (!this.required && str.trim() === "") {
            // setting empty string to an optional color input
            this.setPreviewElementBg(null);
            this.setValueElementValue("");
            return true;
          }

          var color = jsc.parseColorString(str);
          if (!color) {
            return false; // could not parse
          }
          if (this.format.toLowerCase() === "any") {
            this._setFormat(color.format); // adapt format
            if (!jsc.isAlphaFormat(this.getFormat())) {
              color.rgba[3] = 1.0; // when switching to a format that doesn't support alpha, set full opacity
            }
          }
          this.fromRGBA(
            color.rgba[0],
            color.rgba[1],
            color.rgba[2],
            color.rgba[3],
            flags
          );
          return true;
        };

        this.randomize = function (
          minV,
          maxV,
          minS,
          maxS,
          minH,
          maxH,
          minA,
          maxA
        ) {
          if (minV === undefined) {
            minV = 0;
          }
          if (maxV === undefined) {
            maxV = 100;
          }
          if (minS === undefined) {
            minS = 0;
          }
          if (maxS === undefined) {
            maxS = 100;
          }
          if (minH === undefined) {
            minH = 0;
          }
          if (maxH === undefined) {
            maxH = 359;
          }
          if (minA === undefined) {
            minA = 1;
          }
          if (maxA === undefined) {
            maxA = 1;
          }

          this.fromHSVA(
            minH + Math.floor(Math.random() * (maxH - minH + 1)),
            minS + Math.floor(Math.random() * (maxS - minS + 1)),
            minV + Math.floor(Math.random() * (maxV - minV + 1)),
            (100 * minA +
              Math.floor(Math.random() * (100 * (maxA - minA) + 1))) /
              100
          );
        };

        this.toString = function (format) {
          if (format === undefined) {
            format = this.getFormat(); // format not specified -> use the current format
          }
          switch (format.toLowerCase()) {
            case "hex":
              return this.toHEXString();
              break;
            case "hexa":
              return this.toHEXAString();
              break;
            case "rgb":
              return this.toRGBString();
              break;
            case "rgba":
              return this.toRGBAString();
              break;
          }
          return false;
        };

        this.toHEXString = function () {
          return jsc.hexColor(
            this.channels.r,
            this.channels.g,
            this.channels.b
          );
        };

        this.toHEXAString = function () {
          return jsc.hexaColor(
            this.channels.r,
            this.channels.g,
            this.channels.b,
            this.channels.a
          );
        };

        this.toRGBString = function () {
          return jsc.rgbColor(
            this.channels.r,
            this.channels.g,
            this.channels.b
          );
        };

        this.toRGBAString = function () {
          return jsc.rgbaColor(
            this.channels.r,
            this.channels.g,
            this.channels.b,
            this.channels.a
          );
        };

        this.toGrayscale = function () {
          return (
            0.213 * this.channels.r +
            0.715 * this.channels.g +
            0.072 * this.channels.b
          );
        };

        this.toCanvas = function () {
          return jsc.genColorPreviewCanvas(this.toRGBAString()).canvas;
        };

        this.toDataURL = function () {
          return this.toCanvas().toDataURL();
        };

        this.toBackground = function () {
          return jsc.pub.background(this.toRGBAString());
        };

        this.isLight = function () {
          return this.toGrayscale() > 255 / 2;
        };

        this.hide = function () {
          if (isPickerOwner()) {
            detachPicker();
          }
        };

        this.show = function () {
          drawPicker();
        };

        this.redraw = function () {
          if (isPickerOwner()) {
            drawPicker();
          }
        };

        this.getFormat = function () {
          return this._currentFormat;
        };

        this._setFormat = function (format) {
          this._currentFormat = format.toLowerCase();
        };

        this.hasAlphaChannel = function () {
          if (this.alphaChannel === "auto") {
            return (
              this.format.toLowerCase() === "any" || // format can change on the fly (e.g. from hex to rgba), so let's consider the alpha channel enabled
              jsc.isAlphaFormat(this.getFormat()) || // the current format supports alpha channel
              this.alpha !== undefined || // initial alpha value is set, so we're working with alpha channel
              this.alphaElement !== undefined // the alpha value is redirected, so we're working with alpha channel
            );
          }

          return this.alphaChannel; // the alpha channel is explicitly set
        };

        this.processValueInput = function (str) {
          if (!this.fromString(str)) {
            // could not parse the color value - let's just expose the current color
            this.exposeColor();
          }
        };

        this.processAlphaInput = function (str) {
          if (!this.fromHSVA(null, null, null, parseFloat(str))) {
            // could not parse the alpha value - let's just expose the current color
            this.exposeColor();
          }
        };

        this.exposeColor = function (flags) {
          var colorStr = this.toString();
          var fmt = this.getFormat();

          // reflect current color in data- attribute
          jsc.setDataAttr(this.targetElement, "current-color", colorStr);

          if (!(flags & jsc.flags.leaveValue) && this.valueElement) {
            if (fmt === "hex" || fmt === "hexa") {
              if (!this.uppercase) {
                colorStr = colorStr.toLowerCase();
              }
              if (!this.hash) {
                colorStr = colorStr.replace(/^#/, "");
              }
            }
            this.setValueElementValue(colorStr);
          }

          if (!(flags & jsc.flags.leaveAlpha) && this.alphaElement) {
            var alphaVal = Math.round(this.channels.a * 100) / 100;
            this.setAlphaElementValue(alphaVal);
          }

          if (!(flags & jsc.flags.leavePreview) && this.previewElement) {
            var previewPos = null; // 'left' | 'right' (null -> fill the entire element)

            if (
              jsc.isTextInput(this.previewElement) || // text input
              (jsc.isButton(this.previewElement) &&
                !jsc.isButtonEmpty(this.previewElement)) // button with text
            ) {
              previewPos = this.previewPosition;
            }

            this.setPreviewElementBg(this.toRGBAString());
          }

          if (isPickerOwner()) {
            redrawPad();
            redrawSld();
            redrawASld();
          }
        };

        this.setPreviewElementBg = function (color) {
          if (!this.previewElement) {
            return;
          }

          var position = null; // color preview position:  null | 'left' | 'right'
          var width = null; // color preview width:  px | null = fill the entire element
          if (
            jsc.isTextInput(this.previewElement) || // text input
            (jsc.isButton(this.previewElement) &&
              !jsc.isButtonEmpty(this.previewElement)) // button with text
          ) {
            position = this.previewPosition;
            width = this.previewSize;
          }

          var backgrounds = [];

          if (!color) {
            // there is no color preview to display -> let's remove any previous background image
            backgrounds.push({
              image: "none",
              position: "left top",
              size: "auto",
              repeat: "no-repeat",
              origin: "padding-box",
            });
          } else {
            // CSS gradient for background color preview
            backgrounds.push({
              image: jsc.genColorPreviewGradient(
                color,
                position,
                width ? width - jsc.pub.previewSeparator.length : null
              ),
              position: "left top",
              size: "auto",
              repeat: position ? "repeat-y" : "repeat",
              origin: "padding-box",
            });

            // data URL of generated PNG image with a gray transparency chessboard
            var preview = jsc.genColorPreviewCanvas(
              "rgba(0,0,0,0)",
              position ? { left: "right", right: "left" }[position] : null,
              width,
              true
            );
            backgrounds.push({
              image: "url('" + preview.canvas.toDataURL() + "')",
              position: (position || "left") + " top",
              size: preview.width + "px " + preview.height + "px",
              repeat: position ? "repeat-y" : "repeat",
              origin: "padding-box",
            });
          }

          var bg = {
            image: [],
            position: [],
            size: [],
            repeat: [],
            origin: [],
          };
          for (var i = 0; i < backgrounds.length; i += 1) {
            bg.image.push(backgrounds[i].image);
            bg.position.push(backgrounds[i].position);
            bg.size.push(backgrounds[i].size);
            bg.repeat.push(backgrounds[i].repeat);
            bg.origin.push(backgrounds[i].origin);
          }

          // set previewElement's background-images
          var sty = {
            "background-image": bg.image.join(", "),
            "background-position": bg.position.join(", "),
            "background-size": bg.size.join(", "),
            "background-repeat": bg.repeat.join(", "),
            "background-origin": bg.origin.join(", "),
          };
          jsc.setStyle(this.previewElement, sty, this.forceStyle);

          // set/restore previewElement's padding
          var padding = {
            left: null,
            right: null,
          };
          if (position) {
            padding[position] = this.previewSize + this.previewPadding + "px";
          }

          var sty = {
            "padding-left": padding.left,
            "padding-right": padding.right,
          };
          jsc.setStyle(this.previewElement, sty, this.forceStyle, true);
        };

        this.setValueElementValue = function (str) {
          if (this.valueElement) {
            if (jsc.nodeName(this.valueElement) === "input") {
              this.valueElement.value = str;
            } else {
              this.valueElement.innerHTML = str;
            }
          }
        };

        this.setAlphaElementValue = function (str) {
          if (this.alphaElement) {
            if (jsc.nodeName(this.alphaElement) === "input") {
              this.alphaElement.value = str;
            } else {
              this.alphaElement.innerHTML = str;
            }
          }
        };

        this._processParentElementsInDOM = function () {
          if (this._parentElementsProcessed) {
            return;
          }
          this._parentElementsProcessed = true;

          var elm = this.targetElement;
          do {
            // If the target element or one of its parent nodes has fixed position,
            // then use fixed positioning instead
            var compStyle = jsc.getCompStyle(elm);
            if (
              compStyle.position &&
              compStyle.position.toLowerCase() === "fixed"
            ) {
              this.fixed = true;
            }

            if (elm !== this.targetElement) {
              // Ensure to attach onParentScroll only once to each parent element
              // (multiple targetElements can share the same parent nodes)
              //
              // Note: It's not just offsetParents that can be scrollable,
              // that's why we loop through all parent nodes
              if (!jsc.getData(elm, "hasScrollListener")) {
                elm.addEventListener("scroll", jsc.onParentScroll, false);
                jsc.setData(elm, "hasScrollListener", true);
              }
            }
          } while ((elm = elm.parentNode) && jsc.nodeName(elm) !== "body");
        };

        this.tryHide = function () {
          if (this.hideOnLeave) {
            this.hide();
          }
        };

        this.set__palette = function (val) {
          this.palette = val;
          this._palette = jsc.parsePaletteValue(val);
          this._paletteHasTransparency = jsc.containsTranparentColor(
            this._palette
          );
        };

        function setOption(option, value) {
          if (typeof option !== "string") {
            throw new Error("Invalid value for option name: " + option);
          }

          // enum option
          if (jsc.enumOpts.hasOwnProperty(option)) {
            if (typeof value === "string") {
              // enum string values are case insensitive
              value = value.toLowerCase();
            }
            if (jsc.enumOpts[option].indexOf(value) === -1) {
              throw new Error(
                "Option '" + option + "' has invalid value: " + value
              );
            }
          }

          // deprecated option
          if (jsc.deprecatedOpts.hasOwnProperty(option)) {
            var oldOpt = option;
            var newOpt = jsc.deprecatedOpts[option];
            if (newOpt) {
              // if we have a new name for this option, let's log a warning and use the new name
              console.warn(
                "Option '%s' is DEPRECATED, using '%s' instead." + jsc.docsRef,
                oldOpt,
                newOpt
              );
              option = newOpt;
            } else {
              // new name not available for the option
              throw new Error("Option '" + option + "' is DEPRECATED");
            }
          }

          var setter = "set__" + option;

          if (typeof THIS[setter] === "function") {
            // a setter exists for this option
            THIS[setter](value);
            return true;
          } else if (option in THIS) {
            // option exists as a property
            THIS[option] = value;
            return true;
          }

          throw new Error("Unrecognized configuration option: " + option);
        }

        function getOption(option) {
          if (typeof option !== "string") {
            throw new Error("Invalid value for option name: " + option);
          }

          // deprecated option
          if (jsc.deprecatedOpts.hasOwnProperty(option)) {
            var oldOpt = option;
            var newOpt = jsc.deprecatedOpts[option];
            if (newOpt) {
              // if we have a new name for this option, let's log a warning and use the new name
              console.warn(
                "Option '%s' is DEPRECATED, using '%s' instead." + jsc.docsRef,
                oldOpt,
                newOpt
              );
              option = newOpt;
            } else {
              // new name not available for the option
              throw new Error("Option '" + option + "' is DEPRECATED");
            }
          }

          var getter = "get__" + option;

          if (typeof THIS[getter] === "function") {
            // a getter exists for this option
            return THIS[getter](value);
          } else if (option in THIS) {
            // option exists as a property
            return THIS[option];
          }

          throw new Error("Unrecognized configuration option: " + option);
        }

        function detachPicker() {
          jsc.removeClass(THIS.targetElement, jsc.pub.activeClassName);
          jsc.picker.wrap.parentNode.removeChild(jsc.picker.wrap);
          delete jsc.picker.owner;
        }

        function drawPicker() {
          // At this point, when drawing the picker, we know what the parent elements are
          // and we can do all related DOM operations, such as registering events on them
          // or checking their positioning
          THIS._processParentElementsInDOM();

          if (!jsc.picker) {
            jsc.picker = {
              owner: null, // owner picker instance
              wrap: jsc.createEl("div"),
              box: jsc.createEl("div"),
              boxS: jsc.createEl("div"), // shadow area
              boxB: jsc.createEl("div"), // border
              pad: jsc.createEl("div"),
              padB: jsc.createEl("div"), // border
              padM: jsc.createEl("div"), // mouse/touch area
              padCanvas: jsc.createPadCanvas(),
              cross: jsc.createEl("div"),
              crossBY: jsc.createEl("div"), // border Y
              crossBX: jsc.createEl("div"), // border X
              crossLY: jsc.createEl("div"), // line Y
              crossLX: jsc.createEl("div"), // line X
              sld: jsc.createEl("div"), // slider
              sldB: jsc.createEl("div"), // border
              sldM: jsc.createEl("div"), // mouse/touch area
              sldGrad: jsc.createSliderGradient(),
              sldPtrS: jsc.createEl("div"), // slider pointer spacer
              sldPtrIB: jsc.createEl("div"), // slider pointer inner border
              sldPtrMB: jsc.createEl("div"), // slider pointer middle border
              sldPtrOB: jsc.createEl("div"), // slider pointer outer border
              asld: jsc.createEl("div"), // alpha slider
              asldB: jsc.createEl("div"), // border
              asldM: jsc.createEl("div"), // mouse/touch area
              asldGrad: jsc.createASliderGradient(),
              asldPtrS: jsc.createEl("div"), // slider pointer spacer
              asldPtrIB: jsc.createEl("div"), // slider pointer inner border
              asldPtrMB: jsc.createEl("div"), // slider pointer middle border
              asldPtrOB: jsc.createEl("div"), // slider pointer outer border
              pal: jsc.createEl("div"), // palette
              btn: jsc.createEl("div"),
              btnT: jsc.createEl("div"), // text
            };

            jsc.picker.pad.appendChild(jsc.picker.padCanvas.elm);
            jsc.picker.padB.appendChild(jsc.picker.pad);
            jsc.picker.cross.appendChild(jsc.picker.crossBY);
            jsc.picker.cross.appendChild(jsc.picker.crossBX);
            jsc.picker.cross.appendChild(jsc.picker.crossLY);
            jsc.picker.cross.appendChild(jsc.picker.crossLX);
            jsc.picker.padB.appendChild(jsc.picker.cross);
            jsc.picker.box.appendChild(jsc.picker.padB);
            jsc.picker.box.appendChild(jsc.picker.padM);

            jsc.picker.sld.appendChild(jsc.picker.sldGrad.elm);
            jsc.picker.sldB.appendChild(jsc.picker.sld);
            jsc.picker.sldB.appendChild(jsc.picker.sldPtrOB);
            jsc.picker.sldPtrOB.appendChild(jsc.picker.sldPtrMB);
            jsc.picker.sldPtrMB.appendChild(jsc.picker.sldPtrIB);
            jsc.picker.sldPtrIB.appendChild(jsc.picker.sldPtrS);
            jsc.picker.box.appendChild(jsc.picker.sldB);
            jsc.picker.box.appendChild(jsc.picker.sldM);

            jsc.picker.asld.appendChild(jsc.picker.asldGrad.elm);
            jsc.picker.asldB.appendChild(jsc.picker.asld);
            jsc.picker.asldB.appendChild(jsc.picker.asldPtrOB);
            jsc.picker.asldPtrOB.appendChild(jsc.picker.asldPtrMB);
            jsc.picker.asldPtrMB.appendChild(jsc.picker.asldPtrIB);
            jsc.picker.asldPtrIB.appendChild(jsc.picker.asldPtrS);
            jsc.picker.box.appendChild(jsc.picker.asldB);
            jsc.picker.box.appendChild(jsc.picker.asldM);

            jsc.picker.box.appendChild(jsc.picker.pal);

            jsc.picker.btn.appendChild(jsc.picker.btnT);
            jsc.picker.box.appendChild(jsc.picker.btn);

            jsc.picker.boxB.appendChild(jsc.picker.box);
            jsc.picker.wrap.appendChild(jsc.picker.boxS);
            jsc.picker.wrap.appendChild(jsc.picker.boxB);

            jsc.picker.wrap.addEventListener(
              "touchstart",
              jsc.onPickerTouchStart,
              jsc.isPassiveEventSupported ? { passive: false } : false
            );
          }

          var p = jsc.picker;

          var displaySlider = !!jsc.getSliderChannel(THIS);
          var displayAlphaSlider = THIS.hasAlphaChannel();
          var pickerDims = jsc.getPickerDims(THIS);
          var crossOuterSize =
            2 * THIS.pointerBorderWidth +
            THIS.pointerThickness +
            2 * THIS.crossSize;
          var controlPadding = jsc.getControlPadding(THIS);
          var borderRadius = Math.min(
            THIS.borderRadius,
            Math.round(THIS.padding * Math.PI)
          ); // px
          var padCursor = "crosshair";

          // wrap
          p.wrap.className = "jscolor-wrap";
          p.wrap.style.width = pickerDims.borderW + "px";
          p.wrap.style.height = pickerDims.borderH + "px";
          p.wrap.style.zIndex = THIS.zIndex;

          // picker
          p.box.className = "jscolor-picker";
          p.box.style.width = pickerDims.paddedW + "px";
          p.box.style.height = pickerDims.paddedH + "px";

          // picker shadow
          p.boxS.className = "jscolor-shadow";
          jsc.setBorderRadius(p.boxS, borderRadius + "px");

          // picker border
          p.boxB.className = "jscolor-border";
          p.boxB.style.border = THIS.borderWidth + "px solid";
          p.boxB.style.borderColor = THIS.borderColor;
          p.boxB.style.background = THIS.backgroundColor;
          jsc.setBorderRadius(p.boxB, borderRadius + "px");

          // IE hack:
          // If the element is transparent, IE will trigger the event on the elements under it,
          // e.g. on Canvas or on elements with border
          p.padM.style.background = "rgba(255,0,0,.2)";
          p.sldM.style.background = "rgba(0,255,0,.2)";
          p.asldM.style.background = "rgba(0,0,255,.2)";

          p.padM.style.opacity =
            p.sldM.style.opacity =
            p.asldM.style.opacity =
              "0";

          // pad
          p.pad.style.position = "relative";
          p.pad.style.width = THIS.width + "px";
          p.pad.style.height = THIS.height + "px";

          // pad - color spectrum (HSV and HVS)
          p.padCanvas.draw(THIS.width, THIS.height, jsc.getPadYChannel(THIS));

          // pad border
          p.padB.style.position = "absolute";
          p.padB.style.left = THIS.padding + "px";
          p.padB.style.top = THIS.padding + "px";
          p.padB.style.border = THIS.controlBorderWidth + "px solid";
          p.padB.style.borderColor = THIS.controlBorderColor;

          // pad mouse area
          p.padM.style.position = "absolute";
          p.padM.style.left = 0 + "px";
          p.padM.style.top = 0 + "px";
          p.padM.style.width =
            THIS.padding +
            2 * THIS.controlBorderWidth +
            THIS.width +
            controlPadding +
            "px";
          p.padM.style.height =
            2 * THIS.controlBorderWidth + 2 * THIS.padding + THIS.height + "px";
          p.padM.style.cursor = padCursor;
          jsc.setData(p.padM, {
            instance: THIS,
            control: "pad",
          });

          // pad cross
          p.cross.style.position = "absolute";
          p.cross.style.left = p.cross.style.top = "0";
          p.cross.style.width = p.cross.style.height = crossOuterSize + "px";

          // pad cross border Y and X
          p.crossBY.style.position = p.crossBX.style.position = "absolute";
          p.crossBY.style.background = p.crossBX.style.background =
            THIS.pointerBorderColor;
          p.crossBY.style.width = p.crossBX.style.height =
            2 * THIS.pointerBorderWidth + THIS.pointerThickness + "px";
          p.crossBY.style.height = p.crossBX.style.width =
            crossOuterSize + "px";
          p.crossBY.style.left = p.crossBX.style.top =
            Math.floor(crossOuterSize / 2) -
            Math.floor(THIS.pointerThickness / 2) -
            THIS.pointerBorderWidth +
            "px";
          p.crossBY.style.top = p.crossBX.style.left = "0";

          // pad cross line Y and X
          p.crossLY.style.position = p.crossLX.style.position = "absolute";
          p.crossLY.style.background = p.crossLX.style.background =
            THIS.pointerColor;
          p.crossLY.style.height = p.crossLX.style.width =
            crossOuterSize - 2 * THIS.pointerBorderWidth + "px";
          p.crossLY.style.width = p.crossLX.style.height =
            THIS.pointerThickness + "px";
          p.crossLY.style.left = p.crossLX.style.top =
            Math.floor(crossOuterSize / 2) -
            Math.floor(THIS.pointerThickness / 2) +
            "px";
          p.crossLY.style.top = p.crossLX.style.left =
            THIS.pointerBorderWidth + "px";

          // slider
          p.sld.style.overflow = "hidden";
          p.sld.style.width = THIS.sliderSize + "px";
          p.sld.style.height = THIS.height + "px";

          // slider gradient
          p.sldGrad.draw(THIS.sliderSize, THIS.height, "#000", "#000");

          // slider border
          p.sldB.style.display = displaySlider ? "block" : "none";
          p.sldB.style.position = "absolute";
          p.sldB.style.left =
            THIS.padding +
            THIS.width +
            2 * THIS.controlBorderWidth +
            2 * controlPadding +
            "px";
          p.sldB.style.top = THIS.padding + "px";
          p.sldB.style.border = THIS.controlBorderWidth + "px solid";
          p.sldB.style.borderColor = THIS.controlBorderColor;

          // slider mouse area
          p.sldM.style.display = displaySlider ? "block" : "none";
          p.sldM.style.position = "absolute";
          p.sldM.style.left =
            THIS.padding +
            THIS.width +
            2 * THIS.controlBorderWidth +
            controlPadding +
            "px";
          p.sldM.style.top = 0 + "px";
          p.sldM.style.width =
            THIS.sliderSize +
            2 * controlPadding +
            2 * THIS.controlBorderWidth +
            (displayAlphaSlider
              ? 0
              : Math.max(0, THIS.padding - controlPadding)) + // remaining padding to the right edge
            "px";
          p.sldM.style.height =
            2 * THIS.controlBorderWidth + 2 * THIS.padding + THIS.height + "px";
          p.sldM.style.cursor = "default";
          jsc.setData(p.sldM, {
            instance: THIS,
            control: "sld",
          });

          // slider pointer inner and outer border
          p.sldPtrIB.style.border = p.sldPtrOB.style.border =
            THIS.pointerBorderWidth + "px solid " + THIS.pointerBorderColor;

          // slider pointer outer border
          p.sldPtrOB.style.position = "absolute";
          p.sldPtrOB.style.left =
            -(2 * THIS.pointerBorderWidth + THIS.pointerThickness) + "px";
          p.sldPtrOB.style.top = "0";

          // slider pointer middle border
          p.sldPtrMB.style.border =
            THIS.pointerThickness + "px solid " + THIS.pointerColor;

          // slider pointer spacer
          p.sldPtrS.style.width = THIS.sliderSize + "px";
          p.sldPtrS.style.height = jsc.pub.sliderInnerSpace + "px";

          // alpha slider
          p.asld.style.overflow = "hidden";
          p.asld.style.width = THIS.sliderSize + "px";
          p.asld.style.height = THIS.height + "px";

          // alpha slider gradient
          p.asldGrad.draw(THIS.sliderSize, THIS.height, "#000");

          // alpha slider border
          p.asldB.style.display = displayAlphaSlider ? "block" : "none";
          p.asldB.style.position = "absolute";
          p.asldB.style.left =
            THIS.padding +
            THIS.width +
            2 * THIS.controlBorderWidth +
            controlPadding +
            (displaySlider
              ? THIS.sliderSize +
                3 * controlPadding +
                2 * THIS.controlBorderWidth
              : 0) +
            "px";
          p.asldB.style.top = THIS.padding + "px";
          p.asldB.style.border = THIS.controlBorderWidth + "px solid";
          p.asldB.style.borderColor = THIS.controlBorderColor;

          // alpha slider mouse area
          p.asldM.style.display = displayAlphaSlider ? "block" : "none";
          p.asldM.style.position = "absolute";
          p.asldM.style.left =
            THIS.padding +
            THIS.width +
            2 * THIS.controlBorderWidth +
            controlPadding +
            (displaySlider
              ? THIS.sliderSize +
                2 * controlPadding +
                2 * THIS.controlBorderWidth
              : 0) +
            "px";
          p.asldM.style.top = 0 + "px";
          p.asldM.style.width =
            THIS.sliderSize +
            2 * controlPadding +
            2 * THIS.controlBorderWidth +
            Math.max(0, THIS.padding - controlPadding) + // remaining padding to the right edge
            "px";
          p.asldM.style.height =
            2 * THIS.controlBorderWidth + 2 * THIS.padding + THIS.height + "px";
          p.asldM.style.cursor = "default";
          jsc.setData(p.asldM, {
            instance: THIS,
            control: "asld",
          });

          // alpha slider pointer inner and outer border
          p.asldPtrIB.style.border = p.asldPtrOB.style.border =
            THIS.pointerBorderWidth + "px solid " + THIS.pointerBorderColor;

          // alpha slider pointer outer border
          p.asldPtrOB.style.position = "absolute";
          p.asldPtrOB.style.left =
            -(2 * THIS.pointerBorderWidth + THIS.pointerThickness) + "px";
          p.asldPtrOB.style.top = "0";

          // alpha slider pointer middle border
          p.asldPtrMB.style.border =
            THIS.pointerThickness + "px solid " + THIS.pointerColor;

          // alpha slider pointer spacer
          p.asldPtrS.style.width = THIS.sliderSize + "px";
          p.asldPtrS.style.height = jsc.pub.sliderInnerSpace + "px";

          // palette
          p.pal.className = "jscolor-palette";
          p.pal.style.display = pickerDims.palette.rows ? "block" : "none";
          p.pal.style.left = THIS.padding + "px";
          p.pal.style.top =
            2 * THIS.controlBorderWidth + 2 * THIS.padding + THIS.height + "px";

          // palette's color samples

          p.pal.innerHTML = "";

          var chessboard = jsc.genColorPreviewCanvas("rgba(0,0,0,0)");

          var si = 0; // color sample's index
          for (var r = 0; r < pickerDims.palette.rows; r++) {
            for (
              var c = 0;
              c < pickerDims.palette.cols && si < THIS._palette.length;
              c++, si++
            ) {
              var sampleColor = THIS._palette[si];
              var sampleCssColor = jsc.rgbaColor.apply(null, sampleColor.rgba);

              var sc = jsc.createEl("div"); // color sample's color
              sc.style.width =
                pickerDims.palette.cellW - 2 * THIS.controlBorderWidth + "px";
              sc.style.height =
                pickerDims.palette.cellH - 2 * THIS.controlBorderWidth + "px";
              sc.style.backgroundColor = sampleCssColor;

              var sw = jsc.createEl("div"); // color sample's wrap
              sw.className = "jscolor-palette-sw";
              sw.style.left =
                (pickerDims.palette.cols <= 1
                  ? 0
                  : Math.round(
                      10 *
                        (c *
                          ((pickerDims.contentW - pickerDims.palette.cellW) /
                            (pickerDims.palette.cols - 1)))
                    ) / 10) + "px";
              sw.style.top =
                r * (pickerDims.palette.cellH + THIS.paletteSpacing) + "px";
              sw.style.border = THIS.controlBorderWidth + "px solid";
              sw.style.borderColor = THIS.controlBorderColor;
              if (sampleColor.rgba[3] !== null && sampleColor.rgba[3] < 1.0) {
                // only create chessboard background if the sample has transparency
                sw.style.backgroundImage =
                  "url('" + chessboard.canvas.toDataURL() + "')";
                sw.style.backgroundRepeat = "repeat";
                sw.style.backgroundPosition = "center center";
              }
              jsc.setData(sw, {
                instance: THIS,
                control: "palette-sw",
                color: sampleColor,
              });
              sw.addEventListener("click", jsc.onPaletteSampleClick, false);
              sw.appendChild(sc);
              p.pal.appendChild(sw);
            }
          }

          // the Close button
          function setBtnBorder() {
            var insetColors = THIS.controlBorderColor.split(/\s+/);
            var outsetColor =
              insetColors.length < 2
                ? insetColors[0]
                : insetColors[1] +
                  " " +
                  insetColors[0] +
                  " " +
                  insetColors[0] +
                  " " +
                  insetColors[1];
            p.btn.style.borderColor = outsetColor;
          }
          var btnPadding = 15; // px
          p.btn.className = "jscolor-btn jscolor-btn-close";
          p.btn.style.display = THIS.closeButton ? "block" : "none";
          p.btn.style.left = THIS.padding + "px";
          p.btn.style.bottom = THIS.padding + "px";
          p.btn.style.padding = "0 " + btnPadding + "px";
          p.btn.style.maxWidth =
            pickerDims.contentW -
            2 * THIS.controlBorderWidth -
            2 * btnPadding +
            "px";
          p.btn.style.height = THIS.buttonHeight + "px";
          p.btn.style.border = THIS.controlBorderWidth + "px solid";
          setBtnBorder();
          p.btn.style.color = THIS.buttonColor;
          p.btn.onmousedown = function () {
            THIS.hide();
          };
          p.btnT.style.display = "inline";
          p.btnT.style.lineHeight = THIS.buttonHeight + "px";
          p.btnT.innerText = THIS.closeText;

          // reposition the pointers
          redrawPad();
          redrawSld();
          redrawASld();

          // If we are changing the owner without first closing the picker,
          // make sure to first deal with the old owner
          if (jsc.picker.owner && jsc.picker.owner !== THIS) {
            jsc.removeClass(
              jsc.picker.owner.targetElement,
              jsc.pub.activeClassName
            );
          }

          // Set a new picker owner
          jsc.picker.owner = THIS;

          // The redrawPosition() method needs picker.owner to be set, that's why we call it here,
          // after setting the owner
          jsc.redrawPosition();

          if (p.wrap.parentNode !== THIS.container) {
            THIS.container.appendChild(p.wrap);
          }

          jsc.addClass(THIS.targetElement, jsc.pub.activeClassName);
        }

        function redrawPad() {
          // redraw the pad pointer
          var yChannel = jsc.getPadYChannel(THIS);
          var x = Math.round((THIS.channels.h / 360) * (THIS.width - 1));
          var y = Math.round(
            (1 - THIS.channels[yChannel] / 100) * (THIS.height - 1)
          );
          var crossOuterSize =
            2 * THIS.pointerBorderWidth +
            THIS.pointerThickness +
            2 * THIS.crossSize;
          var ofs = -Math.floor(crossOuterSize / 2);
          jsc.picker.cross.style.left = x + ofs + "px";
          jsc.picker.cross.style.top = y + ofs + "px";

          // redraw the slider
          switch (jsc.getSliderChannel(THIS)) {
            case "s":
              var rgb1 = jsc.HSV_RGB(THIS.channels.h, 100, THIS.channels.v);
              var rgb2 = jsc.HSV_RGB(THIS.channels.h, 0, THIS.channels.v);
              var color1 =
                "rgb(" +
                Math.round(rgb1[0]) +
                "," +
                Math.round(rgb1[1]) +
                "," +
                Math.round(rgb1[2]) +
                ")";
              var color2 =
                "rgb(" +
                Math.round(rgb2[0]) +
                "," +
                Math.round(rgb2[1]) +
                "," +
                Math.round(rgb2[2]) +
                ")";
              jsc.picker.sldGrad.draw(
                THIS.sliderSize,
                THIS.height,
                color1,
                color2
              );
              break;
            case "v":
              var rgb = jsc.HSV_RGB(THIS.channels.h, THIS.channels.s, 100);
              var color1 =
                "rgb(" +
                Math.round(rgb[0]) +
                "," +
                Math.round(rgb[1]) +
                "," +
                Math.round(rgb[2]) +
                ")";
              var color2 = "#000";
              jsc.picker.sldGrad.draw(
                THIS.sliderSize,
                THIS.height,
                color1,
                color2
              );
              break;
          }

          // redraw the alpha slider
          jsc.picker.asldGrad.draw(
            THIS.sliderSize,
            THIS.height,
            THIS.toHEXString()
          );
        }

        function redrawSld() {
          var sldChannel = jsc.getSliderChannel(THIS);
          if (sldChannel) {
            // redraw the slider pointer
            var y = Math.round(
              (1 - THIS.channels[sldChannel] / 100) * (THIS.height - 1)
            );
            jsc.picker.sldPtrOB.style.top =
              y -
              (2 * THIS.pointerBorderWidth + THIS.pointerThickness) -
              Math.floor(jsc.pub.sliderInnerSpace / 2) +
              "px";
          }

          // redraw the alpha slider
          jsc.picker.asldGrad.draw(
            THIS.sliderSize,
            THIS.height,
            THIS.toHEXString()
          );
        }

        function redrawASld() {
          var y = Math.round((1 - THIS.channels.a) * (THIS.height - 1));
          jsc.picker.asldPtrOB.style.top =
            y -
            (2 * THIS.pointerBorderWidth + THIS.pointerThickness) -
            Math.floor(jsc.pub.sliderInnerSpace / 2) +
            "px";
        }

        function isPickerOwner() {
          return jsc.picker && jsc.picker.owner === THIS;
        }

        function onValueKeyDown(ev) {
          if (jsc.eventKey(ev) === "Enter") {
            if (THIS.valueElement) {
              THIS.processValueInput(THIS.valueElement.value);
            }
            THIS.tryHide();
          }
        }

        function onAlphaKeyDown(ev) {
          if (jsc.eventKey(ev) === "Enter") {
            if (THIS.alphaElement) {
              THIS.processAlphaInput(THIS.alphaElement.value);
            }
            THIS.tryHide();
          }
        }

        function onValueChange(ev) {
          if (jsc.getData(ev, "internal")) {
            return; // skip if the event was internally triggered by jscolor
          }

          var oldVal = THIS.valueElement.value;

          THIS.processValueInput(THIS.valueElement.value); // this might change the value

          jsc.triggerCallback(THIS, "onChange");

          if (THIS.valueElement.value !== oldVal) {
            // value was additionally changed -> let's trigger the change event again, even though it was natively dispatched
            jsc.triggerInputEvent(THIS.valueElement, "change", true, true);
          }
        }

        function onAlphaChange(ev) {
          if (jsc.getData(ev, "internal")) {
            return; // skip if the event was internally triggered by jscolor
          }

          var oldVal = THIS.alphaElement.value;

          THIS.processAlphaInput(THIS.alphaElement.value); // this might change the value

          jsc.triggerCallback(THIS, "onChange");

          // triggering valueElement's onChange (because changing alpha changes the entire color, e.g. with rgba format)
          jsc.triggerInputEvent(THIS.valueElement, "change", true, true);

          if (THIS.alphaElement.value !== oldVal) {
            // value was additionally changed -> let's trigger the change event again, even though it was natively dispatched
            jsc.triggerInputEvent(THIS.alphaElement, "change", true, true);
          }
        }

        function onValueInput(ev) {
          if (jsc.getData(ev, "internal")) {
            return; // skip if the event was internally triggered by jscolor
          }

          if (THIS.valueElement) {
            THIS.fromString(THIS.valueElement.value, jsc.flags.leaveValue);
          }

          jsc.triggerCallback(THIS, "onInput");

          // triggering valueElement's onInput
          // (not needed, it was dispatched normally by the browser)
        }

        function onAlphaInput(ev) {
          if (jsc.getData(ev, "internal")) {
            return; // skip if the event was internally triggered by jscolor
          }

          if (THIS.alphaElement) {
            THIS.fromHSVA(
              null,
              null,
              null,
              parseFloat(THIS.alphaElement.value),
              jsc.flags.leaveAlpha
            );
          }

          jsc.triggerCallback(THIS, "onInput");

          // triggering valueElement's onInput (because changing alpha changes the entire color, e.g. with rgba format)
          jsc.triggerInputEvent(THIS.valueElement, "input", true, true);
        }

        // let's process the DEPRECATED 'options' property (this will be later removed)
        if (jsc.pub.options) {
          // let's set custom default options, if specified
          for (var opt in jsc.pub.options) {
            if (jsc.pub.options.hasOwnProperty(opt)) {
              try {
                setOption(opt, jsc.pub.options[opt]);
              } catch (e) {
                console.warn(e);
              }
            }
          }
        }

        // let's apply configuration presets
        //
        var presetsArr = [];

        if (opts.preset) {
          if (typeof opts.preset === "string") {
            presetsArr = opts.preset.split(/\s+/);
          } else if (Array.isArray(opts.preset)) {
            presetsArr = opts.preset.slice(); // slice() to clone
          } else {
            console.warn("Unrecognized preset value");
          }
        }

        // always use the 'default' preset. If it's not listed, append it to the end.
        if (presetsArr.indexOf("default") === -1) {
          presetsArr.push("default");
        }

        // let's apply the presets in reverse order, so that should there be any overlapping options,
        // the formerly listed preset will override the latter
        for (var i = presetsArr.length - 1; i >= 0; i -= 1) {
          var pres = presetsArr[i];
          if (!pres) {
            continue; // preset is empty string
          }
          if (!jsc.pub.presets.hasOwnProperty(pres)) {
            console.warn("Unknown preset: %s", pres);
            continue;
          }
          for (var opt in jsc.pub.presets[pres]) {
            if (jsc.pub.presets[pres].hasOwnProperty(opt)) {
              try {
                setOption(opt, jsc.pub.presets[pres][opt]);
              } catch (e) {
                console.warn(e);
              }
            }
          }
        }

        // let's set specific options for this color picker
        var nonProperties = [
          // these options won't be set as instance properties
          "preset",
        ];
        for (var opt in opts) {
          if (opts.hasOwnProperty(opt)) {
            if (nonProperties.indexOf(opt) === -1) {
              try {
                setOption(opt, opts[opt]);
              } catch (e) {
                console.warn(e);
              }
            }
          }
        }

        //
        // Install the color picker on chosen element(s)
        //

        // Determine picker's container element
        if (this.container === undefined) {
          this.container = window.document.body; // default container is BODY element
        } else {
          // explicitly set to custom element
          this.container = jsc.node(this.container);
        }

        if (!this.container) {
          throw new Error(
            "Cannot instantiate color picker without a container element"
          );
        }

        // Fetch the target element
        this.targetElement = jsc.node(targetElement);

        if (!this.targetElement) {
          // temporarily customized error message to help with migrating from versions prior to 2.2
          if (
            typeof targetElement === "string" &&
            /^[a-zA-Z][\w:.-]*$/.test(targetElement)
          ) {
            // targetElement looks like valid ID
            var possiblyId = targetElement;
            throw new Error(
              "If '" +
                possiblyId +
                "' is supposed to be an ID, please use '#" +
                possiblyId +
                "' or any valid CSS selector."
            );
          }

          throw new Error(
            "Cannot instantiate color picker without a target element"
          );
        }

        if (
          this.targetElement.jscolor &&
          this.targetElement.jscolor instanceof jsc.pub
        ) {
          throw new Error("Color picker already installed on this element");
        }

        // link this instance with the target element
        this.targetElement.jscolor = this;
        jsc.addClass(this.targetElement, jsc.pub.className);

        // register this instance
        jsc.instances.push(this);

        // if target is BUTTON
        if (jsc.isButton(this.targetElement)) {
          if (this.targetElement.type.toLowerCase() !== "button") {
            // on buttons, always force type to be 'button', e.g. in situations the target <button> has no type
            // and thus defaults to 'submit' and would submit the form when clicked
            this.targetElement.type = "button";
          }

          if (jsc.isButtonEmpty(this.targetElement)) {
            // empty button
            // it is important to clear element's contents first.
            // if we're re-instantiating color pickers on DOM that has been modified by changing page's innerHTML,
            // we would keep adding more non-breaking spaces to element's content (because element's contents survive
            // innerHTML changes, but picker instances don't)
            jsc.removeChildren(this.targetElement);

            // let's insert a non-breaking space
            this.targetElement.appendChild(
              window.document.createTextNode("\xa0")
            );

            // set min-width = previewSize, if not already greater
            var compStyle = jsc.getCompStyle(this.targetElement);
            var currMinWidth = parseFloat(compStyle["min-width"]) || 0;
            if (currMinWidth < this.previewSize) {
              jsc.setStyle(
                this.targetElement,
                {
                  "min-width": this.previewSize + "px",
                },
                this.forceStyle
              );
            }
          }
        }

        // Determine the value element
        if (this.valueElement === undefined) {
          if (jsc.isTextInput(this.targetElement)) {
            // for text inputs, default valueElement is targetElement
            this.valueElement = this.targetElement;
          } else {
            // leave it undefined
          }
        } else if (this.valueElement === null) {
          // explicitly set to null
          // leave it null
        } else {
          // explicitly set to custom element
          this.valueElement = jsc.node(this.valueElement);
        }

        // Determine the alpha element
        if (this.alphaElement) {
          this.alphaElement = jsc.node(this.alphaElement);
        }

        // Determine the preview element
        if (this.previewElement === undefined) {
          this.previewElement = this.targetElement; // default previewElement is targetElement
        } else if (this.previewElement === null) {
          // explicitly set to null
          // leave it null
        } else {
          // explicitly set to custom element
          this.previewElement = jsc.node(this.previewElement);
        }

        // valueElement
        if (this.valueElement && jsc.isTextInput(this.valueElement)) {
          // If the value element has onInput event already set, we need to detach it and attach AFTER our listener.
          // otherwise the picker instance would still contain the old color when accessed from the onInput handler.
          var valueElementOrigEvents = {
            onInput: this.valueElement.oninput,
          };
          this.valueElement.oninput = null;

          this.valueElement.addEventListener("keydown", onValueKeyDown, false);
          this.valueElement.addEventListener("change", onValueChange, false);
          this.valueElement.addEventListener("input", onValueInput, false);
          // the original event listener must be attached AFTER our handler (to let it first set picker's color)
          if (valueElementOrigEvents.onInput) {
            this.valueElement.addEventListener(
              "input",
              valueElementOrigEvents.onInput,
              false
            );
          }

          this.valueElement.setAttribute("autocomplete", "off");
          this.valueElement.setAttribute("autocorrect", "off");
          this.valueElement.setAttribute("autocapitalize", "off");
          this.valueElement.setAttribute("spellcheck", false);
        }

        // alphaElement
        if (this.alphaElement && jsc.isTextInput(this.alphaElement)) {
          this.alphaElement.addEventListener("keydown", onAlphaKeyDown, false);
          this.alphaElement.addEventListener("change", onAlphaChange, false);
          this.alphaElement.addEventListener("input", onAlphaInput, false);

          this.alphaElement.setAttribute("autocomplete", "off");
          this.alphaElement.setAttribute("autocorrect", "off");
          this.alphaElement.setAttribute("autocapitalize", "off");
          this.alphaElement.setAttribute("spellcheck", false);
        }

        // determine initial color value
        //
        var initValue = "FFFFFF";

        if (this.value !== undefined) {
          initValue = this.value; // get initial color from the 'value' property
        } else if (this.valueElement && this.valueElement.value !== undefined) {
          initValue = this.valueElement.value; // get initial color from valueElement's value
        }

        // determine initial alpha value
        //
        var initAlpha = undefined;

        if (this.alpha !== undefined) {
          initAlpha = "" + this.alpha; // get initial alpha value from the 'alpha' property
        } else if (this.alphaElement && this.alphaElement.value !== undefined) {
          initAlpha = this.alphaElement.value; // get initial color from alphaElement's value
        }

        // determine current format based on the initial color value
        //
        this._currentFormat = null;

        if (["auto", "any"].indexOf(this.format.toLowerCase()) > -1) {
          // format is 'auto' or 'any' -> let's auto-detect current format
          var color = jsc.parseColorString(initValue);
          this._currentFormat = color ? color.format : "hex";
        } else {
          // format is specified
          this._currentFormat = this.format.toLowerCase();
        }

        // let's parse the initial color value and expose color's preview
        this.processValueInput(initValue);

        // let's also parse and expose the initial alpha value, if any
        //
        // Note: If the initial color value contains alpha value in it (e.g. in rgba format),
        // this will overwrite it. So we should only process alpha input if there was initial
        // alpha explicitly set, otherwise we could needlessly lose initial value's alpha
        if (initAlpha !== undefined) {
          this.processAlphaInput(initAlpha);
        }

        if (this.random) {
          // randomize the initial color value
          this.randomize.apply(
            this,
            Array.isArray(this.random) ? this.random : []
          );
        }
      },
    };

    //================================
    // Public properties and methods
    //================================

    //
    // These will be publicly available via jscolor.<name> and JSColor.<name>
    //

    // class that will be set to elements having jscolor installed on them
    jsc.pub.className = "jscolor";

    // class that will be set to elements having jscolor active on them
    jsc.pub.activeClassName = "jscolor-active";

    // whether to try to parse the options string by evaluating it using 'new Function()'
    // in case it could not be parsed with JSON.parse()
    jsc.pub.looseJSON = true;

    // presets
    jsc.pub.presets = {};

    // built-in presets
    jsc.pub.presets["default"] = {}; // baseline for customization

    jsc.pub.presets["light"] = {
      // default color scheme
      backgroundColor: "rgba(255,255,255,1)",
      controlBorderColor: "rgba(187,187,187,1)",
      buttonColor: "rgba(0,0,0,1)",
    };
    jsc.pub.presets["dark"] = {
      backgroundColor: "rgba(51,51,51,1)",
      controlBorderColor: "rgba(153,153,153,1)",
      buttonColor: "rgba(240,240,240,1)",
    };

    jsc.pub.presets["small"] = {
      width: 101,
      height: 101,
      padding: 10,
      sliderSize: 14,
      paletteCols: 8,
    };
    jsc.pub.presets["medium"] = {
      width: 181,
      height: 101,
      padding: 12,
      sliderSize: 16,
      paletteCols: 10,
    }; // default size
    jsc.pub.presets["large"] = {
      width: 271,
      height: 151,
      padding: 12,
      sliderSize: 24,
      paletteCols: 15,
    };

    jsc.pub.presets["thin"] = {
      borderWidth: 1,
      controlBorderWidth: 1,
      pointerBorderWidth: 1,
    }; // default thickness
    jsc.pub.presets["thick"] = {
      borderWidth: 2,
      controlBorderWidth: 2,
      pointerBorderWidth: 2,
    };

    // size of space in the sliders
    jsc.pub.sliderInnerSpace = 3; // px

    // transparency chessboard
    jsc.pub.chessboardSize = 8; // px
    jsc.pub.chessboardColor1 = "#666666";
    jsc.pub.chessboardColor2 = "#999999";

    // preview separator
    jsc.pub.previewSeparator = [
      "rgba(255,255,255,.65)",
      "rgba(128,128,128,.65)",
    ];

    // Initializes jscolor
    jsc.pub.init = function () {
      if (jsc.initialized) {
        return;
      }

      // attach some necessary handlers
      window.document.addEventListener(
        "mousedown",
        jsc.onDocumentMouseDown,
        false
      );
      window.document.addEventListener("keyup", jsc.onDocumentKeyUp, false);
      window.addEventListener("resize", jsc.onWindowResize, false);
      window.addEventListener("scroll", jsc.onWindowScroll, false);

      // append default CSS to HEAD
      jsc.appendDefaultCss();

      // install jscolor on current DOM
      jsc.pub.install();

      jsc.initialized = true;

      // call functions waiting in the queue
      while (jsc.readyQueue.length) {
        var func = jsc.readyQueue.shift();
        func();
      }
    };

    // Installs jscolor on current DOM tree
    jsc.pub.install = function (rootNode) {
      var success = true;

      try {
        jsc.installBySelector("[data-jscolor]", rootNode);
      } catch (e) {
        success = false;
        console.warn(e);
      }

      // for backward compatibility with DEPRECATED installation using class name
      if (jsc.pub.lookupClass) {
        try {
          jsc.installBySelector(
            "input." +
              jsc.pub.lookupClass +
              ", " +
              "button." +
              jsc.pub.lookupClass,
            rootNode
          );
        } catch (e) {}
      }

      return success;
    };

    // Registers function to be called as soon as jscolor is initialized (or immediately, if it already is).
    //
    jsc.pub.ready = function (func) {
      if (typeof func !== "function") {
        console.warn("Passed value is not a function");
        return false;
      }

      if (jsc.initialized) {
        func();
      } else {
        jsc.readyQueue.push(func);
      }
      return true;
    };

    // Triggers given input event(s) (e.g. 'input' or 'change') on all color pickers.
    //
    // It is possible to specify multiple events separated with a space.
    // If called before jscolor is initialized, then the events will be triggered after initialization.
    //
    jsc.pub.trigger = function (eventNames) {
      var triggerNow = function () {
        jsc.triggerGlobal(eventNames);
      };

      if (jsc.initialized) {
        triggerNow();
      } else {
        jsc.pub.ready(triggerNow);
      }
    };

    // Hides current color picker box
    jsc.pub.hide = function () {
      if (jsc.picker && jsc.picker.owner) {
        jsc.picker.owner.hide();
      }
    };

    // Returns a data URL of a gray chessboard image that indicates transparency
    jsc.pub.chessboard = function (color) {
      if (!color) {
        color = "rgba(0,0,0,0)";
      }
      var preview = jsc.genColorPreviewCanvas(color);
      return preview.canvas.toDataURL();
    };

    // Returns a data URL of a gray chessboard image that indicates transparency
    jsc.pub.background = function (color) {
      var backgrounds = [];

      // CSS gradient for background color preview
      backgrounds.push(jsc.genColorPreviewGradient(color));

      // data URL of generated PNG image with a gray transparency chessboard
      var preview = jsc.genColorPreviewCanvas();
      backgrounds.push(
        [
          "url('" + preview.canvas.toDataURL() + "')",
          "left top",
          "repeat",
        ].join(" ")
      );

      return backgrounds.join(", ");
    };

    //
    // DEPRECATED properties and methods
    //

    // DEPRECATED. Use jscolor.presets.default instead.
    //
    // Custom default options for all color pickers, e.g. { hash: true, width: 300 }
    jsc.pub.options = {};

    // DEPRECATED. Use data-jscolor attribute instead, which installs jscolor on given element.
    //
    // By default, we'll search for all elements with class="jscolor" and install a color picker on them.
    //
    // You can change what class name will be looked for by setting the property jscolor.lookupClass
    // anywhere in your HTML document. To completely disable the automatic lookup, set it to null.
    //
    jsc.pub.lookupClass = "jscolor";

    // DEPRECATED. Use data-jscolor attribute instead, which installs jscolor on given element.
    //
    // Install jscolor on all elements that have the specified class name
    jsc.pub.installByClassName = function () {
      console.error(
        'jscolor.installByClassName() is DEPRECATED. Use data-jscolor="" attribute instead of a class name.' +
          jsc.docsRef
      );
      return false;
    };

    jsc.register();

    return jsc.pub;
  })(); // END jscolor

  if (typeof window.jscolor === "undefined") {
    window.jscolor = window.JSColor = jscolor;
  }

  // END jscolor code

  return jscolor;
}); // END factory