browser/devtools/shared/widgets/Spectrum.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/browser/devtools/shared/widgets/Spectrum.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,337 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +"use strict";
     1.9 +
    1.10 +const EventEmitter = require("devtools/toolkit/event-emitter");
    1.11 +
    1.12 +/**
    1.13 + * Spectrum creates a color picker widget in any container you give it.
    1.14 + *
    1.15 + * Simple usage example:
    1.16 + *
    1.17 + * const {Spectrum} = require("devtools/shared/widgets/Spectrum");
    1.18 + * let s = new Spectrum(containerElement, [255, 126, 255, 1]);
    1.19 + * s.on("changed", (event, rgba, color) => {
    1.20 + *   console.log("rgba(" + rgba[0] + ", " + rgba[1] + ", " + rgba[2] + ", " + rgba[3] + ")");
    1.21 + * });
    1.22 + * s.show();
    1.23 + * s.destroy();
    1.24 + *
    1.25 + * Note that the color picker is hidden by default and you need to call show to
    1.26 + * make it appear. This 2 stages initialization helps in cases you are creating
    1.27 + * the color picker in a parent element that hasn't been appended anywhere yet
    1.28 + * or that is hidden. Calling show() when the parent element is appended and
    1.29 + * visible will allow spectrum to correctly initialize its various parts.
    1.30 + *
    1.31 + * Fires the following events:
    1.32 + * - changed : When the user changes the current color
    1.33 + */
    1.34 +function Spectrum(parentEl, rgb) {
    1.35 +  EventEmitter.decorate(this);
    1.36 +
    1.37 +  this.element = parentEl.ownerDocument.createElement('div');
    1.38 +  this.parentEl = parentEl;
    1.39 +
    1.40 +  this.element.className = "spectrum-container";
    1.41 +  this.element.innerHTML = [
    1.42 +    "<div class='spectrum-top'>",
    1.43 +      "<div class='spectrum-fill'></div>",
    1.44 +      "<div class='spectrum-top-inner'>",
    1.45 +        "<div class='spectrum-color spectrum-box'>",
    1.46 +          "<div class='spectrum-sat'>",
    1.47 +            "<div class='spectrum-val'>",
    1.48 +              "<div class='spectrum-dragger'></div>",
    1.49 +            "</div>",
    1.50 +          "</div>",
    1.51 +        "</div>",
    1.52 +        "<div class='spectrum-hue spectrum-box'>",
    1.53 +          "<div class='spectrum-slider spectrum-slider-control'></div>",
    1.54 +        "</div>",
    1.55 +      "</div>",
    1.56 +    "</div>",
    1.57 +    "<div class='spectrum-alpha spectrum-checker spectrum-box'>",
    1.58 +      "<div class='spectrum-alpha-inner'>",
    1.59 +        "<div class='spectrum-alpha-handle spectrum-slider-control'></div>",
    1.60 +      "</div>",
    1.61 +    "</div>",
    1.62 +  ].join("");
    1.63 +
    1.64 +  this.onElementClick = this.onElementClick.bind(this);
    1.65 +  this.element.addEventListener("click", this.onElementClick, false);
    1.66 +
    1.67 +  this.parentEl.appendChild(this.element);
    1.68 +
    1.69 +  this.slider = this.element.querySelector(".spectrum-hue");
    1.70 +  this.slideHelper = this.element.querySelector(".spectrum-slider");
    1.71 +  Spectrum.draggable(this.slider, this.onSliderMove.bind(this));
    1.72 +
    1.73 +  this.dragger = this.element.querySelector(".spectrum-color");
    1.74 +  this.dragHelper = this.element.querySelector(".spectrum-dragger");
    1.75 +  Spectrum.draggable(this.dragger, this.onDraggerMove.bind(this));
    1.76 +
    1.77 +  this.alphaSlider = this.element.querySelector(".spectrum-alpha");
    1.78 +  this.alphaSliderInner = this.element.querySelector(".spectrum-alpha-inner");
    1.79 +  this.alphaSliderHelper = this.element.querySelector(".spectrum-alpha-handle");
    1.80 +  Spectrum.draggable(this.alphaSliderInner, this.onAlphaSliderMove.bind(this));
    1.81 +
    1.82 +  if (rgb) {
    1.83 +    this.rgb = rgb;
    1.84 +    this.updateUI();
    1.85 +  }
    1.86 +}
    1.87 +
    1.88 +module.exports.Spectrum = Spectrum;
    1.89 +
    1.90 +Spectrum.hsvToRgb = function(h, s, v, a) {
    1.91 +  let r, g, b;
    1.92 +
    1.93 +  let i = Math.floor(h * 6);
    1.94 +  let f = h * 6 - i;
    1.95 +  let p = v * (1 - s);
    1.96 +  let q = v * (1 - f * s);
    1.97 +  let t = v * (1 - (1 - f) * s);
    1.98 +
    1.99 +  switch(i % 6) {
   1.100 +    case 0: r = v, g = t, b = p; break;
   1.101 +    case 1: r = q, g = v, b = p; break;
   1.102 +    case 2: r = p, g = v, b = t; break;
   1.103 +    case 3: r = p, g = q, b = v; break;
   1.104 +    case 4: r = t, g = p, b = v; break;
   1.105 +    case 5: r = v, g = p, b = q; break;
   1.106 +  }
   1.107 +
   1.108 +  return [r * 255, g * 255, b * 255, a];
   1.109 +};
   1.110 +
   1.111 +Spectrum.rgbToHsv = function(r, g, b, a) {
   1.112 +  r = r / 255;
   1.113 +  g = g / 255;
   1.114 +  b = b / 255;
   1.115 +
   1.116 +  let max = Math.max(r, g, b), min = Math.min(r, g, b);
   1.117 +  let h, s, v = max;
   1.118 +
   1.119 +  let d = max - min;
   1.120 +  s = max == 0 ? 0 : d / max;
   1.121 +
   1.122 +  if(max == min) {
   1.123 +    h = 0; // achromatic
   1.124 +  }
   1.125 +  else {
   1.126 +    switch(max) {
   1.127 +      case r: h = (g - b) / d + (g < b ? 6 : 0); break;
   1.128 +      case g: h = (b - r) / d + 2; break;
   1.129 +      case b: h = (r - g) / d + 4; break;
   1.130 +    }
   1.131 +    h /= 6;
   1.132 +  }
   1.133 +  return [h, s, v, a];
   1.134 +};
   1.135 +
   1.136 +Spectrum.getOffset = function(el) {
   1.137 +  let curleft = 0, curtop = 0;
   1.138 +  if (el.offsetParent) {
   1.139 +    while (el) {
   1.140 +      curleft += el.offsetLeft;
   1.141 +      curtop += el.offsetTop;
   1.142 +      el = el.offsetParent;
   1.143 +    }
   1.144 +  }
   1.145 +  return {
   1.146 +    left: curleft,
   1.147 +    top: curtop
   1.148 +  };
   1.149 +};
   1.150 +
   1.151 +Spectrum.draggable = function(element, onmove, onstart, onstop) {
   1.152 +  onmove = onmove || function() {};
   1.153 +  onstart = onstart || function() {};
   1.154 +  onstop = onstop || function() {};
   1.155 +
   1.156 +  let doc = element.ownerDocument;
   1.157 +  let dragging = false;
   1.158 +  let offset = {};
   1.159 +  let maxHeight = 0;
   1.160 +  let maxWidth = 0;
   1.161 +
   1.162 +  function prevent(e) {
   1.163 +    e.stopPropagation();
   1.164 +    e.preventDefault();
   1.165 +  }
   1.166 +
   1.167 +  function move(e) {
   1.168 +    if (dragging) {
   1.169 +      let pageX = e.pageX;
   1.170 +      let pageY = e.pageY;
   1.171 +
   1.172 +      let dragX = Math.max(0, Math.min(pageX - offset.left, maxWidth));
   1.173 +      let dragY = Math.max(0, Math.min(pageY - offset.top, maxHeight));
   1.174 +
   1.175 +      onmove.apply(element, [dragX, dragY]);
   1.176 +    }
   1.177 +  }
   1.178 +
   1.179 +  function start(e) {
   1.180 +    let rightclick = e.which === 3;
   1.181 +
   1.182 +    if (!rightclick && !dragging) {
   1.183 +      if (onstart.apply(element, arguments) !== false) {
   1.184 +        dragging = true;
   1.185 +        maxHeight = element.offsetHeight;
   1.186 +        maxWidth = element.offsetWidth;
   1.187 +
   1.188 +        offset = Spectrum.getOffset(element);
   1.189 +
   1.190 +        move(e);
   1.191 +
   1.192 +        doc.addEventListener("selectstart", prevent, false);
   1.193 +        doc.addEventListener("dragstart", prevent, false);
   1.194 +        doc.addEventListener("mousemove", move, false);
   1.195 +        doc.addEventListener("mouseup", stop, false);
   1.196 +
   1.197 +        prevent(e);
   1.198 +      }
   1.199 +    }
   1.200 +  }
   1.201 +
   1.202 +  function stop() {
   1.203 +    if (dragging) {
   1.204 +      doc.removeEventListener("selectstart", prevent, false);
   1.205 +      doc.removeEventListener("dragstart", prevent, false);
   1.206 +      doc.removeEventListener("mousemove", move, false);
   1.207 +      doc.removeEventListener("mouseup", stop, false);
   1.208 +      onstop.apply(element, arguments);
   1.209 +    }
   1.210 +    dragging = false;
   1.211 +  }
   1.212 +
   1.213 +  element.addEventListener("mousedown", start, false);
   1.214 +};
   1.215 +
   1.216 +Spectrum.prototype = {
   1.217 +  set rgb(color) {
   1.218 +    this.hsv = Spectrum.rgbToHsv(color[0], color[1], color[2], color[3]);
   1.219 +  },
   1.220 +
   1.221 +  get rgb() {
   1.222 +    let rgb = Spectrum.hsvToRgb(this.hsv[0], this.hsv[1], this.hsv[2], this.hsv[3]);
   1.223 +    return [Math.round(rgb[0]), Math.round(rgb[1]), Math.round(rgb[2]), Math.round(rgb[3]*100)/100];
   1.224 +  },
   1.225 +
   1.226 +  get rgbNoSatVal() {
   1.227 +    let rgb = Spectrum.hsvToRgb(this.hsv[0], 1, 1);
   1.228 +    return [Math.round(rgb[0]), Math.round(rgb[1]), Math.round(rgb[2]), rgb[3]];
   1.229 +  },
   1.230 +
   1.231 +  get rgbCssString() {
   1.232 +    let rgb = this.rgb;
   1.233 +    return "rgba(" + rgb[0] + ", " + rgb[1] + ", " + rgb[2] + ", " + rgb[3] + ")";
   1.234 +  },
   1.235 +
   1.236 +  show: function() {
   1.237 +    this.element.classList.add('spectrum-show');
   1.238 +
   1.239 +    this.slideHeight = this.slider.offsetHeight;
   1.240 +    this.dragWidth = this.dragger.offsetWidth;
   1.241 +    this.dragHeight = this.dragger.offsetHeight;
   1.242 +    this.dragHelperHeight = this.dragHelper.offsetHeight;
   1.243 +    this.slideHelperHeight = this.slideHelper.offsetHeight;
   1.244 +    this.alphaSliderWidth = this.alphaSliderInner.offsetWidth;
   1.245 +    this.alphaSliderHelperWidth = this.alphaSliderHelper.offsetWidth;
   1.246 +
   1.247 +    this.updateUI();
   1.248 +  },
   1.249 +
   1.250 +  onElementClick: function(e) {
   1.251 +    e.stopPropagation();
   1.252 +  },
   1.253 +
   1.254 +  onSliderMove: function(dragX, dragY) {
   1.255 +    this.hsv[0] = (dragY / this.slideHeight);
   1.256 +    this.updateUI();
   1.257 +    this.onChange();
   1.258 +  },
   1.259 +
   1.260 +  onDraggerMove: function(dragX, dragY) {
   1.261 +    this.hsv[1] = dragX / this.dragWidth;
   1.262 +    this.hsv[2] = (this.dragHeight - dragY) / this.dragHeight;
   1.263 +    this.updateUI();
   1.264 +    this.onChange();
   1.265 +  },
   1.266 +
   1.267 +  onAlphaSliderMove: function(dragX, dragY) {
   1.268 +    this.hsv[3] = dragX / this.alphaSliderWidth;
   1.269 +    this.updateUI();
   1.270 +    this.onChange();
   1.271 +  },
   1.272 +
   1.273 +  onChange: function() {
   1.274 +    this.emit("changed", this.rgb, this.rgbCssString);
   1.275 +  },
   1.276 +
   1.277 +  updateHelperLocations: function() {
   1.278 +    // If the UI hasn't been shown yet then none of the dimensions will be correct
   1.279 +    if (!this.element.classList.contains('spectrum-show'))
   1.280 +      return;
   1.281 +
   1.282 +    let h = this.hsv[0];
   1.283 +    let s = this.hsv[1];
   1.284 +    let v = this.hsv[2];
   1.285 +
   1.286 +    // Placing the color dragger
   1.287 +    let dragX = s * this.dragWidth;
   1.288 +    let dragY = this.dragHeight - (v * this.dragHeight);
   1.289 +    let helperDim = this.dragHelperHeight/2;
   1.290 +
   1.291 +    dragX = Math.max(
   1.292 +      -helperDim,
   1.293 +      Math.min(this.dragWidth - helperDim, dragX - helperDim)
   1.294 +    );
   1.295 +    dragY = Math.max(
   1.296 +      -helperDim,
   1.297 +      Math.min(this.dragHeight - helperDim, dragY - helperDim)
   1.298 +    );
   1.299 +
   1.300 +    this.dragHelper.style.top = dragY + "px";
   1.301 +    this.dragHelper.style.left = dragX + "px";
   1.302 +
   1.303 +    // Placing the hue slider
   1.304 +    let slideY = (h * this.slideHeight) - this.slideHelperHeight/2;
   1.305 +    this.slideHelper.style.top = slideY + "px";
   1.306 +
   1.307 +    // Placing the alpha slider
   1.308 +    let alphaSliderX = (this.hsv[3] * this.alphaSliderWidth) - (this.alphaSliderHelperWidth / 2);
   1.309 +    this.alphaSliderHelper.style.left = alphaSliderX + "px";
   1.310 +  },
   1.311 +
   1.312 +  updateUI: function() {
   1.313 +    this.updateHelperLocations();
   1.314 +
   1.315 +    let rgb = this.rgb;
   1.316 +    let rgbNoSatVal = this.rgbNoSatVal;
   1.317 +
   1.318 +    let flatColor = "rgb(" + rgbNoSatVal[0] + ", " + rgbNoSatVal[1] + ", " + rgbNoSatVal[2] + ")";
   1.319 +    let fullColor = "rgba(" + rgb[0] + ", " + rgb[1] + ", " + rgb[2] + ", " + rgb[3] + ")";
   1.320 +
   1.321 +    this.dragger.style.backgroundColor = flatColor;
   1.322 +
   1.323 +    var rgbNoAlpha = "rgb(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + ")";
   1.324 +    var rgbAlpha0 = "rgba(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + ", 0)";
   1.325 +    var alphaGradient = "linear-gradient(to right, " + rgbAlpha0 + ", " + rgbNoAlpha + ")";
   1.326 +    this.alphaSliderInner.style.background = alphaGradient;
   1.327 +  },
   1.328 +
   1.329 +  destroy: function() {
   1.330 +    this.element.removeEventListener("click", this.onElementClick, false);
   1.331 +
   1.332 +    this.parentEl.removeChild(this.element);
   1.333 +
   1.334 +    this.slider = null;
   1.335 +    this.dragger = null;
   1.336 +    this.alphaSlider = this.alphaSliderInner = this.alphaSliderHelper = null;
   1.337 +    this.parentEl = null;
   1.338 +    this.element = null;
   1.339 +  }
   1.340 +};

mercurial