css-compatibility-tooltip-helper.js (9197B)
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 { BrowserLoader } = ChromeUtils.importESModule( 8 "resource://devtools/shared/loader/browser-loader.sys.mjs" 9 ); 10 11 loader.lazyRequireGetter( 12 this, 13 "openDocLink", 14 "resource://devtools/client/shared/link.js", 15 true 16 ); 17 18 class CssCompatibilityTooltipHelper { 19 constructor() { 20 this.addTab = this.addTab.bind(this); 21 } 22 23 #currentTooltip = null; 24 #currentUrl = null; 25 26 #createElement(doc, tag, classList = [], attributeList = {}) { 27 const XHTML_NS = "http://www.w3.org/1999/xhtml"; 28 const newElement = doc.createElementNS(XHTML_NS, tag); 29 for (const elementClass of classList) { 30 newElement.classList.add(elementClass); 31 } 32 33 for (const key in attributeList) { 34 newElement.setAttribute(key, attributeList[key]); 35 } 36 37 return newElement; 38 } 39 40 /* 41 * Attach the UnsupportedBrowserList component to the 42 * ".compatibility-browser-list-wrapper" div to render the 43 * unsupported browser list 44 */ 45 #renderUnsupportedBrowserList(container, unsupportedBrowsers) { 46 // Mount the ReactDOM only if the unsupported browser 47 // list is not empty. Else "compatibility-browser-list-wrapper" 48 // is not defined. For example, for property clip, 49 // unsupportedBrowsers is an empty array 50 if (!unsupportedBrowsers.length) { 51 return; 52 } 53 54 const { require } = BrowserLoader({ 55 baseURI: "resource://devtools/client/shared/widgets/tooltip/", 56 window: this.#currentTooltip.doc.defaultView, 57 }); 58 const { 59 createFactory, 60 createElement, 61 } = require("resource://devtools/client/shared/vendor/react.mjs"); 62 const ReactDOM = require("resource://devtools/client/shared/vendor/react-dom.mjs"); 63 const UnsupportedBrowserList = createFactory( 64 require("resource://devtools/client/inspector/compatibility/components/UnsupportedBrowserList.js") 65 ); 66 67 const unsupportedBrowserList = createElement(UnsupportedBrowserList, { 68 browsers: unsupportedBrowsers, 69 }); 70 ReactDOM.render( 71 unsupportedBrowserList, 72 container.querySelector(".compatibility-browser-list-wrapper") 73 ); 74 } 75 76 /* 77 * Get the first paragraph for the compatibility tooltip 78 * Return a subtree similar to: 79 * <p data-l10n-id="css-compatibility-default-message" 80 * data-l10n-args="{"property":"user-select"}"> 81 * </p> 82 */ 83 #getCompatibilityMessage(doc, data) { 84 const { msgId, property } = data; 85 return this.#createElement(doc, "p", [], { 86 "data-l10n-id": msgId, 87 "data-l10n-args": JSON.stringify({ property }), 88 }); 89 } 90 91 /** 92 * Gets the paragraph elements related to the browserList. 93 * This returns an array with following subtree: 94 * [ 95 * <p data-l10n-id="css-compatibility-browser-list-message"></p>, 96 * <p> 97 * <ul class="compatibility-unsupported-browser-list"> 98 * <list-element /> 99 * </ul> 100 * </p> 101 * ] 102 * The first element is the message and the second element is the 103 * unsupported browserList itself. 104 * If the unsupportedBrowser is an empty array, we return an empty 105 * array back. 106 */ 107 #getBrowserListContainer(doc, unsupportedBrowsers) { 108 if (!unsupportedBrowsers.length) { 109 return null; 110 } 111 112 const browserList = this.#createElement(doc, "p"); 113 const browserListWrapper = this.#createElement(doc, "div", [ 114 "compatibility-browser-list-wrapper", 115 ]); 116 browserList.appendChild(browserListWrapper); 117 118 return browserList; 119 } 120 121 /* 122 * This is the learn more message element linking to the MDN documentation 123 * for the particular incompatible CSS declaration. 124 * The element returned is: 125 * <p data-l10n-id="css-compatibility-learn-more-message" 126 * data-l10n-args="{"property":"user-select"}"> 127 * <span data-l10n-name="link" class="link"></span> 128 * </p> 129 */ 130 #getLearnMoreMessage(doc, { rootProperty }) { 131 const learnMoreMessage = this.#createElement(doc, "p", [], { 132 "data-l10n-id": "css-compatibility-learn-more-message", 133 "data-l10n-args": JSON.stringify({ rootProperty }), 134 }); 135 learnMoreMessage.appendChild( 136 this.#createElement(doc, "span", ["link"], { 137 "data-l10n-name": "link", 138 }) 139 ); 140 141 return learnMoreMessage; 142 } 143 144 /** 145 * Fill the tooltip with inactive CSS information. 146 * 147 * @param {object} data 148 * An object in the following format: { 149 * // Type of compatibility issue 150 * type: <string>, 151 * // The CSS declaration that has compatibility issues 152 * // The raw CSS declaration name that has compatibility issues 153 * declaration: <string>, 154 * property: <string>, 155 * // Alias to the given CSS property 156 * alias: <Array>, 157 * // Link to MDN documentation for the particular CSS rule 158 * url: <string>, 159 * deprecated: <boolean>, 160 * experimental: <boolean>, 161 * // An array of all the browsers that don't support the given CSS rule 162 * unsupportedBrowsers: <Array>, 163 * } 164 * @param {HTMLTooltip} tooltip 165 * The tooltip we are targetting. 166 */ 167 async setContent(data, tooltip) { 168 const fragment = this.getTemplate(data, tooltip); 169 170 tooltip.panel.addEventListener("click", this.addTab); 171 tooltip.once("hidden", () => { 172 tooltip.panel.removeEventListener("click", this.addTab); 173 }); 174 175 await tooltip.setLocalizedFragment(fragment, { width: 267 }); 176 } 177 178 /** 179 * Get the template that the Fluent string will be merged with. This template 180 * looks like this: 181 * 182 * <div class="devtools-tooltip-css-compatibility"> 183 * <p data-l10n-id="css-compatibility-default-message" 184 * data-l10n-args="{"property":"user-select"}"> 185 * <strong></strong> 186 * </p> 187 * <browser-list /> 188 * <p data-l10n-id="css-compatibility-learn-more-message" 189 * data-l10n-args="{"property":"user-select"}"> 190 * <span data-l10n-name="link" class="link"></span> 191 * <strong></strong> 192 * </p> 193 * </div> 194 * 195 * @param {object} data 196 * An object in the following format: { 197 * // Type of compatibility issue 198 * type: <string>, 199 * // The CSS declaration that has compatibility issues 200 * // The raw CSS declaration name that has compatibility issues 201 * declaration: <string>, 202 * property: <string>, 203 * // Alias to the given CSS property 204 * alias: <Array>, 205 * // Link to MDN documentation for the particular CSS rule 206 * url: <string>, 207 * // Link to the spec for the particular CSS rule 208 * specUrl: <string>, 209 * deprecated: <boolean>, 210 * experimental: <boolean>, 211 * // An array of all the browsers that don't support the given CSS rule 212 * unsupportedBrowsers: <Array>, 213 * } 214 * @param {HTMLTooltip} tooltip 215 * The tooltip we are targetting. 216 */ 217 getTemplate(data, tooltip) { 218 const { doc } = tooltip; 219 const { specUrl, url, unsupportedBrowsers } = data; 220 221 this.#currentTooltip = tooltip; 222 this.#currentUrl = url 223 ? `${url}?utm_source=devtools&utm_medium=inspector-css-compatibility&utm_campaign=default` 224 : specUrl; 225 const templateNode = this.#createElement(doc, "template"); 226 227 const tooltipContainer = this.#createElement(doc, "div", [ 228 "devtools-tooltip-css-compatibility", 229 ]); 230 231 tooltipContainer.appendChild(this.#getCompatibilityMessage(doc, data)); 232 const browserListContainer = this.#getBrowserListContainer( 233 doc, 234 unsupportedBrowsers 235 ); 236 if (browserListContainer) { 237 tooltipContainer.appendChild(browserListContainer); 238 this.#renderUnsupportedBrowserList(tooltipContainer, unsupportedBrowsers); 239 } 240 241 if (this.#currentUrl) { 242 tooltipContainer.appendChild(this.#getLearnMoreMessage(doc, data)); 243 } 244 245 templateNode.content.appendChild(tooltipContainer); 246 return doc.importNode(templateNode.content, true); 247 } 248 249 /** 250 * Hide the tooltip, open `this.#currentUrl` in a new tab and focus it. 251 * 252 * @param {DOMEvent} event 253 * The click event originating from the tooltip. 254 */ 255 addTab(event) { 256 // The XUL panel swallows click events so handlers can't be added directly 257 // to the link span. As a workaround we listen to all click events in the 258 // panel and if a link span is clicked we proceed. 259 if (event.target.className !== "link") { 260 return; 261 } 262 263 const tooltip = this.#currentTooltip; 264 tooltip.hide(); 265 266 const isMacOS = Services.appinfo.OS === "Darwin"; 267 openDocLink(this.#currentUrl, { 268 relatedToCurrent: true, 269 inBackground: isMacOS ? event.metaKey : event.ctrlKey, 270 }); 271 } 272 273 destroy() { 274 this.#currentTooltip = null; 275 this.#currentUrl = null; 276 } 277 } 278 279 module.exports = CssCompatibilityTooltipHelper;