Source code
Revision control
Copy as Markdown
Other Tools
/* 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
"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;