gecko-dev/devtools/shared/css-color.js
Nicolas Chevobbe 3976954edf Bug 1259559 - Units cycling with shift+click persists value in Style editor. r=miker
Add an 'unit-change' event fired when shift+click on color and angle swatches.
Add a listener on this event in text-property-editor.js to call the same function
that's called when tooltip edit is commited to persist the new unit.
Edit some tests to adapt to this new behaviour and create some tests to make sure
the value obtained via shift+click are actually persisted.

MozReview-Commit-ID: CcF4oiBPEzT

--HG--
extra : transplant_source : %1B%40%A24%EDW.%C5%96%C8m%3C%1F4%DA%F78%D1%26%8B
2016-03-28 21:01:32 +02:00

475 lines
12 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {Cc, Ci, Cu} = require("chrome");
const Services = require("Services");
const COLOR_UNIT_PREF = "devtools.defaultColorUnit";
const SPECIALVALUES = new Set([
"currentcolor",
"initial",
"inherit",
"transparent",
"unset"
]);
/**
* This module is used to convert between various color types.
*
* Usage:
* let {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
* let {colorUtils} = require("devtools/shared/css-color");
* let color = new colorUtils.CssColor("red");
*
* color.authored === "red"
* color.hasAlpha === false
* color.valid === true
* color.transparent === false // transparent has a special status.
* color.name === "red" // returns hex or rgba when no name available.
* color.hex === "#f00" // returns shortHex when available else returns
* longHex. If alpha channel is present then we
* return this.rgba.
* color.longHex === "#ff0000" // If alpha channel is present then we return
* this.rgba.
* color.rgb === "rgb(255, 0, 0)" // If alpha channel is present then we return
* this.rgba.
* color.rgba === "rgba(255, 0, 0, 1)"
* color.hsl === "hsl(0, 100%, 50%)"
* color.hsla === "hsla(0, 100%, 50%, 1)" // If alpha channel is present
* then we return this.rgba.
*
* color.toString() === "#f00"; // Outputs the color type determined in the
* COLOR_UNIT_PREF constant (above).
* // Color objects can be reused
* color.newColor("green") === "#0f0"; // true
*
* Valid values for COLOR_UNIT_PREF are contained in CssColor.COLORUNIT.
*/
function CssColor(colorValue) {
this.newColor(colorValue);
}
module.exports.colorUtils = {
CssColor: CssColor,
rgbToHsl: rgbToHsl,
setAlpha: setAlpha,
classifyColor: classifyColor
};
/**
* Values used in COLOR_UNIT_PREF
*/
CssColor.COLORUNIT = {
"authored": "authored",
"hex": "hex",
"name": "name",
"rgb": "rgb",
"hsl": "hsl"
};
CssColor.prototype = {
_colorUnit: null,
_colorUnitUppercase: false,
// The value as-authored.
authored: null,
// A lower-cased copy of |authored|.
lowerCased: null,
get colorUnit() {
if (this._colorUnit === null) {
let defaultUnit = Services.prefs.getCharPref(COLOR_UNIT_PREF);
this._colorUnit = CssColor.COLORUNIT[defaultUnit];
this._colorUnitUppercase =
(this.authored === this.authored.toUpperCase());
}
return this._colorUnit;
},
set colorUnit(unit) {
this._colorUnit = unit;
},
/**
* If the current color unit pref is "authored", then set the
* default color unit from the given color. Otherwise, leave the
* color unit untouched.
*
* @param {String} color The color to use
*/
setAuthoredUnitFromColor: function(color) {
if (Services.prefs.getCharPref(COLOR_UNIT_PREF) ===
CssColor.COLORUNIT.authored) {
this._colorUnit = classifyColor(color);
this._colorUnitUppercase = (color === color.toUpperCase());
}
},
get hasAlpha() {
if (!this.valid) {
return false;
}
return this._getRGBATuple().a !== 1;
},
get valid() {
return DOMUtils.isValidCSSColor(this.authored);
},
/**
* Return true for all transparent values e.g. rgba(0, 0, 0, 0).
*/
get transparent() {
try {
let tuple = this._getRGBATuple();
return !(tuple.r || tuple.g || tuple.b || tuple.a);
} catch(e) {
return false;
}
},
get specialValue() {
return SPECIALVALUES.has(this.lowerCased) ? this.authored : null;
},
get name() {
let invalidOrSpecialValue = this._getInvalidOrSpecialValue();
if (invalidOrSpecialValue !== false) {
return invalidOrSpecialValue;
}
try {
let tuple = this._getRGBATuple();
if (tuple.a !== 1) {
return this.rgb;
}
let {r, g, b} = tuple;
return DOMUtils.rgbToColorName(r, g, b);
} catch(e) {
return this.hex;
}
},
get hex() {
let invalidOrSpecialValue = this._getInvalidOrSpecialValue();
if (invalidOrSpecialValue !== false) {
return invalidOrSpecialValue;
}
if (this.hasAlpha) {
return this.rgba;
}
let hex = this.longHex;
if (hex.charAt(1) == hex.charAt(2) &&
hex.charAt(3) == hex.charAt(4) &&
hex.charAt(5) == hex.charAt(6)) {
hex = "#" + hex.charAt(1) + hex.charAt(3) + hex.charAt(5);
}
return hex;
},
get longHex() {
let invalidOrSpecialValue = this._getInvalidOrSpecialValue();
if (invalidOrSpecialValue !== false) {
return invalidOrSpecialValue;
}
if (this.hasAlpha) {
return this.rgba;
}
let tuple = this._getRGBATuple();
return "#" + ((1 << 24) + (tuple.r << 16) + (tuple.g << 8) + (tuple.b << 0)).toString(16).substr(-6);
},
get rgb() {
let invalidOrSpecialValue = this._getInvalidOrSpecialValue();
if (invalidOrSpecialValue !== false) {
return invalidOrSpecialValue;
}
if (!this.hasAlpha) {
if (this.lowerCased.startsWith("rgb(")) {
// The color is valid and begins with rgb(.
return this.authored;
}
let tuple = this._getRGBATuple();
return "rgb(" + tuple.r + ", " + tuple.g + ", " + tuple.b + ")";
}
return this.rgba;
},
get rgba() {
let invalidOrSpecialValue = this._getInvalidOrSpecialValue();
if (invalidOrSpecialValue !== false) {
return invalidOrSpecialValue;
}
if (this.lowerCased.startsWith("rgba(")) {
// The color is valid and begins with rgba(.
return this.authored;
}
let components = this._getRGBATuple();
return "rgba(" + components.r + ", " +
components.g + ", " +
components.b + ", " +
components.a + ")";
},
get hsl() {
let invalidOrSpecialValue = this._getInvalidOrSpecialValue();
if (invalidOrSpecialValue !== false) {
return invalidOrSpecialValue;
}
if (this.lowerCased.startsWith("hsl(")) {
// The color is valid and begins with hsl(.
return this.authored;
}
if (this.hasAlpha) {
return this.hsla;
}
return this._hsl();
},
get hsla() {
let invalidOrSpecialValue = this._getInvalidOrSpecialValue();
if (invalidOrSpecialValue !== false) {
return invalidOrSpecialValue;
}
if (this.lowerCased.startsWith("hsla(")) {
// The color is valid and begins with hsla(.
return this.authored;
}
if (this.hasAlpha) {
let a = this._getRGBATuple().a;
return this._hsl(a);
}
return this._hsl(1);
},
/**
* Check whether the current color value is in the special list e.g.
* transparent or invalid.
*
* @return {String|Boolean}
* - If the current color is a special value e.g. "transparent" then
* return the color.
* - If the color is invalid return an empty string.
* - If the color is a regular color e.g. #F06 so we return false
* to indicate that the color is neither invalid or special.
*/
_getInvalidOrSpecialValue: function() {
if (this.specialValue) {
return this.specialValue;
}
if (!this.valid) {
return "";
}
return false;
},
/**
* Change color
*
* @param {String} color
* Any valid color string
*/
newColor: function(color) {
// Store a lower-cased version of the color to help with format
// testing. The original text is kept as well so it can be
// returned when needed.
this.lowerCased = color.toLowerCase();
this.authored = color;
return this;
},
nextColorUnit: function() {
// Reorder the formats array to have the current format at the
// front so we can cycle through.
let formats = ["hex", "hsl", "rgb", "name"];
let currentFormat = classifyColor(this.toString());
let putOnEnd = formats.splice(0, formats.indexOf(currentFormat));
formats = formats.concat(putOnEnd);
let currentDisplayedColor = this[formats[0]];
for (let format of formats) {
if (this[format].toLowerCase() !== currentDisplayedColor.toLowerCase()) {
this.colorUnit = CssColor.COLORUNIT[format];
break;
}
}
return this.toString();
},
/**
* Return a string representing a color of type defined in COLOR_UNIT_PREF.
*/
toString: function() {
let color;
switch(this.colorUnit) {
case CssColor.COLORUNIT.authored:
color = this.authored;
break;
case CssColor.COLORUNIT.hex:
color = this.hex;
break;
case CssColor.COLORUNIT.hsl:
color = this.hsl;
break;
case CssColor.COLORUNIT.name:
color = this.name;
break;
case CssColor.COLORUNIT.rgb:
color = this.rgb;
break;
default:
color = this.rgb;
}
if (this._colorUnitUppercase &&
this.colorUnit != CssColor.COLORUNIT.authored) {
color = color.toUpperCase();
}
return color;
},
/**
* Returns a RGBA 4-Tuple representation of a color or transparent as
* appropriate.
*/
_getRGBATuple: function() {
let tuple = DOMUtils.colorToRGBA(this.authored);
tuple.a = parseFloat(tuple.a.toFixed(1));
return tuple;
},
_hsl: function(maybeAlpha) {
if (this.lowerCased.startsWith("hsl(") && maybeAlpha === undefined) {
// We can use it as-is.
return this.authored;
}
let {r, g, b} = this._getRGBATuple();
let [h,s,l] = rgbToHsl([r,g,b]);
if (maybeAlpha !== undefined) {
return "hsla(" + h + ", " + s + "%, " + l + "%, " + maybeAlpha + ")";
} else {
return "hsl(" + h + ", " + s + "%, " + l + "%)";
}
},
/**
* This method allows comparison of CssColor objects using ===.
*/
valueOf: function() {
return this.rgba;
},
};
/**
* Convert rgb value to hsl
*
* @param {array} rgb
* Array of rgb values
* @return {array}
* Array of hsl values.
*/
function rgbToHsl([r,g,b]) {
r = r / 255;
g = g / 255;
b = b / 255;
let max = Math.max(r, g, b);
let min = Math.min(r, g, b);
let h;
let s;
let l = (max + min) / 2;
if (max == min){
h = s = 0;
} else {
let d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch(max) {
case r:
h = ((g - b) / d) % 6;
break;
case g:
h = (b - r) / d + 2;
break;
case b:
h = (r - g) / d + 4;
break;
}
h *= 60;
if (h < 0) {
h += 360;
}
}
return [Math.round(h), Math.round(s * 100), Math.round(l * 100)];
}
/**
* Takes a color value of any type (hex, hsl, hsla, rgb, rgba)
* and an alpha value to generate an rgba string with the correct
* alpha value.
*
* @param {String} colorValue
* Color in the form of hex, hsl, hsla, rgb, rgba.
* @param {Number} alpha
* Alpha value for the color, between 0 and 1.
* @return {String}
* Converted color with `alpha` value in rgba form.
*/
function setAlpha(colorValue, alpha) {
let color = new CssColor(colorValue);
// Throw if the color supplied is not valid.
if (!color.valid) {
throw new Error("Invalid color.");
}
// If an invalid alpha valid, just set to 1.
if (!(alpha >= 0 && alpha <= 1)) {
alpha = 1;
}
let { r, g, b } = color._getRGBATuple();
return "rgba(" + r + ", " + g + ", " + b + ", " + alpha + ")";
}
/**
* Given a color, classify its type as one of the possible color
* units, as known by |CssColor.colorUnit|.
*
* @param {String} value
* The color, in any form accepted by CSS.
* @return {String}
* The color classification, one of "rgb", "hsl", "hex", or "name".
*/
function classifyColor(value) {
value = value.toLowerCase();
if (value.startsWith("rgb(") || value.startsWith("rgba(")) {
return CssColor.COLORUNIT.rgb;
} else if (value.startsWith("hsl(") || value.startsWith("hsla(")) {
return CssColor.COLORUNIT.hsl;
} else if (/^#[0-9a-f]+$/.exec(value)) {
return CssColor.COLORUNIT.hex;
}
return CssColor.COLORUNIT.name;
}
loader.lazyGetter(this, "DOMUtils", function () {
return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
});