SharingUtils.sys.mjs (9484B)
1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- 2 * This Source Code Form is subject to the terms of the Mozilla Public 3 * License, v. 2.0. If a copy of the MPL was not distributed with this 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 6 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; 7 import { BrowserUtils } from "resource://gre/modules/BrowserUtils.sys.mjs"; 8 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; 9 10 const APPLE_COPY_LINK = "com.apple.share.CopyLink.invite"; 11 12 let lazy = {}; 13 14 XPCOMUtils.defineLazyServiceGetters(lazy, { 15 MacSharingService: [ 16 "@mozilla.org/widget/macsharingservice;1", 17 Ci.nsIMacSharingService, 18 ], 19 WindowsUIUtils: ["@mozilla.org/windows-ui-utils;1", Ci.nsIWindowsUIUtils], 20 }); 21 22 class SharingUtilsCls { 23 /** 24 * Updates a sharing item in a given menu, creating it if necessary. 25 */ 26 updateShareURLMenuItem(browser, insertAfterEl) { 27 if (!Services.prefs.getBoolPref("browser.menu.share_url.allow", true)) { 28 return; 29 } 30 31 let shareURL = insertAfterEl.nextElementSibling; 32 if (!shareURL?.matches(".share-tab-url-item")) { 33 shareURL = this.#createShareURLMenuItem(insertAfterEl); 34 } 35 36 shareURL.browserToShare = Cu.getWeakReference(browser); 37 if (AppConstants.platform != "macosx") { 38 // On macOS, we keep the item enabled and handle enabled state 39 // inside the menupopup. 40 // Everywhere else, we disable the item, as there's no submenu. 41 shareURL.hidden = !BrowserUtils.getShareableURL(browser.currentURI); 42 } 43 } 44 45 /** 46 * Creates and returns the "Share" menu item. 47 */ 48 #createShareURLMenuItem(insertAfterEl) { 49 let menu = insertAfterEl.parentNode; 50 let shareURL = null; 51 let document = insertAfterEl.ownerDocument; 52 if (AppConstants.platform != "win" && AppConstants.platform != "macosx") { 53 shareURL = this.#buildCopyLinkItem(document); 54 } else { 55 if (AppConstants.platform == "win") { 56 shareURL = this.#buildShareURLItem(document); 57 } else if (AppConstants.platform == "macosx") { 58 shareURL = this.#buildShareURLMenu(document); 59 } 60 let l10nID = 61 menu.id == "tabContextMenu" 62 ? "tab-context-share-url" 63 : "menu-file-share-url"; 64 document.l10n.setAttributes(shareURL, l10nID); 65 } 66 shareURL.classList.add("share-tab-url-item"); 67 68 menu.insertBefore(shareURL, insertAfterEl.nextSibling); 69 return shareURL; 70 } 71 72 /** 73 * Returns a menu item specifically for accessing Windows sharing services. 74 */ 75 #buildShareURLItem(document) { 76 let shareURLMenuItem = document.createXULElement("menuitem"); 77 shareURLMenuItem.addEventListener("command", this); 78 return shareURLMenuItem; 79 } 80 81 /** 82 * Returns a menu specifically for accessing macOSx sharing services . 83 */ 84 #buildShareURLMenu(document) { 85 let menu = document.createXULElement("menu"); 86 let menuPopup = document.createXULElement("menupopup"); 87 menuPopup.addEventListener("popupshowing", this); 88 menu.appendChild(menuPopup); 89 return menu; 90 } 91 92 /** 93 * Return a menuitem that only copies the link. Useful for 94 * OSes where we do not yet have full share support, like Linux. 95 * 96 * We currently also use this on macOS because for some reason Apple does not 97 * provide the share service option for this. 98 */ 99 #buildCopyLinkItem(document) { 100 let shareURLMenuItem = document.createXULElement("menuitem"); 101 document.l10n.setAttributes(shareURLMenuItem, "menu-share-copy-link"); 102 shareURLMenuItem.classList.add("share-copy-link"); 103 104 if (AppConstants.platform == "macosx") { 105 shareURLMenuItem.classList.add("menuitem-iconic"); 106 shareURLMenuItem.setAttribute( 107 "image", 108 "chrome://global/skin/icons/link.svg" 109 ); 110 } else { 111 // On macOS the command handling happens by virtue of the submenu 112 // command event listener. 113 shareURLMenuItem.addEventListener("command", this); 114 } 115 return shareURLMenuItem; 116 } 117 118 /** 119 * Get the sharing data for a given DOM node. 120 */ 121 getDataToShare(node) { 122 let browser = node.browserToShare?.get(); 123 let urlToShare = null; 124 let titleToShare = null; 125 126 if (browser) { 127 let maybeToShare = BrowserUtils.getShareableURL(browser.currentURI); 128 if (maybeToShare) { 129 urlToShare = maybeToShare; 130 titleToShare = browser.contentTitle; 131 } 132 } 133 return { urlToShare, titleToShare }; 134 } 135 136 /** 137 * Populates the "Share" menupopup on macOSx. 138 */ 139 initializeShareURLPopup(menuPopup) { 140 if (AppConstants.platform != "macosx") { 141 return; 142 } 143 144 // Empty menupopup 145 while (menuPopup.firstChild) { 146 menuPopup.firstChild.remove(); 147 } 148 149 let document = menuPopup.ownerDocument; 150 let { gURLBar } = menuPopup.ownerGlobal; 151 152 let { urlToShare } = this.getDataToShare(menuPopup.parentNode); 153 154 // If we can't share the current URL, we display the items disabled, 155 // but enable the "more..." item at the bottom, to allow the user to 156 // change sharing preferences in the system dialog. 157 let shouldEnable = !!urlToShare; 158 if (!urlToShare) { 159 // Fake it so we can ask the sharing service for services: 160 urlToShare = Services.io.newURI("https://mozilla.org/"); 161 } 162 163 let currentURI = gURLBar.makeURIReadable(urlToShare).displaySpec; 164 let services = lazy.MacSharingService.getSharingProviders(currentURI); 165 166 // Apple seems reluctant to provide copy link as a feature. Add it at the 167 // start if it's not there. 168 if (!services.some(s => s.name == APPLE_COPY_LINK)) { 169 let item = this.#buildCopyLinkItem(document); 170 if (!shouldEnable) { 171 item.setAttribute("disabled", "true"); 172 } 173 menuPopup.appendChild(item); 174 } 175 176 services.forEach(share => { 177 let item = document.createXULElement("menuitem"); 178 item.classList.add("menuitem-iconic"); 179 item.setAttribute("label", share.menuItemTitle); 180 item.setAttribute("share-name", share.name); 181 item.setAttribute("image", ChromeUtils.encodeURIForSrcset(share.image)); 182 if (!shouldEnable) { 183 item.setAttribute("disabled", "true"); 184 } 185 menuPopup.appendChild(item); 186 }); 187 menuPopup.appendChild(document.createXULElement("menuseparator")); 188 let moreItem = document.createXULElement("menuitem"); 189 document.l10n.setAttributes(moreItem, "menu-share-more"); 190 moreItem.classList.add("menuitem-iconic", "share-more-button"); 191 menuPopup.appendChild(moreItem); 192 193 menuPopup.addEventListener("command", this); 194 menuPopup.parentNode 195 .closest("menupopup") 196 .addEventListener("popuphiding", this); 197 menuPopup.setAttribute("data-initialized", true); 198 } 199 200 onShareURLCommand(event) { 201 // Only call sharing services for the "Share" menu item. These services 202 // are accessed from a submenu popup for MacOS or the "Share" menu item 203 // for Windows. Use .closest() as a hack to find either the item itself 204 // or a parent with the right class. 205 let target = event.target.closest(".share-tab-url-item"); 206 if (!target) { 207 return; 208 } 209 let { gURLBar } = target.ownerGlobal; 210 211 // urlToShare/titleToShare may be null, in which case only the "more" 212 // item is enabled, so handle that case first: 213 if (event.target.classList.contains("share-more-button")) { 214 lazy.MacSharingService.openSharingPreferences(); 215 return; 216 } 217 218 let { urlToShare, titleToShare } = this.getDataToShare(target); 219 let currentURI = gURLBar.makeURIReadable(urlToShare).displaySpec; 220 221 if (event.target.classList.contains("share-copy-link")) { 222 BrowserUtils.copyLink(currentURI, titleToShare); 223 } else if (AppConstants.platform == "win") { 224 lazy.WindowsUIUtils.shareUrl(currentURI, titleToShare); 225 } else { 226 // On macOSX platforms 227 let shareName = event.target.getAttribute("share-name"); 228 if (shareName) { 229 lazy.MacSharingService.shareUrl(shareName, currentURI, titleToShare); 230 } 231 } 232 } 233 234 onPopupHiding(event) { 235 // We don't want to rebuild the contents of the "Share" menupopup if only its submenu is 236 // hidden. So bail if this isn't the top menupopup in the DOM tree: 237 if (event.target.parentNode.closest("menupopup")) { 238 return; 239 } 240 // Otherwise, clear its "data-initialized" attribute. 241 let menupopup = event.target.querySelector( 242 ".share-tab-url-item" 243 )?.menupopup; 244 menupopup?.removeAttribute("data-initialized"); 245 246 event.target.removeEventListener("popuphiding", this); 247 } 248 249 onPopupShowing(event) { 250 if (!event.target.hasAttribute("data-initialized")) { 251 this.initializeShareURLPopup(event.target); 252 } 253 } 254 255 handleEvent(aEvent) { 256 switch (aEvent.type) { 257 case "command": 258 this.onShareURLCommand(aEvent); 259 break; 260 case "popuphiding": 261 this.onPopupHiding(aEvent); 262 break; 263 case "popupshowing": 264 this.onPopupShowing(aEvent); 265 break; 266 } 267 } 268 269 testOnlyMockUIUtils(mock) { 270 if (!Cu.isInAutomation) { 271 throw new Error("Can only mock utils in automation."); 272 } 273 // eslint-disable-next-line mozilla/valid-lazy 274 Object.defineProperty(lazy, "WindowsUIUtils", { 275 get() { 276 if (mock) { 277 return mock; 278 } 279 return Cc["@mozilla.org/windows-ui-utils;1"].getService( 280 Ci.nsIWindowsUIUtils 281 ); 282 }, 283 }); 284 } 285 } 286 287 export let SharingUtils = new SharingUtilsCls();