summaryrefslogtreecommitdiffstats
path: root/devtools/client/shared/widgets/tooltip/SwatchColorPickerTooltip.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/shared/widgets/tooltip/SwatchColorPickerTooltip.js')
-rw-r--r--devtools/client/shared/widgets/tooltip/SwatchColorPickerTooltip.js363
1 files changed, 363 insertions, 0 deletions
diff --git a/devtools/client/shared/widgets/tooltip/SwatchColorPickerTooltip.js b/devtools/client/shared/widgets/tooltip/SwatchColorPickerTooltip.js
new file mode 100644
index 0000000000..6cfceccc0b
--- /dev/null
+++ b/devtools/client/shared/widgets/tooltip/SwatchColorPickerTooltip.js
@@ -0,0 +1,363 @@
+/* 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 { colorUtils } = require("resource://devtools/shared/css/color.js");
+const Spectrum = require("resource://devtools/client/shared/widgets/Spectrum.js");
+const SwatchBasedEditorTooltip = require("resource://devtools/client/shared/widgets/tooltip/SwatchBasedEditorTooltip.js");
+const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
+const L10N = new LocalizationHelper(
+ "devtools/client/locales/inspector.properties"
+);
+const { openDocLink } = require("resource://devtools/client/shared/link.js");
+const {
+ A11Y_CONTRAST_LEARN_MORE_LINK,
+} = require("resource://devtools/client/accessibility/constants.js");
+loader.lazyRequireGetter(
+ this,
+ "throttle",
+ "resource://devtools/shared/throttle.js",
+ true
+);
+
+loader.lazyRequireGetter(
+ this,
+ ["getFocusableElements", "wrapMoveFocus"],
+ "resource://devtools/client/shared/focus.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "PICKER_TYPES",
+ "resource://devtools/shared/picker-constants.js"
+);
+
+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.
+ * It just wraps a standard Tooltip and sets its content with an instance of a
+ * color picker.
+ *
+ * The activeSwatch element expected by the tooltip must follow some guidelines
+ * to be compatible with this feature:
+ * - the background-color of the activeSwatch should be set to the current
+ * color, it will be updated when the color is changed via the color-picker.
+ * - the `data-color` attribute should be set either on the activeSwatch or on
+ * a parent node, and should also contain the current color.
+ * - finally if the color value should be displayed next to the swatch as text,
+ * the activeSwatch should have a nextSibling. Note that this sibling may
+ * contain more than just text initially, but it will be updated after a color
+ * change and will only contain the text.
+ *
+ * An example of valid markup (with data-color on a parent and a nextSibling):
+ *
+ * <span data-color="#FFF"> <!-- activeSwatch.closest("[data-color]") -->
+ * <span
+ * style="background-color: rgb(255, 255, 255);"
+ * ></span> <!-- activeSwatch -->
+ * <span>#FFF</span> <!-- activeSwatch.nextSibling -->
+ * </span>
+ *
+ * Another example with everything on the activeSwatch itself:
+ *
+ * <span> <!-- container, to illustrate that the swatch has no sibling here. -->
+ * <span
+ * data-color="#FFF"
+ * style="background-color: rgb(255, 255, 255);"
+ * ></span> <!-- activeSwatch & activeSwatch.closest("[data-color]") -->
+ * </span>
+ *
+ * @param {Document} document
+ * The document to attach the SwatchColorPickerTooltip. This is either the toolbox
+ * document if the tooltip is a popup tooltip or the panel's document if it is an
+ * inline editor.
+ * @param {InspectorPanel} inspector
+ * The inspector panel, needed for the eyedropper.
+ */
+
+class SwatchColorPickerTooltip extends SwatchBasedEditorTooltip {
+ constructor(document, inspector) {
+ super(document);
+ this.inspector = inspector;
+
+ // Creating a spectrum instance. this.spectrum will always be a promise that
+ // resolves to the spectrum instance
+ this.spectrum = this.setColorPickerContent([0, 0, 0, 1]);
+ this._onSpectrumColorChange = this._onSpectrumColorChange.bind(this);
+ this._openEyeDropper = this._openEyeDropper.bind(this);
+ this._openDocLink = this._openDocLink.bind(this);
+ this._onTooltipKeydown = this._onTooltipKeydown.bind(this);
+
+ // Selecting color by hovering on the spectrum widget could create a lot
+ // of requests. Throttle by 50ms to avoid this. See Bug 1665547.
+ this._selectColor = throttle(this._selectColor.bind(this), 50);
+
+ this.tooltip.container.addEventListener("keydown", this._onTooltipKeydown);
+ }
+
+ /**
+ * Fill the tooltip with a new instance of the spectrum color picker widget
+ * initialized with the given color, and return the instance of spectrum
+ */
+
+ setColorPickerContent(color) {
+ const { doc } = this.tooltip;
+ this.tooltip.panel.innerHTML = "";
+
+ const container = doc.createElementNS(XHTML_NS, "div");
+ container.id = "spectrum-tooltip";
+
+ const node = doc.createElementNS(XHTML_NS, "div");
+ node.id = "spectrum";
+ container.appendChild(node);
+
+ const widget = new Spectrum(node, color);
+ this.tooltip.panel.appendChild(container);
+ this.tooltip.setContentSize({ width: 215 });
+
+ widget.inspector = this.inspector;
+
+ // Wait for the tooltip to be shown before calling widget.show
+ // as it expect to be visible in order to compute DOM element sizes.
+ this.tooltip.once("shown", () => {
+ widget.show();
+ });
+
+ return widget;
+ }
+
+ /**
+ * Overriding the SwatchBasedEditorTooltip.show function to set spectrum's
+ * color.
+ */
+ async show() {
+ // set contrast enabled for the spectrum
+ const name = this.activeSwatch.dataset.propertyName;
+ const colorFunction = this.activeSwatch.dataset.colorFunction;
+
+ // Only enable contrast if the type of property is color
+ // and its value isn't inside a color-modifying function (e.g. color-mix()).
+ this.spectrum.contrastEnabled =
+ name === "color" && colorFunction !== "color-mix";
+ if (this.spectrum.contrastEnabled) {
+ const { nodeFront } = this.inspector.selection;
+ const { pageStyle } = nodeFront.inspectorFront;
+ this.spectrum.textProps = await pageStyle.getComputed(nodeFront, {
+ filterProperties: ["font-size", "font-weight", "opacity"],
+ });
+ this.spectrum.backgroundColorData = await nodeFront.getBackgroundColor();
+ }
+
+ // Then set spectrum's color and listen to color changes to preview them
+ if (this.activeSwatch) {
+ this._originalColor = this._getSwatchColorContainer().dataset.color;
+ const color = this.activeSwatch.style.backgroundColor;
+
+ this.spectrum.off("changed", this._onSpectrumColorChange);
+ this.spectrum.rgb = this._colorToRgba(color);
+ this.spectrum.on("changed", this._onSpectrumColorChange);
+ this.spectrum.updateUI();
+ }
+
+ // Call then parent class' show function
+ await super.show();
+
+ const eyeButton =
+ this.tooltip.container.querySelector("#eyedropper-button");
+ const canShowEyeDropper = await this.inspector.supportsEyeDropper();
+ if (canShowEyeDropper) {
+ eyeButton.disabled = false;
+ eyeButton.removeAttribute("title");
+ eyeButton.addEventListener("click", this._openEyeDropper);
+ } else {
+ eyeButton.disabled = true;
+ eyeButton.title = L10N.getStr("eyedropper.disabled.title");
+ }
+
+ const learnMoreButton =
+ this.tooltip.container.querySelector("#learn-more-button");
+ if (learnMoreButton) {
+ learnMoreButton.addEventListener("click", this._openDocLink);
+ learnMoreButton.addEventListener("keydown", e => e.stopPropagation());
+ }
+
+ // Add focus to the first focusable element in the tooltip and attach keydown
+ // event listener to tooltip
+ this.focusableElements[0].focus();
+ this.tooltip.container.addEventListener(
+ "keydown",
+ this._onTooltipKeydown,
+ true
+ );
+
+ this.emit("ready");
+ }
+
+ _onTooltipKeydown(event) {
+ const { target, key, shiftKey } = event;
+
+ if (key !== "Tab") {
+ return;
+ }
+
+ const focusMoved = !!wrapMoveFocus(
+ this.focusableElements,
+ target,
+ shiftKey
+ );
+ if (focusMoved) {
+ // Focus was moved to the begining/end of the tooltip, so we need to prevent the
+ // default focus change that would happen here.
+ event.preventDefault();
+ }
+
+ event.stopPropagation();
+ }
+
+ _getSwatchColorContainer() {
+ // Depending on the UI, the data-color attribute might be set on the
+ // swatch itself, or a parent node.
+ // This data attribute is also used for the "Copy color" feature, so it
+ // can be useful to set it on a container rather than on the swatch.
+ return this.activeSwatch.closest("[data-color]");
+ }
+
+ _onSpectrumColorChange(rgba, cssColor) {
+ this._selectColor(cssColor);
+ }
+
+ _selectColor(color) {
+ if (this.activeSwatch) {
+ this.activeSwatch.style.backgroundColor = color;
+
+ color = this._toDefaultType(color);
+
+ this._getSwatchColorContainer().dataset.color = color;
+ if (this.activeSwatch.nextSibling) {
+ this.activeSwatch.nextSibling.textContent = color;
+ }
+ this.preview(color);
+
+ if (this.eyedropperOpen) {
+ this.commit();
+ }
+ }
+ }
+
+ /**
+ * Override the implementation from SwatchBasedEditorTooltip.
+ */
+ onTooltipHidden() {
+ // If the tooltip is hidden while the eyedropper is being used, we should not commit
+ // the changes.
+ if (this.eyedropperOpen) {
+ return;
+ }
+
+ super.onTooltipHidden();
+ this.tooltip.container.removeEventListener(
+ "keydown",
+ this._onTooltipKeydown
+ );
+ }
+
+ _openEyeDropper() {
+ const { inspectorFront, toolbox, telemetry } = this.inspector;
+
+ telemetry
+ .getHistogramById(TELEMETRY_PICKER_EYEDROPPER_OPEN_COUNT)
+ .add(true);
+
+ // cancelling picker(if it is already selected) on opening eye-dropper
+ toolbox.nodePicker.stop({ canceled: true });
+
+ // disable simulating touch events if RDM is active
+ toolbox.tellRDMAboutPickerState(true, PICKER_TYPES.EYEDROPPER);
+
+ // pickColorFromPage will focus the content document. If the devtools are in a
+ // separate window, the colorpicker tooltip will be closed before pickColorFromPage
+ // resolves. Flip the flag early to avoid issues with onTooltipHidden().
+ this.eyedropperOpen = true;
+
+ inspectorFront.pickColorFromPage({ copyOnSelect: false }).then(() => {
+ // close the colorpicker tooltip so that only the eyedropper is open.
+ this.hide();
+
+ this.tooltip.emit("eyedropper-opened");
+ }, console.error);
+
+ inspectorFront.once("color-picked", color => {
+ toolbox.win.focus();
+ this._selectColor(color);
+ this._onEyeDropperDone();
+ });
+
+ inspectorFront.once("color-pick-canceled", () => {
+ this._onEyeDropperDone();
+ });
+ }
+
+ _openDocLink() {
+ openDocLink(A11Y_CONTRAST_LEARN_MORE_LINK);
+ this.hide();
+ }
+
+ _onEyeDropperDone() {
+ // enable simulating touch events if RDM is active
+ this.inspector.toolbox.tellRDMAboutPickerState(
+ false,
+ PICKER_TYPES.EYEDROPPER
+ );
+
+ this.eyedropperOpen = false;
+ this.activeSwatch = null;
+ }
+
+ _colorToRgba(color) {
+ color = new colorUtils.CssColor(color);
+ const rgba = color.getRGBATuple();
+ return [rgba.r, rgba.g, rgba.b, rgba.a];
+ }
+
+ _toDefaultType(color) {
+ let unit = this.inspector.defaultColorUnit;
+ let forceUppercase = false;
+ if (unit === colorUtils.CssColor.COLORUNIT.authored) {
+ unit = colorUtils.classifyColor(this._originalColor);
+ forceUppercase = colorUtils.colorIsUppercase(this._originalColor);
+ }
+
+ const colorObj = new colorUtils.CssColor(color);
+ return colorObj.toString(unit, forceUppercase);
+ }
+
+ /**
+ * Overriding the SwatchBasedEditorTooltip.isEditing function to consider the
+ * eyedropper.
+ */
+ isEditing() {
+ return this.tooltip.isVisible() || this.eyedropperOpen;
+ }
+
+ get focusableElements() {
+ return getFocusableElements(this.tooltip.container).filter(
+ el => !!el.offsetParent
+ );
+ }
+
+ destroy() {
+ super.destroy();
+ this.inspector = null;
+ this.spectrum.off("changed", this._onSpectrumColorChange);
+ this.spectrum.destroy();
+ }
+}
+
+module.exports = SwatchColorPickerTooltip;