--- a/devtools/client/jar.mn
+++ b/devtools/client/jar.mn
@@ -81,16 +81,17 @@ devtools.jar:
content/framework/toolbox-process-window.js (framework/toolbox-process-window.js)
content/inspector/index.xhtml (inspector/index.xhtml)
content/framework/connect/connect.xhtml (framework/connect/connect.xhtml)
content/framework/connect/connect.css (framework/connect/connect.css)
content/framework/connect/connect.js (framework/connect/connect.js)
content/shared/widgets/graphs-frame.xhtml (shared/widgets/graphs-frame.xhtml)
content/shared/widgets/cubic-bezier.css (shared/widgets/cubic-bezier.css)
content/shared/widgets/filter-widget.css (shared/widgets/filter-widget.css)
+ content/shared/widgets/color-widget.css (shared/widgets/color-widget.css)
content/shared/widgets/spectrum.css (shared/widgets/spectrum.css)
content/aboutdebugging/aboutdebugging.xhtml (aboutdebugging/aboutdebugging.xhtml)
content/aboutdebugging/aboutdebugging.css (aboutdebugging/aboutdebugging.css)
content/responsive.html/index.xhtml (responsive.html/index.xhtml)
content/dom/index.html (dom/index.html)
content/dom/main.js (dom/main.js)
content/accessibility/index.html (accessibility/index.html)
content/accessibility/main.js (accessibility/main.js)
--- a/devtools/client/locales/en-US/inspector.properties
+++ b/devtools/client/locales/en-US/inspector.properties
@@ -415,16 +415,54 @@ inspector.sidebar.newBadge=new!
# This is the title shown in a tab in the side panel of the Inspector panel
# that corresponds to the tool displaying animations defined in the page.
inspector.sidebar.animationInspectorTitle=Animations
# LOCALIZATION NOTE (inspector.eyedropper.label): A string displayed as the tooltip of
# a button in the inspector which toggles the Eyedropper tool
inspector.eyedropper.label=Grab a color from the page
+# LOCALIZATION NOTE (inspector.colorwidget.colorNameLabel):
+# The label for the current color widget's color name field.
+inspector.colorwidget.colorNameLabel=Color Name:
+
+# LOCALIZATION NOTE (inspector.colorwidget.contrastRatio.header):
+# This string is used as a header to indicate the contrast section of the
+# color widget.
+inspector.colorwidget.contrastRatio.header=Contrast Ratio
+
+# LOCALIZATION NOTE (inspector.colorwidget.contrastRatio.invalidColor):
+# This string is used when an invalid color is passed as a background color
+# to the color widget.
+inspector.colorwidget.contrastRatio.invalidColor=Invalid color
+
+# LOCALIZATION NOTE (inspector.colorwidget.contrastRatio.info):
+# This string is used to explain the contrast ratio grading system when you hover over the help icon in the contrast info.
+inspector.colorwidget.contrastRatio.info=The contrast ratio grading system for text has the following grading: Fail, AA*, AAA* AAA from lowest to highest readability.\nIt was calculated based on the computed background color:
+
+# LOCALIZATION NOTE (inspector.colorwidget.contrastRatio.failGrade):
+# This string is used to indicate that the text fails for contrast ratio grading criteria.
+inspector.colorwidget.contrastRatio.failGrade=Fail
+
+# LOCALIZATION NOTE (inspector.colorwidget.contrastRatio.failInfo):
+# This string is used to explain that the text fails for contrast ratio grading criteria.
+inspector.colorwidget.contrastRatio.failInfo=This contrast ratio fails for all text sizes.
+
+# LOCALIZATION NOTE (inspector.colorwidget.contrastRatio.AABigInfo):
+# This string is used to explain that the text passes AA* grade for contrast ratio.
+inspector.colorwidget.contrastRatio.AABigInfo=This contrast ratio passes the AA grade for big text (at least 18 point or 14 point bold sized text).
+
+# LOCALIZATION NOTE (inspector.colorwidget.contrastRatio.AAABigInfo):
+# This string is used to explain that the text passes the AA grade and AAA* for contrast ratio.
+inspector.colorwidget.contrastRatio.AAABigInfo=This contrast ratio passes the AA grade for all text and AAA grade for big text (at least 18 point or 14 point bold sized text).
+
+# LOCALIZATION NOTE (inspector.colorwidget.contrastRatio.AAAInfo):
+# This string is used to explain that the text passes AAA grade for contrast ratio.
+inspector.colorwidget.contrastRatio.AAAInfo=This contrast ratio passes the AAA grade for all text sizes.
+
# LOCALIZATION NOTE (inspector.breadcrumbs.label): A string visible only to a screen reader and
# is used to label (using aria-label attribute) a container for inspector breadcrumbs
inspector.breadcrumbs.label=Breadcrumbs
# LOCALIZATION NOTE (inspector.browserStyles.label): This is the label for the checkbox
# that specifies whether the styles that are not from the user's stylesheet should be
# displayed or not.
inspector.browserStyles.label=Browser styles
--- a/devtools/client/preferences/devtools-client.js
+++ b/devtools/client/preferences/devtools-client.js
@@ -60,16 +60,18 @@ pref("devtools.inspector.showAllAnonymou
// Enable the Flexbox highlighter
pref("devtools.inspector.flexboxHighlighter.enabled", false);
// Enable the CSS shapes highlighter
pref("devtools.inspector.shapesHighlighter.enabled", true);
// Enable the Flexbox Inspector panel
pref("devtools.flexboxinspector.enabled", false);
// Enable the new Animation Inspector
pref("devtools.new-animationinspector.enabled", true);
+// Enable the new color widget
+pref("devtools.inspector.colorWidget.enabled", false);
// Enable the Variable Fonts editor only in Nightly
#if defined(NIGHTLY_BUILD)
pref("devtools.inspector.fonteditor.enabled", true);
#else
pref("devtools.inspector.fonteditor.enabled", false);
#endif
// Enable the font highlight-on-hover feature
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/widgets/ColorWidget.js
@@ -0,0 +1,690 @@
+/* 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/. */
+
+/**
+ * This file is a new working copy of Spectrum.js for the purposes of refreshing the color
+ * widget. It is hidden behind a pref("devtools.inspector.colorWidget.enabled").
+ */
+
+"use strict";
+
+const EventEmitter = require("devtools/shared/event-emitter");
+const {colorUtils} = require("devtools/shared/css/color");
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/client/locales/inspector.properties");
+
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+const SAMPLE_TEXT = "Abc";
+
+/**
+ * ColorWidget creates a color picker widget in any container you give it.
+ *
+ * Simple usage example:
+ *
+ * const {ColorWidget} = require("devtools/client/shared/widgets/ColorWidget");
+ * let s = new ColorWidget(containerElement, [255, 126, 255, 1]);
+ * s.on("changed", (rgba, color) => {
+ * console.log("rgba(" + rgba[0] + ", " + rgba[1] + ", " + rgba[2] + ", " +
+ * rgba[3] + ")");
+ * });
+ * s.show();
+ * s.destroy();
+ *
+ * Note that the color picker is hidden by default and you need to call show to
+ * make it appear. This 2 stages initialization helps in cases you are creating
+ * the color picker in a parent element that hasn't been appended anywhere yet
+ * or that is hidden. Calling show() when the parent element is appended and
+ * visible will allow the color widget to correctly initialize its various parts.
+ *
+ * Fires the following events:
+ * - changed : When the user changes the current color
+ */
+function ColorWidget(parentEl, rgb) {
+ EventEmitter.decorate(this);
+
+ this.parentEl = parentEl;
+
+ this.onAlphaSliderMove = this.onAlphaSliderMove.bind(this);
+ this.onElementClick = this.onElementClick.bind(this);
+ this.onDraggerMove = this.onDraggerMove.bind(this);
+ this.onHexInputChange = this.onHexInputChange.bind(this);
+ this.onHslaInputChange = this.onHslaInputChange.bind(this);
+ this.onRgbaInputChange = this.onRgbaInputChange.bind(this);
+ this.onSelectValueChange = this.onSelectValueChange.bind(this);
+ this.onSliderMove = this.onSliderMove.bind(this);
+ this.updateContrast = this.updateContrast.bind(this);
+
+ this.initializeColorWidget();
+
+ if (rgb) {
+ this.rgb = rgb;
+ this.updateUI();
+ }
+}
+
+/**
+ * Calculates the contrast grade and title for the given contrast
+ * ratio and background color.
+ *
+ * @param {Number} contrastRatio
+ * Contrast ratio to calculate grade.
+ * @param {String} backgroundColor
+ * A string of the form `rgba(r, g, b, a)` where r, g, b and a are floats.
+ * @return {Object} An object of the form {grade, title}.
+ * |grade| is a string containing the contrast grade.
+ * |title| is a string containing the title of the colorwidget.
+ */
+ColorWidget.calculateGradeAndTitle = function(contrastRatio, backgroundColor) {
+ let grade = "";
+ let title = "";
+
+ if (contrastRatio < 3.0) {
+ grade = L10N.getStr("inspector.colorwidget.contrastRatio.failGrade");
+ title = L10N.getStr("inspector.colorwidget.contrastRatio.failInfo");
+ } else if (contrastRatio < 4.5) {
+ grade = "AA*";
+ title = L10N.getStr("inspector.colorwidget.contrastRatio.AABigInfo");
+ } else if (contrastRatio < 7.0) {
+ grade = "AAA*";
+ title = L10N.getStr("inspector.colorwidget.contrastRatio.AAABigInfo");
+ } else if (contrastRatio < 22.0) {
+ grade = "AAA";
+ title = L10N.getStr("inspector.colorwidget.contrastRatio.AAAInfo");
+ }
+ title += "\n";
+ title += L10N.getStr("inspector.colorwidget.contrastRatio.info") + " ";
+ title += backgroundColor;
+
+ return { grade, title };
+};
+
+/**
+ * Converts the contrastRatio to a string of length 4 by rounding
+ * contrastRatio and padding the required number of 0s before or
+ * after.
+ *
+ * @param {Number} contrastRatio
+ * The contrast ratio to be formatted.
+ * @return {String} the formatted ratio.
+ */
+ColorWidget.ratioToString = function(contrastRatio) {
+ let formattedRatio = (contrastRatio < 10) ? "0" : "";
+ formattedRatio += contrastRatio.toFixed(2);
+ return formattedRatio;
+};
+
+ColorWidget.hsvToRgb = function(h, s, v, a) {
+ let r, g, b;
+
+ const i = Math.floor(h * 6);
+ const f = h * 6 - i;
+ const p = v * (1 - s);
+ const q = v * (1 - f * s);
+ const t = v * (1 - (1 - f) * s);
+
+ switch (i % 6) {
+ case 0: r = v; g = t; b = p; break;
+ case 1: r = q; g = v; b = p; break;
+ case 2: r = p; g = v; b = t; break;
+ case 3: r = p; g = q; b = v; break;
+ case 4: r = t; g = p; b = v; break;
+ case 5: r = v; g = p; b = q; break;
+ }
+
+ return [r * 255, g * 255, b * 255, a];
+};
+
+ColorWidget.rgbToHsv = function(r, g, b, a) {
+ r = r / 255;
+ g = g / 255;
+ b = b / 255;
+
+ const max = Math.max(r, g, b);
+ const min = Math.min(r, g, b);
+
+ const v = max;
+ const d = max - min;
+ const s = max == 0 ? 0 : d / max;
+
+ let h;
+ if (max == min) {
+ // achromatic
+ h = 0;
+ } else {
+ switch (max) {
+ case r: h = (g - b) / d + (g < b ? 6 : 0); break;
+ case g: h = (b - r) / d + 2; break;
+ case b: h = (r - g) / d + 4; break;
+ }
+ h /= 6;
+ }
+ return [h, s, v, a];
+};
+
+ColorWidget.hslToCssString = function(h, s, l, a) {
+ return `hsla(${h}, ${s}%, ${l}%, ${a})`;
+};
+
+ColorWidget.draggable = function(element, onmove, onstart, onstop) {
+ onmove = onmove || function() {};
+ onstart = onstart || function() {};
+ onstop = onstop || function() {};
+
+ const doc = element.ownerDocument;
+ let dragging = false;
+ let offset = {};
+ let maxHeight = 0;
+ let maxWidth = 0;
+
+ function prevent(e) {
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ function move(e) {
+ if (dragging) {
+ if (e.buttons === 0) {
+ // The button is no longer pressed but we did not get a mouseup event.
+ stop();
+ return;
+ }
+ const pageX = e.pageX;
+ const pageY = e.pageY;
+
+ const dragX = Math.max(0, Math.min(pageX - offset.left, maxWidth));
+ const dragY = Math.max(0, Math.min(pageY - offset.top, maxHeight));
+
+ onmove.apply(element, [dragX, dragY]);
+ }
+ }
+
+ function start(e) {
+ const rightclick = e.which === 3;
+
+ if (!rightclick && !dragging) {
+ if (onstart.apply(element, arguments) !== false) {
+ dragging = true;
+ maxHeight = element.offsetHeight;
+ maxWidth = element.offsetWidth;
+
+ offset = element.getBoundingClientRect();
+
+ move(e);
+
+ doc.addEventListener("selectstart", prevent);
+ doc.addEventListener("dragstart", prevent);
+ doc.addEventListener("mousemove", move);
+ doc.addEventListener("mouseup", stop);
+
+ prevent(e);
+ }
+ }
+ }
+
+ function stop() {
+ if (dragging) {
+ doc.removeEventListener("selectstart", prevent);
+ doc.removeEventListener("dragstart", prevent);
+ doc.removeEventListener("mousemove", move);
+ doc.removeEventListener("mouseup", stop);
+ onstop.apply(element, arguments);
+ }
+ dragging = false;
+ }
+
+ element.addEventListener("mousedown", start);
+};
+
+ColorWidget.prototype = {
+ set rgb(color) {
+ this.hsv = ColorWidget.rgbToHsv(color[0], color[1], color[2], color[3]);
+
+ const { h, s, l } = new colorUtils.CssColor(this.rgbCssString)._getHSLATuple();
+ this.hsl = [h, s, l, color[3]];
+ },
+
+ get rgb() {
+ const rgb = ColorWidget.hsvToRgb(this.hsv[0], this.hsv[1], this.hsv[2],
+ this.hsv[3]);
+ return [Math.round(rgb[0]), Math.round(rgb[1]), Math.round(rgb[2]),
+ Math.round(rgb[3] * 100) / 100];
+ },
+
+ get rgbNoSatVal() {
+ const rgb = ColorWidget.hsvToRgb(this.hsv[0], 1, 1);
+ return [Math.round(rgb[0]), Math.round(rgb[1]), Math.round(rgb[2]), rgb[3]];
+ },
+
+ get rgbCssString() {
+ const rgb = this.rgb;
+ return "rgba(" + rgb[0] + ", " + rgb[1] + ", " + rgb[2] + ", " + rgb[3] + ")";
+ },
+
+ initializeColorWidget: function() {
+ const colorNameLabel = L10N.getStr("inspector.colorwidget.colorNameLabel");
+ this.parentEl.innerHTML = "";
+ this.element = this.parentEl.ownerDocument.createElementNS(XHTML_NS, "div");
+
+ this.element.className = "colorwidget-container";
+ // eslint-disable-next-line no-unsanitized/property
+ this.element.innerHTML = `
+ <div class="colorwidget-top">
+ <div class="colorwidget-fill"></div>
+ <div class="colorwidget-top-inner">
+ <div class="colorwidget-color colorwidget-box">
+ <div class="colorwidget-sat">
+ <div class="colorwidget-val">
+ <div class="colorwidget-dragger"></div>
+ </div>
+ </div>
+ </div>
+ <div class="colorwidget-hue colorwidget-box">
+ <div class="colorwidget-slider colorwidget-slider-control"></div>
+ </div>
+ </div>
+ </div>
+ <div class="colorwidget-alpha colorwidget-checker colorwidget-box">
+ <div class="colorwidget-alpha-inner">
+ <div class="colorwidget-alpha-handle colorwidget-slider-control"></div>
+ </div>
+ </div>
+ <div class="colorwidget-value">
+ <select class="colorwidget-select">
+ <option value="hex">Hex</option>
+ <option value="rgba">RGBA</option>
+ <option value="hsla">HSLA</option>
+ </select>
+ <div class="colorwidget-hex">
+ <input class="colorwidget-hex-input"/>
+ </div>
+ <div class="colorwidget-rgba colorwidget-hidden">
+ <input class="colorwidget-rgba-r" data-id="r" />
+ <input class="colorwidget-rgba-g" data-id="g" />
+ <input class="colorwidget-rgba-b" data-id="b" />
+ <input class="colorwidget-rgba-a" data-id="a" />
+ </div>
+ <div class="colorwidget-hsla colorwidget-hidden">
+ <input class="colorwidget-hsla-h" data-id="h" />
+ <input class="colorwidget-hsla-s" data-id="s" />
+ <input class="colorwidget-hsla-l" data-id="l" />
+ <input class="colorwidget-hsla-a" data-id="a" />
+ </div>
+ </div>
+ <div class="colorwidget-colorname">
+ <label class="colorwidget-colorname-label">${colorNameLabel}</label>
+ <span class="colorwidget-colorname-value"></span>
+ </div>
+ <div class="colorwidget-contrast">
+ <div class="colorwidget-contrast-info"></div>
+ <div class="colorwidget-contrast-inner">
+ <span class="colorwidget-colorswatch"></span>
+ <span class="colorwidget-contrast-ratio"></span>
+ <span class="colorwidget-contrast-grade"></span>
+ <button class="colorwidget-contrast-help devtools-button"></button>
+ </div>
+ </div>
+ `;
+
+ this.element.addEventListener("click", this.onElementClick);
+
+ this.parentEl.appendChild(this.element);
+
+ this.closestBackgroundColor = "rgba(255, 255, 255, 1)";
+
+ this.colorName = this.element.querySelector(".colorwidget-colorname-value");
+
+ this.contrast = this.element.querySelector(".colorwidget-contrast");
+ this.contrastInfo = this.element.querySelector(".colorwidget-contrast-info");
+ this.contrastInfo.textContent = L10N.getStr(
+ "inspector.colorwidget.contrastRatio.header"
+ );
+
+ this.contrastInner = this.element.querySelector(".colorwidget-contrast-inner");
+ this.contrastSwatch = this.contrastInner.querySelector(".colorwidget-colorswatch");
+
+ this.contrastSwatch.textContent = SAMPLE_TEXT;
+
+ this.contrastRatio = this.contrastInner.querySelector(".colorwidget-contrast-ratio");
+ this.contrastGrade = this.contrastInner.querySelector(".colorwidget-contrast-grade");
+ this.contrastHelp = this.contrastInner.querySelector(".colorwidget-contrast-help");
+
+ this.slider = this.element.querySelector(".colorwidget-hue");
+ this.slideHelper = this.element.querySelector(".colorwidget-slider");
+ ColorWidget.draggable(this.slider, this.onSliderMove);
+
+ this.dragger = this.element.querySelector(".colorwidget-color");
+ this.dragHelper = this.element.querySelector(".colorwidget-dragger");
+ ColorWidget.draggable(this.dragger, this.onDraggerMove);
+
+ this.alphaSlider = this.element.querySelector(".colorwidget-alpha");
+ this.alphaSliderInner = this.element.querySelector(".colorwidget-alpha-inner");
+ this.alphaSliderHelper = this.element.querySelector(".colorwidget-alpha-handle");
+ ColorWidget.draggable(this.alphaSliderInner, this.onAlphaSliderMove);
+
+ this.colorSelect = this.element.querySelector(".colorwidget-select");
+ this.colorSelect.addEventListener("change", this.onSelectValueChange);
+
+ this.hexValue = this.element.querySelector(".colorwidget-hex");
+ this.hexValueInput = this.element.querySelector(".colorwidget-hex-input");
+ this.hexValueInput.addEventListener("input", this.onHexInputChange);
+
+ this.rgbaValue = this.element.querySelector(".colorwidget-rgba");
+ this.rgbaValueInputs = {
+ r: this.element.querySelector(".colorwidget-rgba-r"),
+ g: this.element.querySelector(".colorwidget-rgba-g"),
+ b: this.element.querySelector(".colorwidget-rgba-b"),
+ a: this.element.querySelector(".colorwidget-rgba-a"),
+ };
+ this.rgbaValue.addEventListener("input", this.onRgbaInputChange);
+
+ this.hslaValue = this.element.querySelector(".colorwidget-hsla");
+ this.hslaValueInputs = {
+ h: this.element.querySelector(".colorwidget-hsla-h"),
+ s: this.element.querySelector(".colorwidget-hsla-s"),
+ l: this.element.querySelector(".colorwidget-hsla-l"),
+ a: this.element.querySelector(".colorwidget-hsla-a"),
+ };
+ this.hslaValue.addEventListener("input", this.onHslaInputChange);
+ },
+
+ async show() {
+ this.initializeColorWidget();
+ this.element.classList.add("colorwidget-show");
+
+ this.slideHeight = this.slider.offsetHeight;
+ this.dragWidth = this.dragger.offsetWidth;
+ this.dragHeight = this.dragger.offsetHeight;
+ this.dragHelperHeight = this.dragHelper.offsetHeight;
+ this.slideHelperHeight = this.slideHelper.offsetHeight;
+ this.alphaSliderWidth = this.alphaSliderInner.offsetWidth;
+ this.alphaSliderHelperWidth = this.alphaSliderHelper.offsetWidth;
+
+ if (this.inspector && this.inspector.selection.nodeFront && this.contrastEnabled) {
+ const node = this.inspector.selection.nodeFront;
+ this.closestBackgroundColor = await node.getClosestBackgroundColor();
+ }
+ this.updateContrast();
+
+ this.updateUI();
+ },
+
+ onElementClick: function(e) {
+ e.stopPropagation();
+ },
+
+ onSliderMove: function(dragX, dragY) {
+ this.hsv[0] = (dragY / this.slideHeight);
+ this.hsl[0] = (dragY / this.slideHeight) * 360;
+ this.updateUI();
+ this.onChange();
+ },
+
+ onDraggerMove: function(dragX, dragY) {
+ this.hsv[1] = dragX / this.dragWidth;
+ this.hsv[2] = (this.dragHeight - dragY) / this.dragHeight;
+
+ this.hsl[2] = ((2 - this.hsv[1]) * this.hsv[2] / 2);
+ if (this.hsl[2] && this.hsl[2] < 1) {
+ this.hsl[1] = this.hsv[1] * this.hsv[2] /
+ (this.hsl[2] < 0.5 ? this.hsl[2] * 2 : 2 - this.hsl[2] * 2);
+ this.hsl[1] = this.hsl[1] * 100;
+ }
+ this.hsl[2] = this.hsl[2] * 100;
+
+ this.updateUI();
+ this.onChange();
+ },
+
+ onAlphaSliderMove: function(dragX, dragY) {
+ this.hsv[3] = dragX / this.alphaSliderWidth;
+ this.hsl[3] = dragX / this.alphaSliderWidth;
+ this.updateUI();
+ this.onChange();
+ },
+
+ onSelectValueChange: function(event) {
+ const selection = event.target.value;
+ this.colorSelect.classList.remove("colorwidget-select-spacing");
+ this.hexValue.classList.add("colorwidget-hidden");
+ this.rgbaValue.classList.add("colorwidget-hidden");
+ this.hslaValue.classList.add("colorwidget-hidden");
+
+ switch (selection) {
+ case "hex":
+ this.hexValue.classList.remove("colorwidget-hidden");
+ break;
+ case "rgba":
+ this.colorSelect.classList.add("colorwidget-select-spacing");
+ this.rgbaValue.classList.remove("colorwidget-hidden");
+ break;
+ case "hsla":
+ this.colorSelect.classList.add("colorwidget-select-spacing");
+ this.hslaValue.classList.remove("colorwidget-hidden");
+ break;
+ }
+ },
+
+ onHexInputChange: function(event) {
+ const hex = event.target.value;
+ const color = new colorUtils.CssColor(hex, true);
+ if (!color.rgba) {
+ return;
+ }
+
+ const { r, g, b, a } = color.getRGBATuple();
+ this.rgb = [r, g, b, a];
+ this.updateUI();
+ this.onChange();
+ },
+
+ onRgbaInputChange: function(event) {
+ const field = event.target.dataset.id;
+ const value = event.target.value.toString();
+ if (!value || isNaN(value) || value.endsWith(".")) {
+ return;
+ }
+
+ const rgb = this.rgb;
+
+ switch (field) {
+ case "r":
+ rgb[0] = value;
+ break;
+ case "g":
+ rgb[1] = value;
+ break;
+ case "b":
+ rgb[2] = value;
+ break;
+ case "a":
+ rgb[3] = Math.min(value, 1);
+ break;
+ }
+
+ this.rgb = rgb;
+
+ this.updateUI();
+ this.onChange();
+ },
+
+ onHslaInputChange: function(event) {
+ const field = event.target.dataset.id;
+ let value = event.target.value.toString();
+ if ((field === "s" || field === "l") && !value.endsWith("%")) {
+ return;
+ }
+
+ if (value.endsWith("%")) {
+ value = value.substring(0, value.length - 1);
+ }
+
+ if (!value || isNaN(value) || value.endsWith(".")) {
+ return;
+ }
+
+ const hsl = this.hsl;
+
+ switch (field) {
+ case "h":
+ hsl[0] = value;
+ break;
+ case "s":
+ hsl[1] = value;
+ break;
+ case "l":
+ hsl[2] = value;
+ break;
+ case "a":
+ hsl[3] = Math.min(value, 1);
+ break;
+ }
+
+ const cssString = ColorWidget.hslToCssString(hsl[0], hsl[1], hsl[2], hsl[3]);
+ const { r, g, b, a } = new colorUtils.CssColor(cssString).getRGBATuple();
+
+ this.rgb = [r, g, b, a];
+
+ this.hsl = hsl;
+
+ this.updateUI();
+ this.onChange();
+ },
+
+ onChange: function() {
+ this.updateContrast();
+ this.emit("changed", this.rgb, this.rgbCssString);
+ },
+
+ updateContrast: function() {
+ if (!this.contrastEnabled) {
+ this.contrast.style.display = "none";
+ return;
+ }
+
+ this.contrast.style.display = "initial";
+
+ if (!colorUtils.isValidCSSColor(this.closestBackgroundColor)) {
+ this.contrastRatio.textContent = L10N.getStr(
+ "inspector.colorwidget.contrastRatio.invalidColor"
+ );
+
+ this.contrastGrade.textContent = "";
+ this.contrastHelp.removeAttribute("title");
+ return;
+ }
+ if (!this.rgbaColor) {
+ this.rgbaColor = new colorUtils.CssColor(this.closestBackgroundColor);
+ }
+ this.rgbaColor.newColor(this.closestBackgroundColor);
+ const rgba = this.rgbaColor.getRGBATuple();
+ const backgroundColor = [rgba.r, rgba.g, rgba.b, rgba.a];
+
+ const textColor = this.rgb;
+
+ const ratio = colorUtils.calculateContrastRatio(backgroundColor, textColor);
+
+ const contrastDetails = ColorWidget.calculateGradeAndTitle(ratio,
+ this.rgbaColor.toString());
+
+ this.contrastRatio.textContent = ColorWidget.ratioToString(ratio);
+ this.contrastGrade.textContent = contrastDetails.grade;
+
+ this.contrastHelp.setAttribute("title", contrastDetails.title);
+
+ this.contrastSwatch.style.backgroundColor = this.rgbaColor.toString();
+ this.contrastSwatch.style.color = this.rgbCssString;
+ },
+
+ updateHelperLocations: function() {
+ // If the UI hasn't been shown yet then none of the dimensions will be
+ // correct
+ if (!this.element.classList.contains("colorwidget-show")) {
+ return;
+ }
+
+ const h = this.hsv[0];
+ const s = this.hsv[1];
+ const v = this.hsv[2];
+
+ // Placing the color dragger
+ let dragX = s * this.dragWidth;
+ let dragY = this.dragHeight - (v * this.dragHeight);
+ const helperDim = this.dragHelperHeight / 2;
+
+ dragX = Math.max(
+ -helperDim,
+ Math.min(this.dragWidth - helperDim, dragX - helperDim)
+ );
+ dragY = Math.max(
+ -helperDim,
+ Math.min(this.dragHeight - helperDim, dragY - helperDim)
+ );
+
+ this.dragHelper.style.top = dragY + "px";
+ this.dragHelper.style.left = dragX + "px";
+
+ // Placing the hue slider
+ const slideY = (h * this.slideHeight) - this.slideHelperHeight / 2;
+ this.slideHelper.style.top = slideY + "px";
+
+ // Placing the alpha slider
+ const alphaSliderX = (this.hsv[3] * this.alphaSliderWidth) -
+ (this.alphaSliderHelperWidth / 2);
+ this.alphaSliderHelper.style.left = alphaSliderX + "px";
+
+ const color = new colorUtils.CssColor(this.rgbCssString);
+
+ // Updating the hex field
+ this.hexValueInput.value = color.hex;
+
+ // Updating the RGBA fields
+ const rgb = this.rgb;
+ this.rgbaValueInputs.r.value = rgb[0];
+ this.rgbaValueInputs.g.value = rgb[1];
+ this.rgbaValueInputs.b.value = rgb[2];
+ this.rgbaValueInputs.a.value = parseFloat(rgb[3].toFixed(1));
+
+ // Updating the HSLA fields
+ this.hslaValueInputs.h.value = this.hsl[0];
+ this.hslaValueInputs.s.value = this.hsl[1] + "%";
+ this.hslaValueInputs.l.value = this.hsl[2] + "%";
+ this.hslaValueInputs.a.value = parseFloat(this.hsl[3].toFixed(1));
+ },
+
+ updateUI: function() {
+ this.updateHelperLocations();
+
+ const rgb = this.rgb;
+ const rgbNoSatVal = this.rgbNoSatVal;
+
+ const flatColor = "rgb(" + rgbNoSatVal[0] + ", " + rgbNoSatVal[1] + ", " +
+ rgbNoSatVal[2] + ")";
+
+ this.dragger.style.backgroundColor = flatColor;
+
+ const rgbNoAlpha = "rgb(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + ")";
+ const rgbAlpha0 = "rgba(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + ", 0)";
+ const alphaGradient = "linear-gradient(to right, " + rgbAlpha0 + ", " +
+ rgbNoAlpha + ")";
+ this.alphaSliderInner.style.background = alphaGradient;
+
+ const colorName = colorUtils.rgbToColorName(rgb[0], rgb[1], rgb[2]);
+ this.colorName.textContent = colorName || "---";
+ },
+
+ destroy: function() {
+ this.element.removeEventListener("click", this.onElementClick);
+
+ this.parentEl.removeChild(this.element);
+
+ this.slider = null;
+ this.dragger = null;
+ this.alphaSlider = this.alphaSliderInner = this.alphaSliderHelper = null;
+ this.parentEl = null;
+ this.element = null;
+ this.colorName = null;
+ }
+};
+
+module.exports = ColorWidget;
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/widgets/color-widget.css
@@ -0,0 +1,254 @@
+/* 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/. */
+
+#eyedropper-button {
+ margin-inline-start: 5px;
+ display: block;
+}
+
+#eyedropper-button::before {
+ background-image: url(chrome://devtools/skin/images/command-eyedropper.svg);
+}
+
+/* Mix-in classes */
+
+.colorwidget-checker {
+ background-color: #eee;
+ background-image: linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc),
+ linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc);
+ background-size: 12px 12px;
+ background-position: 0 0, 6px 6px;
+}
+
+.colorwidget-slider-control {
+ cursor: pointer;
+ box-shadow: 0 0 2px rgba(0,0,0,.6);
+ background: #fff;
+ border-radius: 10px;
+ opacity: .8;
+}
+
+.colorwidget-box {
+ border: 1px solid rgba(0,0,0,0.2);
+ border-radius: 2px;
+ background-clip: content-box;
+}
+
+.colorwidget-colorswatch {
+ background-color: transparent;
+ color: transparent;
+ border: 1px solid transparent;
+}
+
+/* Elements */
+
+#colorwidget-tooltip {
+ padding: 4px;
+}
+
+.colorwidget-container {
+ position: relative;
+ display: none;
+ top: 0;
+ left: 0;
+ border-radius: 0;
+ width: 200px;
+ padding: 5px;
+}
+
+.colorwidget-show {
+ display: inline-block;
+}
+
+/* Keep aspect ratio:
+http://www.briangrinstead.com/blog/keep-aspect-ratio-with-html-and-css */
+.colorwidget-top {
+ position: relative;
+ width: 100%;
+ display: inline-block;
+}
+
+.colorwidget-top-inner {
+ position: absolute;
+ top:0;
+ left:0;
+ bottom:0;
+ right:0;
+}
+
+.colorwidget-contrast {
+ color: var(--theme-content-color1);
+ padding-top: 4px;
+}
+
+.colorwidget-colorswatch, .colorwidget-contrast-ratio, .colorwidget-contrast-grade, .colorwidget-contrast-help {
+ display: inline-block;
+}
+
+.colorwidget-colorswatch {
+ width: 28%;
+}
+
+.colorwidget-contrast-ratio {
+ font-family: Courier New, Courier, monospace;
+ padding-left: 8px;
+ width: 26%;
+}
+
+.colorwidget-contrast-grade {
+ font-family: Courier New, Courier, monospace;
+ width: 18%;
+}
+
+.colorwidget-contrast-help {
+ margin-inline-start: 5px;
+}
+
+.colorwidget-contrast-help::before {
+ background-image: url(chrome://devtools/skin/images/help.svg);
+}
+
+.colorwidget-colorswatch {
+ text-align: center;
+ color: transparent;
+}
+
+.colorwidget-color {
+ position: absolute;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ right: 20%;
+}
+
+.colorwidget-hue {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 83%;
+}
+
+.colorwidget-fill {
+ /* Same as colorwidget-color width */
+ margin-top: 85%;
+}
+
+.colorwidget-sat, .colorwidget-val {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+}
+
+.colorwidget-dragger, .colorwidget-slider {
+ -moz-user-select: none;
+}
+
+.colorwidget-alpha {
+ position: relative;
+ height: 8px;
+ margin-top: 3px;
+}
+
+.colorwidget-alpha-inner {
+ height: 100%;
+}
+
+.colorwidget-alpha-handle {
+ position: absolute;
+ top: -3px;
+ bottom: -3px;
+ width: 5px;
+ left: 50%;
+}
+
+.colorwidget-sat {
+ background-image: linear-gradient(to right, #FFF, rgba(204, 154, 129, 0));
+}
+
+.colorwidget-val {
+ background-image: linear-gradient(to top, #000000, rgba(204, 154, 129, 0));
+}
+
+.colorwidget-hue {
+ background: linear-gradient(to bottom, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%);
+}
+
+.colorwidget-dragger {
+ position: absolute;
+ top: 0px;
+ left: 0px;
+ cursor: pointer;
+ border-radius: 50%;
+ height: 8px;
+ width: 8px;
+ border: 1px solid white;
+ box-shadow: 0 0 2px rgba(0,0,0,.6);
+}
+
+.colorwidget-slider {
+ position: absolute;
+ top: 0;
+ height: 5px;
+ left: -3px;
+ right: -3px;
+}
+
+/**
+ * Color Widget Editor
+ */
+
+.colorwidget-value {
+ position: relative;
+ margin-top: 8px;
+}
+
+/**
+ * Color Widget Select
+ */
+
+.colorwidget-select {
+ width: 100%;
+}
+
+.colorwidget-select-spacing {
+ letter-spacing: 40px;
+}
+
+.colorwidget-select-spacing option {
+ letter-spacing: initial;
+}
+
+/**
+ * Color Widget Inputs
+ */
+
+.colorwidget-hidden {
+ display: none;
+}
+
+.colorwidget-hex,
+.colorwidget-rgba,
+.colorwidget-hsla {
+ width: 200px;
+ font-size: 0;
+}
+
+.colorwidget-hex-input {
+ width: 192px;
+}
+
+.colorwidget-rgba-r,
+.colorwidget-rgba-g,
+.colorwidget-rgba-b,
+.colorwidget-rgba-a,
+.colorwidget-hsla-h,
+.colorwidget-hsla-s,
+.colorwidget-hsla-l,
+.colorwidget-hsla-a {
+ width: 42px;
+ margin: 0;
+}
\ No newline at end of file
--- a/devtools/client/shared/widgets/moz.build
+++ b/devtools/client/shared/widgets/moz.build
@@ -8,16 +8,17 @@ DIRS += [
'tooltip',
]
DevToolsModules(
'AbstractTreeItem.jsm',
'BarGraphWidget.js',
'BreadcrumbsWidget.jsm',
'Chart.js',
+ 'ColorWidget.js',
'CubicBezierPresets.js',
'CubicBezierWidget.js',
'FastListWidget.js',
'FilterWidget.js',
'FlameGraph.js',
'Graphs.js',
'GraphsWorker.js',
'LineGraphWidget.js',
--- a/devtools/client/shared/widgets/tooltip/SwatchColorPickerTooltip.js
+++ b/devtools/client/shared/widgets/tooltip/SwatchColorPickerTooltip.js
@@ -1,20 +1,26 @@
/* 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 Services = require("Services");
const {colorUtils} = require("devtools/shared/css/color");
const {Spectrum} = require("devtools/client/shared/widgets/Spectrum");
const SwatchBasedEditorTooltip = require("devtools/client/shared/widgets/tooltip/SwatchBasedEditorTooltip");
const {LocalizationHelper} = require("devtools/shared/l10n");
const L10N = new LocalizationHelper("devtools/client/locales/inspector.properties");
+loader.lazyRequireGetter(this, "ColorWidget", "devtools/client/shared/widgets/ColorWidget");
+
+const COLOR_WIDGET_ENABLED_PREF = "devtools.inspector.colorWidget.enabled";
+const NEW_COLOR_WIDGET = Services.prefs.getBoolPref(COLOR_WIDGET_ENABLED_PREF);
+
const TELEMETRY_PICKER_EYEDROPPER_OPEN_COUNT = "DEVTOOLS_PICKER_EYEDROPPER_OPENED_COUNT";
const XHTML_NS = "http://www.w3.org/1999/xhtml";
/**
* The swatch color picker tooltip class is a specific class meant to be used
* along with output-parser's generated color swatches.
* It extends the parent SwatchBasedEditorTooltip class.
@@ -51,21 +57,29 @@ class SwatchColorPickerTooltip extends S
setColorPickerContent(color) {
const { doc } = this.tooltip;
const container = doc.createElementNS(XHTML_NS, "div");
container.id = "spectrum-tooltip";
const node = doc.createElementNS(XHTML_NS, "div");
- node.id = "spectrum";
- container.appendChild(node);
+ let widget;
- const widget = new Spectrum(node, color);
- this.tooltip.setContent(container, { width: 218, height: 224 });
+ if (NEW_COLOR_WIDGET) {
+ node.id = "colorwidget";
+ container.appendChild(node);
+ widget = new ColorWidget(node, color);
+ this.tooltip.setContent(container, { width: 218, height: 320 });
+ } else {
+ node.id = "spectrum";
+ container.appendChild(node);
+ widget = new Spectrum(node, color);
+ this.tooltip.setContent(container, { width: 218, height: 224 });
+ }
widget.inspector = this.inspector;
const eyedropper = doc.createElementNS(XHTML_NS, "button");
eyedropper.id = "eyedropper-button";
eyedropper.className = "devtools-button";
/* pointerEvents for eyedropper has to be set auto to display tooltip when
* eyedropper is disabled in non-HTML documents.
--- a/devtools/client/themes/tooltips.css
+++ b/devtools/client/themes/tooltips.css
@@ -1,14 +1,15 @@
/* vim:set ts=2 sw=2 sts=2 et: */
/* 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/. */
/* Import stylesheets for specific tooltip widgets */
+@import url(chrome://devtools/content/shared/widgets/color-widget.css);
@import url(chrome://devtools/content/shared/widgets/cubic-bezier.css);
@import url(chrome://devtools/content/shared/widgets/filter-widget.css);
@import url(chrome://devtools/content/shared/widgets/spectrum.css);
/* Tooltip specific theme variables */
.theme-dark {
--bezier-diagonal-color: #eee;