view-helpers.js (8351B)
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 "use strict"; 5 6 const { KeyCodes } = require("resource://devtools/client/shared/keycodes.js"); 7 8 const PANE_APPEARANCE_DELAY = 50; 9 10 var namedTimeoutsStore = new Map(); 11 12 /** 13 * Helper for draining a rapid succession of events and invoking a callback 14 * once everything settles down. 15 * 16 * @param string id 17 * A string identifier for the named timeout. 18 * @param number wait 19 * The amount of milliseconds to wait after no more events are fired. 20 * @param function callback 21 * Invoked when no more events are fired after the specified time. 22 */ 23 const setNamedTimeout = function setNamedTimeout(id, wait, callback) { 24 clearNamedTimeout(id); 25 26 namedTimeoutsStore.set( 27 id, 28 setTimeout(() => namedTimeoutsStore.delete(id) && callback(), wait) 29 ); 30 }; 31 exports.setNamedTimeout = setNamedTimeout; 32 33 /** 34 * Clears a named timeout. 35 * 36 * @see setNamedTimeout 37 * 38 * @param string id 39 * A string identifier for the named timeout. 40 */ 41 const clearNamedTimeout = function clearNamedTimeout(id) { 42 if (!namedTimeoutsStore) { 43 return; 44 } 45 clearTimeout(namedTimeoutsStore.get(id)); 46 namedTimeoutsStore.delete(id); 47 }; 48 exports.clearNamedTimeout = clearNamedTimeout; 49 50 /** 51 * Helpers for creating and messaging between UI components. 52 */ 53 exports.ViewHelpers = { 54 /** 55 * Convenience method, dispatching a custom event. 56 * 57 * @param Node target 58 * A custom target element to dispatch the event from. 59 * @param string type 60 * The name of the event. 61 * @param any detail 62 * The data passed when initializing the event. 63 * @return boolean 64 * True if the event was cancelled or a registered handler 65 * called preventDefault. 66 */ 67 dispatchEvent(target, type, detail) { 68 if (!(target instanceof Node)) { 69 // Event cancelled. 70 return true; 71 } 72 const document = target.ownerDocument || target; 73 const dispatcher = target.ownerDocument ? target : document.documentElement; 74 75 const event = document.createEvent("CustomEvent"); 76 event.initCustomEvent(type, true, true, detail); 77 return dispatcher.dispatchEvent(event); 78 }, 79 80 /** 81 * Helper delegating some of the DOM attribute methods of a node to a widget. 82 * 83 * @param object widget 84 * The widget to assign the methods to. 85 * @param Node node 86 * A node to delegate the methods to. 87 */ 88 delegateWidgetAttributeMethods(widget, node) { 89 widget.getAttribute = widget.getAttribute || node.getAttribute.bind(node); 90 widget.setAttribute = widget.setAttribute || node.setAttribute.bind(node); 91 widget.removeAttribute = 92 widget.removeAttribute || node.removeAttribute.bind(node); 93 }, 94 95 /** 96 * Helper delegating some of the DOM event methods of a node to a widget. 97 * 98 * @param object widget 99 * The widget to assign the methods to. 100 * @param Node node 101 * A node to delegate the methods to. 102 */ 103 delegateWidgetEventMethods(widget, node) { 104 widget.addEventListener = 105 widget.addEventListener || node.addEventListener.bind(node); 106 widget.removeEventListener = 107 widget.removeEventListener || node.removeEventListener.bind(node); 108 }, 109 110 /** 111 * Checks if the specified object looks like it's been decorated by an 112 * event emitter. 113 * 114 * @return boolean 115 * True if it looks, walks and quacks like an event emitter. 116 */ 117 isEventEmitter(object) { 118 return object?.on && object?.off && object?.once && object?.emit; 119 }, 120 121 /** 122 * Checks if the specified object is an instance of a DOM node. 123 * 124 * @return boolean 125 * True if it's a node, false otherwise. 126 */ 127 isNode(object) { 128 return ( 129 object instanceof Node || 130 object instanceof Element || 131 Cu.getClassName(object) == "DocumentFragment" 132 ); 133 }, 134 135 /** 136 * Prevents event propagation when navigation keys are pressed. 137 * 138 * @param Event e 139 * The event to be prevented. 140 */ 141 preventScrolling(e) { 142 switch (e.keyCode) { 143 case KeyCodes.DOM_VK_UP: 144 case KeyCodes.DOM_VK_DOWN: 145 case KeyCodes.DOM_VK_LEFT: 146 case KeyCodes.DOM_VK_RIGHT: 147 case KeyCodes.DOM_VK_PAGE_UP: 148 case KeyCodes.DOM_VK_PAGE_DOWN: 149 case KeyCodes.DOM_VK_HOME: 150 case KeyCodes.DOM_VK_END: 151 e.preventDefault(); 152 e.stopPropagation(); 153 } 154 }, 155 156 /** 157 * Check if the enter key or space was pressed 158 * 159 * @param event event 160 * The event triggered by a keydown or keypress on an element 161 */ 162 isSpaceOrReturn(event) { 163 return ( 164 event.keyCode === KeyCodes.DOM_VK_SPACE || 165 event.keyCode === KeyCodes.DOM_VK_RETURN 166 ); 167 }, 168 169 /** 170 * Sets a toggled pane hidden or visible. The pane can either be displayed on 171 * the side (right or left depending on the locale) or at the bottom. 172 * 173 * @param object flags 174 * An object containing some of the following properties: 175 * - visible: true if the pane should be shown, false to hide 176 * - animated: true to display an animation on toggle 177 * - delayed: true to wait a few cycles before toggle 178 * - callback: a function to invoke when the toggle finishes 179 * @param Node pane 180 * The element representing the pane to toggle. 181 */ 182 togglePane(flags, pane) { 183 // Make sure a pane is actually available first. 184 if (!pane) { 185 return; 186 } 187 188 // Hiding is always handled via margins, not the hidden attribute. 189 pane.removeAttribute("hidden"); 190 191 // Add a class to the pane to handle min-widths, margins and animations. 192 pane.classList.add("generic-toggled-pane"); 193 194 // Avoid toggles in the middle of animation. 195 if (pane.hasAttribute("animated")) { 196 return; 197 } 198 199 // Avoid useless toggles. 200 if (flags.visible == !pane.classList.contains("pane-collapsed")) { 201 if (flags.callback) { 202 flags.callback(); 203 } 204 return; 205 } 206 207 // The "animated" attributes enables animated toggles (slide in-out). 208 if (flags.animated) { 209 pane.setAttribute("animated", ""); 210 } else { 211 pane.removeAttribute("animated"); 212 } 213 214 // Computes and sets the pane margins in order to hide or show it. 215 const doToggle = () => { 216 // Negative margins are applied to "right" and "left" to support RTL and 217 // LTR directions, as well as to "bottom" to support vertical layouts. 218 // Unnecessary negative margins are forced to 0 via CSS in widgets.css. 219 if (flags.visible) { 220 pane.style.marginLeft = "0"; 221 pane.style.marginRight = "0"; 222 pane.style.marginBottom = "0"; 223 pane.classList.remove("pane-collapsed"); 224 } else { 225 const width = Math.floor(pane.getAttribute("width")) + 1; 226 const height = Math.floor(pane.getAttribute("height")) + 1; 227 pane.style.marginLeft = -width + "px"; 228 pane.style.marginRight = -width + "px"; 229 pane.style.marginBottom = -height + "px"; 230 } 231 232 // Wait for the animation to end before calling afterToggle() 233 if (flags.animated) { 234 const options = { 235 useCapture: false, 236 once: true, 237 }; 238 239 pane.addEventListener( 240 "transitionend", 241 () => { 242 // Prevent unwanted transitions: if the panel is hidden and the layout 243 // changes margins will be updated and the panel will pop out. 244 pane.removeAttribute("animated"); 245 246 if (!flags.visible) { 247 pane.classList.add("pane-collapsed"); 248 } 249 if (flags.callback) { 250 flags.callback(); 251 } 252 }, 253 options 254 ); 255 } else { 256 if (!flags.visible) { 257 pane.classList.add("pane-collapsed"); 258 } 259 260 // Invoke the callback immediately since there's no transition. 261 if (flags.callback) { 262 flags.callback(); 263 } 264 } 265 }; 266 267 // Sometimes it's useful delaying the toggle a few ticks to ensure 268 // a smoother slide in-out animation. 269 if (flags.delayed) { 270 pane.ownerDocument.defaultView.setTimeout( 271 doToggle, 272 PANE_APPEARANCE_DELAY 273 ); 274 } else { 275 doToggle(); 276 } 277 }, 278 };