opentabs.mjs (26210B)
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 import { 6 classMap, 7 html, 8 map, 9 when, 10 } from "chrome://global/content/vendor/lit.all.mjs"; 11 import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; 12 import { getLogger, MAX_TABS_FOR_RECENT_BROWSING } from "./helpers.mjs"; 13 import { searchTabList } from "./search-helpers.mjs"; 14 import { ViewPage, ViewPageContent } from "./viewpage.mjs"; 15 // eslint-disable-next-line import/no-unassigned-import 16 import "chrome://browser/content/firefoxview/opentabs-tab-list.mjs"; 17 18 const lazy = {}; 19 20 ChromeUtils.defineESModuleGetters(lazy, { 21 BookmarkList: "resource://gre/modules/BookmarkList.sys.mjs", 22 BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs", 23 NonPrivateTabs: "resource:///modules/OpenTabs.sys.mjs", 24 OpenTabsController: "resource:///modules/OpenTabsController.sys.mjs", 25 getTabsTargetForWindow: "resource:///modules/OpenTabs.sys.mjs", 26 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", 27 TabMetrics: "moz-src:///browser/components/tabbrowser/TabMetrics.sys.mjs", 28 }); 29 30 ChromeUtils.defineLazyGetter(lazy, "fxAccounts", () => { 31 return ChromeUtils.importESModule( 32 "resource://gre/modules/FxAccounts.sys.mjs" 33 ).getFxAccountsSingleton(); 34 }); 35 36 const TOPIC_DEVICESTATE_CHANGED = "firefox-view.devicestate.changed"; 37 const TOPIC_DEVICELIST_UPDATED = "fxaccounts:devicelist_updated"; 38 39 /** 40 * A collection of open tabs grouped by window. 41 * 42 * @property {Array<Window>} windows 43 * A list of windows with the same privateness 44 * @property {string} sortOption 45 * The sorting order of open tabs: 46 * - "recency": Sorted by recent activity. (For recent browsing, this is the only option.) 47 * - "tabStripOrder": Match the order in which they appear on the tab strip. 48 */ 49 class OpenTabsInView extends ViewPage { 50 static properties = { 51 ...ViewPage.properties, 52 windows: { type: Array }, 53 searchQuery: { type: String }, 54 sortOption: { type: String }, 55 }; 56 static queries = { 57 viewCards: { all: "view-opentabs-card" }, 58 optionsContainer: ".open-tabs-options", 59 searchTextbox: "moz-input-search", 60 }; 61 62 initialWindowsReady = false; 63 currentWindow = null; 64 openTabsTarget = null; 65 66 constructor() { 67 super(); 68 this._started = false; 69 this.windows = []; 70 this.currentWindow = this.getWindow(); 71 if (lazy.PrivateBrowsingUtils.isWindowPrivate(this.currentWindow)) { 72 this.openTabsTarget = lazy.getTabsTargetForWindow(this.currentWindow); 73 } else { 74 this.openTabsTarget = lazy.NonPrivateTabs; 75 } 76 this.searchQuery = ""; 77 this.sortOption = this.recentBrowsing 78 ? "recency" 79 : Services.prefs.getStringPref( 80 "browser.tabs.firefox-view.ui-state.opentabs.sort-option", 81 "recency" 82 ); 83 } 84 85 start() { 86 if (this._started) { 87 return; 88 } 89 this._started = true; 90 this.#setupTabChangeListener(); 91 92 // To resolve the race between this component wanting to render all the windows' 93 // tabs, while those windows are still potentially opening, flip this property 94 // once the promise resolves and we'll bail out of rendering until then. 95 this.openTabsTarget.readyWindowsPromise.finally(() => { 96 this.initialWindowsReady = true; 97 this._updateWindowList(); 98 }); 99 100 for (let card of this.viewCards) { 101 card.paused = false; 102 card.viewVisibleCallback?.(); 103 } 104 105 if (this.recentBrowsing) { 106 this.recentBrowsingElement.addEventListener( 107 "MozInputSearch:search", 108 this 109 ); 110 } 111 112 this.bookmarkList = new lazy.BookmarkList(this.#getAllTabUrls(), () => 113 this.viewCards.forEach(card => card.requestUpdate()) 114 ); 115 } 116 117 shouldUpdate(changedProperties) { 118 if (!this.initialWindowsReady) { 119 return false; 120 } 121 return super.shouldUpdate(changedProperties); 122 } 123 124 disconnectedCallback() { 125 super.disconnectedCallback(); 126 this.stop(); 127 } 128 129 stop() { 130 if (!this._started) { 131 return; 132 } 133 this._started = false; 134 this.paused = true; 135 136 this.openTabsTarget.removeEventListener("TabChange", this); 137 this.openTabsTarget.removeEventListener("TabRecencyChange", this); 138 139 for (let card of this.viewCards) { 140 card.paused = true; 141 card.viewHiddenCallback?.(); 142 } 143 144 if (this.recentBrowsing) { 145 this.recentBrowsingElement.removeEventListener( 146 "MozInputSearch:search", 147 this 148 ); 149 } 150 151 this.bookmarkList.removeListeners(); 152 } 153 154 viewVisibleCallback() { 155 this.start(); 156 } 157 158 viewHiddenCallback() { 159 this.stop(); 160 } 161 162 #setupTabChangeListener() { 163 if (this.sortOption === "recency") { 164 this.openTabsTarget.addEventListener("TabRecencyChange", this); 165 this.openTabsTarget.removeEventListener("TabChange", this); 166 } else { 167 this.openTabsTarget.removeEventListener("TabRecencyChange", this); 168 this.openTabsTarget.addEventListener("TabChange", this); 169 } 170 } 171 172 #getAllTabUrls() { 173 return this.openTabsTarget 174 .getAllTabs() 175 .map(({ linkedBrowser }) => linkedBrowser?.currentURI?.spec) 176 .filter(Boolean); 177 } 178 179 render() { 180 if (this.recentBrowsing) { 181 return this.getRecentBrowsingTemplate(); 182 } 183 let currentWindowIndex, currentWindowTabs; 184 let index = 1; 185 const otherWindows = []; 186 this.windows.forEach(win => { 187 const tabs = this.openTabsTarget.getTabsForWindow( 188 win, 189 this.sortOption === "recency" 190 ); 191 if (win === this.currentWindow) { 192 currentWindowIndex = index++; 193 currentWindowTabs = tabs; 194 } else { 195 otherWindows.push([index++, tabs, win]); 196 } 197 }); 198 199 const cardClasses = classMap({ 200 "height-limited": this.windows.length > 3, 201 "width-limited": this.windows.length > 1, 202 }); 203 let cardCount; 204 if (this.windows.length <= 1) { 205 cardCount = "one"; 206 } else if (this.windows.length === 2) { 207 cardCount = "two"; 208 } else { 209 cardCount = "three-or-more"; 210 } 211 return html` 212 <link 213 rel="stylesheet" 214 href="chrome://browser/content/firefoxview/view-opentabs.css" 215 /> 216 <link 217 rel="stylesheet" 218 href="chrome://browser/content/firefoxview/firefoxview.css" 219 /> 220 <div class="sticky-container bottom-fade"> 221 <h2 class="page-header" data-l10n-id="firefoxview-opentabs-header"></h2> 222 <div class="open-tabs-options"> 223 <moz-input-search 224 data-l10n-id="firefoxview-search-text-box-opentabs" 225 data-l10n-attrs="placeholder" 226 @MozInputSearch:search=${this.onSearchQuery} 227 ></moz-input-search> 228 <div class="open-tabs-sort-wrapper"> 229 <div class="open-tabs-sort-option"> 230 <input 231 type="radio" 232 id="sort-by-recency" 233 name="open-tabs-sort-option" 234 value="recency" 235 ?checked=${this.sortOption === "recency"} 236 @click=${this.onChangeSortOption} 237 /> 238 <label 239 for="sort-by-recency" 240 data-l10n-id="firefoxview-sort-open-tabs-by-recency-label" 241 ></label> 242 </div> 243 <div class="open-tabs-sort-option"> 244 <input 245 type="radio" 246 id="sort-by-order" 247 name="open-tabs-sort-option" 248 value="tabStripOrder" 249 ?checked=${this.sortOption === "tabStripOrder"} 250 @click=${this.onChangeSortOption} 251 /> 252 <label 253 for="sort-by-order" 254 data-l10n-id="firefoxview-sort-open-tabs-by-order-label" 255 ></label> 256 </div> 257 </div> 258 </div> 259 </div> 260 <div 261 card-count=${cardCount} 262 class="view-opentabs-card-container cards-container" 263 > 264 ${when( 265 currentWindowIndex && currentWindowTabs, 266 () => html` 267 <view-opentabs-card 268 class=${cardClasses} 269 .tabs=${currentWindowTabs} 270 .paused=${this.paused} 271 data-inner-id=${this.currentWindow.windowGlobalChild 272 .innerWindowId} 273 data-l10n-id="firefoxview-opentabs-current-window-header" 274 data-l10n-args=${JSON.stringify({ 275 winID: currentWindowIndex, 276 })} 277 .searchQuery=${this.searchQuery} 278 .bookmarkList=${this.bookmarkList} 279 ></view-opentabs-card> 280 ` 281 )} 282 ${map( 283 otherWindows, 284 ([winID, tabs, win]) => html` 285 <view-opentabs-card 286 class=${cardClasses} 287 .tabs=${tabs} 288 .paused=${this.paused} 289 data-inner-id=${win.windowGlobalChild.innerWindowId} 290 data-l10n-id="firefoxview-opentabs-window-header" 291 data-l10n-args=${JSON.stringify({ winID })} 292 .searchQuery=${this.searchQuery} 293 .bookmarkList=${this.bookmarkList} 294 ></view-opentabs-card> 295 ` 296 )} 297 </div> 298 `; 299 } 300 301 onSearchQuery(e) { 302 if (!this.recentBrowsing) { 303 Glean.firefoxviewNext.searchInitiatedSearch.record({ 304 page: "opentabs", 305 }); 306 } 307 this.searchQuery = e.detail.query; 308 } 309 310 onChangeSortOption(e) { 311 this.sortOption = e.target.value; 312 this.#setupTabChangeListener(); 313 if (!this.recentBrowsing) { 314 Services.prefs.setStringPref( 315 "browser.tabs.firefox-view.ui-state.opentabs.sort-option", 316 this.sortOption 317 ); 318 } 319 } 320 321 /** 322 * Render a template for the 'Recent browsing' page, which shows a shorter list of 323 * open tabs in the current window. 324 * 325 * @returns {TemplateResult} 326 * The recent browsing template. 327 */ 328 getRecentBrowsingTemplate() { 329 const tabs = this.openTabsTarget.getRecentTabs(); 330 return html`<view-opentabs-card 331 .tabs=${tabs} 332 .recentBrowsing=${true} 333 .paused=${this.paused} 334 .searchQuery=${this.searchQuery} 335 .bookmarkList=${this.bookmarkList} 336 ></view-opentabs-card>`; 337 } 338 339 handleEvent({ detail, type }) { 340 if (this.recentBrowsing && type === "MozInputSearch:search") { 341 this.onSearchQuery({ detail }); 342 return; 343 } 344 let windowIds; 345 switch (type) { 346 case "TabRecencyChange": 347 case "TabChange": 348 windowIds = detail.windowIds; 349 this._updateWindowList(); 350 this.bookmarkList.setTrackedUrls(this.#getAllTabUrls()); 351 break; 352 } 353 if (this.recentBrowsing) { 354 return; 355 } 356 if (windowIds?.length) { 357 // there were tab changes to one or more windows 358 for (let winId of windowIds) { 359 const cardForWin = this.shadowRoot.querySelector( 360 `view-opentabs-card[data-inner-id="${winId}"]` 361 ); 362 if (this.searchQuery) { 363 cardForWin?.updateSearchResults(); 364 } 365 cardForWin?.requestUpdate(); 366 } 367 } else { 368 let winId = window.windowGlobalChild.innerWindowId; 369 let cardForWin = this.shadowRoot.querySelector( 370 `view-opentabs-card[data-inner-id="${winId}"]` 371 ); 372 if (this.searchQuery) { 373 cardForWin?.updateSearchResults(); 374 } 375 } 376 } 377 378 async _updateWindowList() { 379 this.windows = this.openTabsTarget.currentWindows; 380 } 381 } 382 customElements.define("view-opentabs", OpenTabsInView); 383 384 /** 385 * A card which displays a list of open tabs for a window. 386 * 387 * @property {boolean} showMore 388 * Whether to force all tabs to be shown, regardless of available space. 389 * @property {MozTabbrowserTab[]} tabs 390 * The open tabs to show. 391 * @property {string} title 392 * The window title. 393 */ 394 class OpenTabsInViewCard extends ViewPageContent { 395 static properties = { 396 showMore: { type: Boolean }, 397 tabs: { type: Array }, 398 title: { type: String }, 399 recentBrowsing: { type: Boolean }, 400 searchQuery: { type: String }, 401 searchResults: { type: Array }, 402 showAll: { type: Boolean }, 403 cumulativeSearches: { type: Number }, 404 bookmarkList: { type: Object }, 405 }; 406 static MAX_TABS_FOR_COMPACT_HEIGHT = 7; 407 408 constructor() { 409 super(); 410 this.showMore = false; 411 this.tabs = []; 412 this.title = ""; 413 this.recentBrowsing = false; 414 this.devices = []; 415 this.searchQuery = ""; 416 this.searchResults = null; 417 this.showAll = false; 418 this.cumulativeSearches = 0; 419 this.controller = new lazy.OpenTabsController(this, {}); 420 } 421 422 static queries = { 423 cardEl: "card-container", 424 tabContextMenu: "view-opentabs-contextmenu", 425 tabList: "opentabs-tab-list", 426 }; 427 428 openContextMenu(e) { 429 let { originalEvent } = e.detail; 430 this.tabContextMenu.toggle({ 431 triggerNode: e.originalTarget, 432 originalEvent, 433 }); 434 } 435 436 getMaxTabsLength() { 437 if (this.recentBrowsing && !this.showAll) { 438 return MAX_TABS_FOR_RECENT_BROWSING; 439 } else if (this.classList.contains("height-limited") && !this.showMore) { 440 return OpenTabsInViewCard.MAX_TABS_FOR_COMPACT_HEIGHT; 441 } 442 return -1; 443 } 444 445 isShowAllLinkVisible() { 446 return ( 447 this.recentBrowsing && 448 this.searchQuery && 449 this.searchResults.length > MAX_TABS_FOR_RECENT_BROWSING && 450 !this.showAll 451 ); 452 } 453 454 isShowMoreLinkVisible() { 455 if (!this.classList.contains("height-limited")) { 456 return false; 457 } 458 459 let tabCount = (this.searchQuery ? this.searchResults : this.tabs).length; 460 return tabCount > OpenTabsInViewCard.MAX_TABS_FOR_COMPACT_HEIGHT; 461 } 462 463 toggleShowMore(event) { 464 if ( 465 event.type == "click" || 466 (event.type == "keydown" && event.code == "Enter") || 467 (event.type == "keydown" && event.code == "Space") 468 ) { 469 event.preventDefault(); 470 this.showMore = !this.showMore; 471 } 472 } 473 474 enableShowAll(event) { 475 if ( 476 event.type == "click" || 477 (event.type == "keydown" && event.code == "Enter") || 478 (event.type == "keydown" && event.code == "Space") 479 ) { 480 event.preventDefault(); 481 Glean.firefoxviewNext.searchShowAllShowallbutton.record({ 482 section: "opentabs", 483 }); 484 this.showAll = true; 485 } 486 } 487 488 onTabListRowClick(event) { 489 // Don't open pinned tab if mute/unmute indicator button selected 490 if ( 491 Array.from(event.explicitOriginalTarget.classList).includes( 492 "fxview-tab-row-pinned-media-button" 493 ) 494 ) { 495 return; 496 } 497 const tab = event.originalTarget.tabElement; 498 const browserWindow = tab.ownerGlobal; 499 browserWindow.focus(); 500 browserWindow.gBrowser.selectedTab = tab; 501 502 Glean.firefoxviewNext.openTabTabs.record({ 503 page: this.recentBrowsing ? "recentbrowsing" : "opentabs", 504 window: this.title || "Window 1 (Current)", 505 }); 506 if (this.searchQuery) { 507 Glean.firefoxview.cumulativeSearches[ 508 this.recentBrowsing ? "recentbrowsing" : "opentabs" 509 ].accumulateSingleSample(this.cumulativeSearches); 510 this.cumulativeSearches = 0; 511 } 512 } 513 514 closeTab(event) { 515 const tab = event.originalTarget.tabElement; 516 tab?.ownerGlobal.gBrowser.removeTab( 517 tab, 518 lazy.TabMetrics.userTriggeredContext() 519 ); 520 521 Glean.firefoxviewNext.closeOpenTabTabs.record(); 522 } 523 524 viewVisibleCallback() { 525 this.getRootNode().host.toggleVisibilityInCardContainer(true); 526 } 527 528 viewHiddenCallback() { 529 this.getRootNode().host.toggleVisibilityInCardContainer(true); 530 } 531 532 firstUpdated() { 533 this.getRootNode().host.toggleVisibilityInCardContainer(true); 534 } 535 536 render() { 537 return html` 538 <link 539 rel="stylesheet" 540 href="chrome://browser/content/firefoxview/firefoxview.css" 541 /> 542 <card-container 543 ?preserveCollapseState=${this.recentBrowsing} 544 shortPageName=${this.recentBrowsing ? "opentabs" : null} 545 ?showViewAll=${this.recentBrowsing} 546 ?removeBlockEndMargin=${!this.recentBrowsing} 547 > 548 ${when( 549 this.recentBrowsing, 550 () => 551 html`<h3 552 slot="header" 553 data-l10n-id="firefoxview-opentabs-header" 554 ></h3>`, 555 () => html`<h3 slot="header">${this.title}</h3>` 556 )} 557 <div class="fxview-tab-list-container" slot="main"> 558 <opentabs-tab-list 559 .hasPopup=${"menu"} 560 ?compactRows=${this.classList.contains("width-limited")} 561 @fxview-tab-list-primary-action=${this.onTabListRowClick} 562 @fxview-tab-list-secondary-action=${this.openContextMenu} 563 @fxview-tab-list-tertiary-action=${this.closeTab} 564 secondaryActionClass="options-button" 565 tertiaryActionClass="dismiss-button" 566 .maxTabsLength=${this.getMaxTabsLength()} 567 .tabItems=${this.searchResults || 568 this.controller.getTabListItems(this.tabs, this.recentBrowsing)} 569 .searchQuery=${this.searchQuery} 570 .pinnedTabsGridView=${!this.recentBrowsing} 571 ><view-opentabs-contextmenu slot="menu"></view-opentabs-contextmenu> 572 </opentabs-tab-list> 573 </div> 574 ${when( 575 this.recentBrowsing, 576 () => 577 html` <div 578 @click=${this.enableShowAll} 579 @keydown=${this.enableShowAll} 580 data-l10n-id="firefoxview-show-all" 581 ?hidden=${!this.isShowAllLinkVisible()} 582 slot="footer" 583 tabindex="0" 584 role="link" 585 ></div>`, 586 () => 587 html` <div 588 @click=${this.toggleShowMore} 589 @keydown=${this.toggleShowMore} 590 data-l10n-id=${this.showMore 591 ? "firefoxview-show-less" 592 : "firefoxview-show-more"} 593 ?hidden=${!this.isShowMoreLinkVisible()} 594 slot="footer" 595 tabindex="0" 596 role="link" 597 ></div>` 598 )} 599 </card-container> 600 `; 601 } 602 603 willUpdate(changedProperties) { 604 if (changedProperties.has("searchQuery")) { 605 this.showAll = false; 606 this.cumulativeSearches = this.searchQuery 607 ? this.cumulativeSearches + 1 608 : 0; 609 } 610 if (changedProperties.has("searchQuery") || changedProperties.has("tabs")) { 611 this.updateSearchResults(); 612 } 613 } 614 615 updateSearchResults() { 616 this.searchResults = this.searchQuery 617 ? searchTabList( 618 this.searchQuery, 619 this.controller.getTabListItems(this.tabs) 620 ) 621 : null; 622 } 623 624 updated() { 625 this.updateBookmarkStars(); 626 } 627 628 async updateBookmarkStars() { 629 const tabItems = [...this.tabList.tabItems]; 630 for (const row of tabItems) { 631 const isBookmark = await this.bookmarkList.isBookmark(row.url); 632 if (isBookmark && !row.indicators.includes("bookmark")) { 633 row.indicators.push("bookmark"); 634 } 635 if (!isBookmark && row.indicators.includes("bookmark")) { 636 row.indicators = row.indicators.filter(i => i !== "bookmark"); 637 } 638 row.primaryL10nId = this.controller.getPrimaryL10nId( 639 this.isRecentBrowsing, 640 row.indicators 641 ); 642 } 643 this.tabList.tabItems = tabItems; 644 } 645 } 646 customElements.define("view-opentabs-card", OpenTabsInViewCard); 647 648 /** 649 * A context menu of actions available for open tab list items. 650 */ 651 class OpenTabsContextMenu extends MozLitElement { 652 static properties = { 653 devices: { type: Array }, 654 triggerNode: { hasChanged: () => true, type: Object }, 655 }; 656 657 static queries = { 658 panelList: "panel-list", 659 }; 660 661 constructor() { 662 super(); 663 this.triggerNode = null; 664 this.boundObserve = (...args) => this.observe(...args); 665 this.devices = []; 666 } 667 668 get logger() { 669 return getLogger("OpenTabsContextMenu"); 670 } 671 672 get ownerViewPage() { 673 return this.ownerDocument.querySelector("view-opentabs"); 674 } 675 676 connectedCallback() { 677 super.connectedCallback(); 678 this.fetchDevicesPromise = this.fetchDevices(); 679 Services.obs.addObserver(this.boundObserve, TOPIC_DEVICELIST_UPDATED); 680 Services.obs.addObserver(this.boundObserve, TOPIC_DEVICESTATE_CHANGED); 681 } 682 683 disconnectedCallback() { 684 super.disconnectedCallback(); 685 Services.obs.removeObserver(this.boundObserve, TOPIC_DEVICELIST_UPDATED); 686 Services.obs.removeObserver(this.boundObserve, TOPIC_DEVICESTATE_CHANGED); 687 } 688 689 observe(_subject, topic, _data) { 690 if ( 691 topic == TOPIC_DEVICELIST_UPDATED || 692 topic == TOPIC_DEVICESTATE_CHANGED 693 ) { 694 this.fetchDevicesPromise = this.fetchDevices(); 695 } 696 } 697 698 async fetchDevices() { 699 const currentWindow = this.ownerViewPage.getWindow(); 700 if (currentWindow?.gSync) { 701 try { 702 await lazy.fxAccounts.device.refreshDeviceList(); 703 } catch (e) { 704 this.logger.warn("Could not refresh the FxA device list", e); 705 } 706 this.devices = currentWindow.gSync.getSendTabTargets(); 707 } 708 } 709 710 async toggle({ triggerNode, originalEvent }) { 711 if (this.panelList?.open) { 712 // the menu will close so avoid all the other work to update its contents 713 this.panelList.toggle(originalEvent); 714 return; 715 } 716 this.triggerNode = triggerNode; 717 await this.fetchDevicesPromise; 718 await this.getUpdateComplete(); 719 this.panelList.toggle(originalEvent); 720 } 721 722 copyLink(e) { 723 lazy.BrowserUtils.copyLink(this.triggerNode.url, this.triggerNode.title); 724 this.ownerViewPage.recordContextMenuTelemetry("copy-link", e); 725 } 726 727 closeTab(e) { 728 const tab = this.triggerNode.tabElement; 729 tab?.ownerGlobal.gBrowser.removeTab(tab); 730 this.ownerViewPage.recordContextMenuTelemetry("close-tab", e); 731 } 732 733 pinTab(e) { 734 const tab = this.triggerNode.tabElement; 735 tab?.ownerGlobal.gBrowser.pinTab(tab); 736 this.ownerViewPage.recordContextMenuTelemetry("pin-tab", e); 737 } 738 739 unpinTab(e) { 740 const tab = this.triggerNode.tabElement; 741 tab?.ownerGlobal.gBrowser.unpinTab(tab); 742 this.ownerViewPage.recordContextMenuTelemetry("unpin-tab", e); 743 } 744 745 toggleAudio(e) { 746 const tab = this.triggerNode.tabElement; 747 tab.toggleMuteAudio(); 748 this.ownerViewPage.recordContextMenuTelemetry( 749 `${ 750 this.triggerNode.indicators.includes("muted") ? "unmute" : "mute" 751 }-tab`, 752 e 753 ); 754 } 755 756 moveTabsToStart(e) { 757 const tab = this.triggerNode.tabElement; 758 tab?.ownerGlobal.gBrowser.moveTabsToStart(tab); 759 this.ownerViewPage.recordContextMenuTelemetry("move-tab-start", e); 760 } 761 762 moveTabsToEnd(e) { 763 const tab = this.triggerNode.tabElement; 764 tab?.ownerGlobal.gBrowser.moveTabsToEnd(tab); 765 this.ownerViewPage.recordContextMenuTelemetry("move-tab-end", e); 766 } 767 768 moveTabsToWindow(e) { 769 const tab = this.triggerNode.tabElement; 770 tab?.ownerGlobal.gBrowser.replaceTabsWithWindow(tab); 771 this.ownerViewPage.recordContextMenuTelemetry("move-tab-window", e); 772 } 773 774 moveMenuTemplate() { 775 const tab = this.triggerNode?.tabElement; 776 if (!tab) { 777 return null; 778 } 779 const browserWindow = tab.ownerGlobal; 780 const tabs = browserWindow?.gBrowser.visibleTabs || []; 781 const position = tabs.indexOf(tab); 782 783 return html` 784 <panel-list slot="submenu" id="move-tab-menu"> 785 ${position > 0 786 ? html`<panel-item 787 @click=${this.moveTabsToStart} 788 data-l10n-id="fxviewtabrow-move-tab-start" 789 data-l10n-attrs="accesskey" 790 ></panel-item>` 791 : null} 792 ${position < tabs.length - 1 793 ? html`<panel-item 794 @click=${this.moveTabsToEnd} 795 data-l10n-id="fxviewtabrow-move-tab-end" 796 data-l10n-attrs="accesskey" 797 ></panel-item>` 798 : null} 799 <panel-item 800 @click=${this.moveTabsToWindow} 801 data-l10n-id="fxviewtabrow-move-tab-window" 802 data-l10n-attrs="accesskey" 803 ></panel-item> 804 </panel-list> 805 `; 806 } 807 808 async sendTabToDevice(e) { 809 let deviceId = e.target.getAttribute("device-id"); 810 let device = this.devices.find(dev => dev.id == deviceId); 811 const viewPage = this.ownerViewPage; 812 viewPage.recordContextMenuTelemetry("send-tab-device", e); 813 814 if (device && this.triggerNode) { 815 await viewPage 816 .getWindow() 817 .gSync.sendTabToDevice( 818 this.triggerNode.url, 819 [device], 820 this.triggerNode.title 821 ); 822 } 823 } 824 825 sendTabTemplate() { 826 return html` <panel-list slot="submenu" id="send-tab-menu"> 827 ${this.devices.map(device => { 828 return html` 829 <panel-item @click=${this.sendTabToDevice} device-id=${device.id} 830 >${device.name}</panel-item 831 > 832 `; 833 })} 834 </panel-list>`; 835 } 836 837 render() { 838 const tab = this.triggerNode?.tabElement; 839 if (!tab) { 840 return null; 841 } 842 843 return html` 844 <link 845 rel="stylesheet" 846 href="chrome://browser/content/firefoxview/firefoxview.css" 847 /> 848 <panel-list data-tab-type="opentabs"> 849 <panel-item 850 data-l10n-id="fxviewtabrow-move-tab" 851 data-l10n-attrs="accesskey" 852 submenu="move-tab-menu" 853 >${this.moveMenuTemplate()}</panel-item 854 > 855 <panel-item 856 data-l10n-id=${tab.pinned 857 ? "fxviewtabrow-unpin-tab" 858 : "fxviewtabrow-pin-tab"} 859 data-l10n-attrs="accesskey" 860 @click=${tab.pinned ? this.unpinTab : this.pinTab} 861 ></panel-item> 862 <panel-item 863 data-l10n-id=${tab.hasAttribute("muted") 864 ? "fxviewtabrow-unmute-tab" 865 : "fxviewtabrow-mute-tab"} 866 data-l10n-attrs="accesskey" 867 @click=${this.toggleAudio} 868 ></panel-item> 869 <hr /> 870 <panel-item 871 data-l10n-id="fxviewtabrow-copy-link" 872 data-l10n-attrs="accesskey" 873 @click=${this.copyLink} 874 ></panel-item> 875 ${this.devices.length >= 1 876 ? html`<panel-item 877 data-l10n-id="fxviewtabrow-send-to-device" 878 data-l10n-attrs="accesskey" 879 submenu="send-tab-menu" 880 >${this.sendTabTemplate()}</panel-item 881 >` 882 : null} 883 </panel-list> 884 `; 885 } 886 } 887 customElements.define("view-opentabs-contextmenu", OpenTabsContextMenu);