remote-node-picker-notice.js (5187B)
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 { 8 CanvasFrameAnonymousContentHelper, 9 } = require("resource://devtools/server/actors/highlighters/utils/markup.js"); 10 11 loader.lazyGetter(this, "HighlightersBundle", () => { 12 return new Localization(["devtools/shared/highlighters.ftl"], true); 13 }); 14 15 loader.lazyGetter(this, "isAndroid", () => { 16 return Services.appinfo.OS === "Android"; 17 }); 18 19 /** 20 * The RemoteNodePickerNotice is a class that displays a notice in a remote debugged page. 21 * This is used to signal to users they can click/tap an element to select it in the 22 * about:devtools-toolbox toolbox inspector. 23 */ 24 class RemoteNodePickerNotice { 25 #highlighterEnvironment; 26 #previousHoveredElement; 27 28 rootElementId = "node-picker-notice-root"; 29 hideButtonId = "node-picker-notice-hide-button"; 30 infoNoticeElementId = "node-picker-notice-info"; 31 32 /** 33 * @param {highlighterEnvironment} highlighterEnvironment 34 */ 35 constructor(highlighterEnvironment) { 36 this.#highlighterEnvironment = highlighterEnvironment; 37 38 this.markup = new CanvasFrameAnonymousContentHelper( 39 this.#highlighterEnvironment, 40 this.#buildMarkup, 41 { 42 contentRootHostClassName: 43 "devtools-highlighter-remote-node-picker-notice", 44 } 45 ); 46 this.isReady = this.markup.initialize(); 47 } 48 49 #buildMarkup = () => { 50 const container = this.markup.createNode({ 51 attributes: { class: "highlighter-container" }, 52 }); 53 54 // Wrapper element. 55 const wrapper = this.markup.createNode({ 56 parent: container, 57 attributes: { 58 id: this.rootElementId, 59 hidden: "true", 60 overlay: "true", 61 }, 62 }); 63 64 const toolbar = this.markup.createNode({ 65 parent: wrapper, 66 attributes: { 67 id: "node-picker-notice-toolbar", 68 class: "toolbar", 69 }, 70 }); 71 72 this.markup.createNode({ 73 parent: toolbar, 74 attributes: { 75 id: "node-picker-notice-icon", 76 class: isAndroid ? "touch" : "", 77 }, 78 }); 79 80 const actionStr = HighlightersBundle.formatValueSync( 81 isAndroid 82 ? "remote-node-picker-notice-action-touch" 83 : "remote-node-picker-notice-action-desktop" 84 ); 85 86 this.markup.createNode({ 87 nodeType: "span", 88 parent: toolbar, 89 text: HighlightersBundle.formatValueSync("remote-node-picker-notice", { 90 action: actionStr, 91 }), 92 attributes: { 93 id: this.infoNoticeElementId, 94 }, 95 }); 96 97 this.markup.createNode({ 98 nodeType: "button", 99 parent: toolbar, 100 text: HighlightersBundle.formatValueSync( 101 "remote-node-picker-notice-hide-button" 102 ), 103 attributes: { 104 id: this.hideButtonId, 105 }, 106 }); 107 108 return container; 109 }; 110 111 destroy() { 112 // hide will nullify take care of this.#abortController. 113 this.hide(); 114 this.markup.destroy(); 115 this.#highlighterEnvironment = null; 116 this.#previousHoveredElement = null; 117 } 118 119 /** 120 * We can't use event listener directly on the anonymous content because they aren't 121 * working while the page is paused. 122 * This is called from the NodePicker instance for easier events management. 123 * 124 * @param {ClickEvent} 125 */ 126 onClick(e) { 127 const target = e.originalTarget || e.target; 128 const targetId = target?.id; 129 130 if (targetId === this.hideButtonId) { 131 this.hide(); 132 } 133 } 134 135 /** 136 * Since we can't use :hover in the CSS for the anonymous content as it wouldn't work 137 * when the page is paused, we have to roll our own implementation, adding a `.hover` 138 * class for the element we want to style on hover (e.g. the close button). 139 * This is called from the NodePicker instance for easier events management. 140 * 141 * @param {MouseMoveEvent} 142 */ 143 handleHoveredElement(e) { 144 const hideButton = this.markup.getElement(this.hideButtonId); 145 146 const target = e.originalTarget || e.target; 147 const targetId = target?.id; 148 149 // If the user didn't change targets, do nothing 150 if (this.#previousHoveredElement?.id === targetId) { 151 return; 152 } 153 154 if (targetId === this.hideButtonId) { 155 hideButton.classList?.add("hover"); 156 } else { 157 hideButton.classList?.remove("hover"); 158 } 159 this.#previousHoveredElement = target; 160 } 161 162 getMarkupRootElement() { 163 return this.markup.getElement(this.rootElementId); 164 } 165 166 async show() { 167 if (this.#highlighterEnvironment.isXUL) { 168 return false; 169 } 170 await this.isReady; 171 172 // Show the highlighter's root element. 173 const root = this.getMarkupRootElement(); 174 root.removeAttribute("hidden"); 175 root.setAttribute("overlay", "true"); 176 177 return true; 178 } 179 180 hide() { 181 if (this.#highlighterEnvironment.isXUL) { 182 return; 183 } 184 185 // Hide the overlay. 186 this.getMarkupRootElement().setAttribute("hidden", "true"); 187 // Reset the hover state 188 this.markup.getElement(this.hideButtonId).classList.remove("hover"); 189 this.#previousHoveredElement = null; 190 } 191 } 192 exports.RemoteNodePickerNotice = RemoteNodePickerNotice;