SwatchBasedEditorTooltip.js (7340B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 const EventEmitter = require("resource://devtools/shared/event-emitter.js"); 8 const KeyShortcuts = require("resource://devtools/client/shared/key-shortcuts.js"); 9 const { 10 HTMLTooltip, 11 } = require("resource://devtools/client/shared/widgets/tooltip/HTMLTooltip.js"); 12 13 loader.lazyRequireGetter( 14 this, 15 "KeyCodes", 16 "resource://devtools/client/shared/keycodes.js", 17 true 18 ); 19 20 /** 21 * Base class for all (color, gradient, ...)-swatch based value editors inside 22 * tooltips 23 * 24 * @param {Document} document 25 * The document to attach the SwatchBasedEditorTooltip. This should be the 26 * toolbox document 27 */ 28 29 class SwatchBasedEditorTooltip { 30 constructor(document) { 31 EventEmitter.decorate(this); 32 33 // This one will consume outside clicks as it makes more sense to let the user 34 // close the tooltip by clicking out 35 // It will also close on <escape> and <enter> 36 this.tooltip = new HTMLTooltip(document, { 37 type: "arrow", 38 consumeOutsideClicks: true, 39 useXulWrapper: true, 40 }); 41 42 // By default, swatch-based editor tooltips revert value change on <esc> and 43 // commit value change on <enter> 44 this.shortcuts = new KeyShortcuts({ 45 window: this.tooltip.doc.defaultView, 46 }); 47 this.shortcuts.on("Escape", event => { 48 if (!this.tooltip.isVisible()) { 49 return; 50 } 51 this.revert(); 52 this.hide(); 53 event.stopPropagation(); 54 event.preventDefault(); 55 }); 56 this.shortcuts.on("Return", event => { 57 if (!this.tooltip.isVisible()) { 58 return; 59 } 60 this.commit(); 61 this.hide(); 62 event.stopPropagation(); 63 event.preventDefault(); 64 }); 65 66 // All target swatches are kept in a WeakMap, indexed by swatch DOM elements 67 this.swatches = new WeakMap(); 68 69 // When a swatch is clicked, and for as long as the tooltip is shown, the 70 // activeSwatch property will hold the reference to the swatch DOM element 71 // that was clicked 72 this.activeSwatch = null; 73 74 this._onSwatchClick = this._onSwatchClick.bind(this); 75 this._onSwatchKeyDown = this._onSwatchKeyDown.bind(this); 76 } 77 78 /** 79 * Reports if the tooltip is currently shown 80 * 81 * @return {boolean} True if the tooltip is displayed. 82 */ 83 isVisible() { 84 return this.tooltip.isVisible(); 85 } 86 87 /** 88 * Reports if the tooltip is currently editing the targeted value 89 * 90 * @return {boolean} True if the tooltip is editing. 91 */ 92 isEditing() { 93 return this.isVisible(); 94 } 95 96 /** 97 * Show the editor tooltip for the currently active swatch. 98 * 99 * @return {Promise} a promise that resolves once the editor tooltip is displayed, or 100 * immediately if there is no currently active swatch. 101 */ 102 show() { 103 if (this.tooltipAnchor) { 104 const onShown = this.tooltip.once("shown"); 105 106 this.tooltip.show(this.tooltipAnchor); 107 this.tooltip.once("hidden", () => this.onTooltipHidden()); 108 109 return onShown; 110 } 111 112 return Promise.resolve(); 113 } 114 115 /** 116 * Can be overridden by subclasses if implementation specific behavior is needed on 117 * tooltip hidden. 118 */ 119 onTooltipHidden() { 120 // When the tooltip is closed by clicking outside the panel we want to commit any 121 // changes. 122 if (!this._reverted) { 123 this.commit(); 124 } 125 this._reverted = false; 126 127 // Once the tooltip is hidden we need to clean up any remaining objects. 128 this.activeSwatch = null; 129 } 130 131 hide() { 132 if (this.swatchActivatedWithKeyboard) { 133 this.activeSwatch.focus(); 134 this.swatchActivatedWithKeyboard = null; 135 } 136 137 this.tooltip.hide(); 138 } 139 140 /** 141 * Add a new swatch DOM element to the list of swatch elements this editor 142 * tooltip knows about. That means from now on, clicking on that swatch will 143 * toggle the editor. 144 * 145 * @param {node} swatchEl 146 * The element to add 147 * @param {object} callbacks 148 * Callbacks that will be executed when the editor wants to preview a 149 * value change, or revert a change, or commit a change. 150 * - onShow: will be called when one of the swatch tooltip is shown 151 * - onPreview: will be called when one of the sub-classes calls 152 * preview 153 * - onRevert: will be called when the user ESCapes out of the tooltip 154 * - onCommit: will be called when the user presses ENTER or clicks 155 * outside the tooltip. 156 */ 157 addSwatch(swatchEl, callbacks = {}) { 158 if (!callbacks.onShow) { 159 callbacks.onShow = function () {}; 160 } 161 if (!callbacks.onPreview) { 162 callbacks.onPreview = function () {}; 163 } 164 if (!callbacks.onRevert) { 165 callbacks.onRevert = function () {}; 166 } 167 if (!callbacks.onCommit) { 168 callbacks.onCommit = function () {}; 169 } 170 171 this.swatches.set(swatchEl, { 172 callbacks, 173 }); 174 swatchEl.addEventListener("click", this._onSwatchClick); 175 swatchEl.addEventListener("keydown", this._onSwatchKeyDown); 176 } 177 178 removeSwatch(swatchEl) { 179 if (this.swatches.has(swatchEl)) { 180 if (this.activeSwatch === swatchEl) { 181 this.hide(); 182 this.activeSwatch = null; 183 } 184 swatchEl.removeEventListener("click", this._onSwatchClick); 185 swatchEl.removeEventListener("keydown", this._onSwatchKeyDown); 186 this.swatches.delete(swatchEl); 187 } 188 } 189 190 _onSwatchKeyDown(event) { 191 if ( 192 event.keyCode === KeyCodes.DOM_VK_RETURN || 193 event.keyCode === KeyCodes.DOM_VK_SPACE 194 ) { 195 event.preventDefault(); 196 event.stopPropagation(); 197 this._onSwatchClick(event); 198 } 199 } 200 201 _onSwatchClick(event) { 202 const { shiftKey, clientX, clientY, target } = event; 203 204 // If mouse coordinates are 0, the event listener could have been triggered 205 // by a keybaord 206 this.swatchActivatedWithKeyboard = 207 event.key && clientX === 0 && clientY === 0; 208 209 if (shiftKey) { 210 event.stopPropagation(); 211 return; 212 } 213 214 const swatch = this.swatches.get(target); 215 216 if (swatch) { 217 this.activeSwatch = target; 218 this.show(); 219 swatch.callbacks.onShow(); 220 event.stopPropagation(); 221 } 222 } 223 224 /** 225 * Not called by this parent class, needs to be taken care of by sub-classes 226 */ 227 preview(value) { 228 if (this.activeSwatch) { 229 const swatch = this.swatches.get(this.activeSwatch); 230 swatch.callbacks.onPreview(value); 231 } 232 } 233 234 /** 235 * This parent class only calls this on <esc> keydown 236 */ 237 revert() { 238 if (this.activeSwatch) { 239 this._reverted = true; 240 const swatch = this.swatches.get(this.activeSwatch); 241 this.tooltip.once("hidden", () => { 242 swatch.callbacks.onRevert(); 243 }); 244 } 245 } 246 247 /** 248 * This parent class only calls this on <enter> keydown 249 */ 250 commit() { 251 if (this.activeSwatch) { 252 const swatch = this.swatches.get(this.activeSwatch); 253 swatch.callbacks.onCommit(); 254 } 255 } 256 257 get tooltipAnchor() { 258 return this.activeSwatch; 259 } 260 261 destroy() { 262 this.activeSwatch = null; 263 this.tooltip.off("keydown", this._onTooltipKeydown); 264 this.tooltip.destroy(); 265 this.shortcuts.destroy(); 266 } 267 } 268 269 module.exports = SwatchBasedEditorTooltip;