summaryrefslogtreecommitdiffstats
path: root/devtools/client/shared/widgets/tooltip/SwatchBasedEditorTooltip.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/shared/widgets/tooltip/SwatchBasedEditorTooltip.js')
-rw-r--r--devtools/client/shared/widgets/tooltip/SwatchBasedEditorTooltip.js270
1 files changed, 270 insertions, 0 deletions
diff --git a/devtools/client/shared/widgets/tooltip/SwatchBasedEditorTooltip.js b/devtools/client/shared/widgets/tooltip/SwatchBasedEditorTooltip.js
new file mode 100644
index 0000000000..acc71125e8
--- /dev/null
+++ b/devtools/client/shared/widgets/tooltip/SwatchBasedEditorTooltip.js
@@ -0,0 +1,270 @@
+/* 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 EventEmitter = require("resource://devtools/shared/event-emitter.js");
+const KeyShortcuts = require("resource://devtools/client/shared/key-shortcuts.js");
+const {
+ HTMLTooltip,
+} = require("resource://devtools/client/shared/widgets/tooltip/HTMLTooltip.js");
+
+loader.lazyRequireGetter(
+ this,
+ "KeyCodes",
+ "resource://devtools/client/shared/keycodes.js",
+ true
+);
+
+/**
+ * Base class for all (color, gradient, ...)-swatch based value editors inside
+ * tooltips
+ *
+ * @param {Document} document
+ * The document to attach the SwatchBasedEditorTooltip. This should be the
+ * toolbox document
+ */
+
+class SwatchBasedEditorTooltip {
+ constructor(document) {
+ EventEmitter.decorate(this);
+
+ // This one will consume outside clicks as it makes more sense to let the user
+ // close the tooltip by clicking out
+ // It will also close on <escape> and <enter>
+ this.tooltip = new HTMLTooltip(document, {
+ type: "arrow",
+ consumeOutsideClicks: true,
+ useXulWrapper: true,
+ });
+
+ // By default, swatch-based editor tooltips revert value change on <esc> and
+ // commit value change on <enter>
+ this.shortcuts = new KeyShortcuts({
+ window: this.tooltip.doc.defaultView,
+ });
+ this.shortcuts.on("Escape", event => {
+ if (!this.tooltip.isVisible()) {
+ return;
+ }
+ this.revert();
+ this.hide();
+ event.stopPropagation();
+ event.preventDefault();
+ });
+ this.shortcuts.on("Return", event => {
+ if (!this.tooltip.isVisible()) {
+ return;
+ }
+ this.commit();
+ this.hide();
+ event.stopPropagation();
+ event.preventDefault();
+ });
+
+ // All target swatches are kept in a map, indexed by swatch DOM elements
+ this.swatches = new Map();
+
+ // When a swatch is clicked, and for as long as the tooltip is shown, the
+ // activeSwatch property will hold the reference to the swatch DOM element
+ // that was clicked
+ this.activeSwatch = null;
+
+ this._onSwatchClick = this._onSwatchClick.bind(this);
+ this._onSwatchKeyDown = this._onSwatchKeyDown.bind(this);
+ }
+
+ /**
+ * Reports if the tooltip is currently shown
+ *
+ * @return {Boolean} True if the tooltip is displayed.
+ */
+ isVisible() {
+ return this.tooltip.isVisible();
+ }
+
+ /**
+ * Reports if the tooltip is currently editing the targeted value
+ *
+ * @return {Boolean} True if the tooltip is editing.
+ */
+ isEditing() {
+ return this.isVisible();
+ }
+
+ /**
+ * Show the editor tooltip for the currently active swatch.
+ *
+ * @return {Promise} a promise that resolves once the editor tooltip is displayed, or
+ * immediately if there is no currently active swatch.
+ */
+ show() {
+ if (this.tooltipAnchor) {
+ const onShown = this.tooltip.once("shown");
+
+ this.tooltip.show(this.tooltipAnchor);
+ this.tooltip.once("hidden", () => this.onTooltipHidden());
+
+ return onShown;
+ }
+
+ return Promise.resolve();
+ }
+
+ /**
+ * Can be overridden by subclasses if implementation specific behavior is needed on
+ * tooltip hidden.
+ */
+ onTooltipHidden() {
+ // When the tooltip is closed by clicking outside the panel we want to commit any
+ // changes.
+ if (!this._reverted) {
+ this.commit();
+ }
+ this._reverted = false;
+
+ // Once the tooltip is hidden we need to clean up any remaining objects.
+ this.activeSwatch = null;
+ }
+
+ hide() {
+ if (this.swatchActivatedWithKeyboard) {
+ this.activeSwatch.focus();
+ this.swatchActivatedWithKeyboard = null;
+ }
+
+ this.tooltip.hide();
+ }
+
+ /**
+ * Add a new swatch DOM element to the list of swatch elements this editor
+ * tooltip knows about. That means from now on, clicking on that swatch will
+ * toggle the editor.
+ *
+ * @param {node} swatchEl
+ * The element to add
+ * @param {object} callbacks
+ * Callbacks that will be executed when the editor wants to preview a
+ * value change, or revert a change, or commit a change.
+ * - onShow: will be called when one of the swatch tooltip is shown
+ * - onPreview: will be called when one of the sub-classes calls
+ * preview
+ * - onRevert: will be called when the user ESCapes out of the tooltip
+ * - onCommit: will be called when the user presses ENTER or clicks
+ * outside the tooltip.
+ */
+ addSwatch(swatchEl, callbacks = {}) {
+ if (!callbacks.onShow) {
+ callbacks.onShow = function () {};
+ }
+ if (!callbacks.onPreview) {
+ callbacks.onPreview = function () {};
+ }
+ if (!callbacks.onRevert) {
+ callbacks.onRevert = function () {};
+ }
+ if (!callbacks.onCommit) {
+ callbacks.onCommit = function () {};
+ }
+
+ this.swatches.set(swatchEl, {
+ callbacks,
+ });
+ swatchEl.addEventListener("click", this._onSwatchClick);
+ swatchEl.addEventListener("keydown", this._onSwatchKeyDown);
+ }
+
+ removeSwatch(swatchEl) {
+ if (this.swatches.has(swatchEl)) {
+ if (this.activeSwatch === swatchEl) {
+ this.hide();
+ this.activeSwatch = null;
+ }
+ swatchEl.removeEventListener("click", this._onSwatchClick);
+ swatchEl.removeEventListener("keydown", this._onSwatchKeyDown);
+ this.swatches.delete(swatchEl);
+ }
+ }
+
+ _onSwatchKeyDown(event) {
+ if (
+ event.keyCode === KeyCodes.DOM_VK_RETURN ||
+ event.keyCode === KeyCodes.DOM_VK_SPACE
+ ) {
+ event.preventDefault();
+ event.stopPropagation();
+ this._onSwatchClick(event);
+ }
+ }
+
+ _onSwatchClick(event) {
+ const { shiftKey, clientX, clientY, target } = event;
+
+ // If mouse coordinates are 0, the event listener could have been triggered
+ // by a keybaord
+ this.swatchActivatedWithKeyboard =
+ event.key && clientX === 0 && clientY === 0;
+
+ if (shiftKey) {
+ event.stopPropagation();
+ return;
+ }
+
+ const swatch = this.swatches.get(target);
+
+ if (swatch) {
+ this.activeSwatch = target;
+ this.show();
+ swatch.callbacks.onShow();
+ event.stopPropagation();
+ }
+ }
+
+ /**
+ * Not called by this parent class, needs to be taken care of by sub-classes
+ */
+ preview(value) {
+ if (this.activeSwatch) {
+ const swatch = this.swatches.get(this.activeSwatch);
+ swatch.callbacks.onPreview(value);
+ }
+ }
+
+ /**
+ * This parent class only calls this on <esc> keydown
+ */
+ revert() {
+ if (this.activeSwatch) {
+ this._reverted = true;
+ const swatch = this.swatches.get(this.activeSwatch);
+ this.tooltip.once("hidden", () => {
+ swatch.callbacks.onRevert();
+ });
+ }
+ }
+
+ /**
+ * This parent class only calls this on <enter> keydown
+ */
+ commit() {
+ if (this.activeSwatch) {
+ const swatch = this.swatches.get(this.activeSwatch);
+ swatch.callbacks.onCommit();
+ }
+ }
+
+ get tooltipAnchor() {
+ return this.activeSwatch;
+ }
+
+ destroy() {
+ this.swatches.clear();
+ this.activeSwatch = null;
+ this.tooltip.off("keydown", this._onTooltipKeydown);
+ this.tooltip.destroy();
+ this.shortcuts.destroy();
+ }
+}
+
+module.exports = SwatchBasedEditorTooltip;