/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { classMap, html, ifDefined, styleMap, when, } from "chrome://global/content/vendor/lit.all.mjs"; import { FxviewTabListBase, FxviewTabRowBase, } from "chrome://browser/content/firefoxview/fxview-tab-list.mjs"; // eslint-disable-next-line import/no-unassigned-import import "chrome://global/content/elements/moz-button.mjs"; const lazy = {}; let XPCOMUtils; XPCOMUtils = ChromeUtils.importESModule( "resource://gre/modules/XPCOMUtils.sys.mjs" ).XPCOMUtils; XPCOMUtils.defineLazyPreferenceGetter( lazy, "virtualListEnabledPref", "browser.firefox-view.virtual-list.enabled" ); /** * A list of clickable tab items * * @property {boolean} pinnedTabsGridView - Whether to show pinned tabs in a grid view */ export class OpenTabsTabList extends FxviewTabListBase { constructor() { super(); this.pinnedTabsGridView = false; this.pinnedTabs = []; this.unpinnedTabs = []; } static properties = { pinnedTabsGridView: { type: Boolean }, }; static queries = { ...FxviewTabListBase.queries, rowEls: { all: "opentabs-tab-row", }, }; willUpdate(changes) { this.activeIndex = Math.min( Math.max(this.activeIndex, 0), this.tabItems.length - 1 ); if (changes.has("dateTimeFormat") || changes.has("updatesPaused")) { this.clearIntervalTimer(); if (!this.updatesPaused && this.dateTimeFormat == "relative") { this.startIntervalTimer(); this.onIntervalUpdate(); } } // Move pinned tabs to the beginning of the list if (this.pinnedTabsGridView) { // Can set maxTabsLength to -1 to have no max this.unpinnedTabs = this.tabItems.filter( tab => !tab.indicators.includes("pinned") ); this.pinnedTabs = this.tabItems.filter(tab => tab.indicators.includes("pinned") ); if (this.maxTabsLength > 0) { this.unpinnedTabs = this.unpinnedTabs.slice(0, this.maxTabsLength); } this.tabItems = [...this.pinnedTabs, ...this.unpinnedTabs]; } else if (this.maxTabsLength > 0) { this.tabItems = this.tabItems.slice(0, this.maxTabsLength); } } /** * Focuses the expected element (either the link or button) within fxview-tab-row * The currently focused/active element ID within a row is stored in this.currentActiveElementId */ handleFocusElementInRow(e) { let fxviewTabRow = e.target; if (e.code == "ArrowUp") { // Focus either the link or button of the previous row based on this.currentActiveElementId e.preventDefault(); if ( (this.pinnedTabsGridView && this.activeIndex >= this.pinnedTabs.length) || !this.pinnedTabsGridView ) { this.focusPrevRow(); } } else if (e.code == "ArrowDown") { // Focus either the link or button of the next row based on this.currentActiveElementId e.preventDefault(); if ( this.pinnedTabsGridView && this.activeIndex < this.pinnedTabs.length ) { this.focusIndex(this.pinnedTabs.length); } else { this.focusNextRow(); } } else if (e.code == "ArrowRight") { // Focus either the link or the button in the current row and // set this.currentActiveElementId to that element's ID e.preventDefault(); if (document.dir == "rtl") { fxviewTabRow.moveFocusLeft(); } else { fxviewTabRow.moveFocusRight(); } } else if (e.code == "ArrowLeft") { // Focus either the link or the button in the current row and // set this.currentActiveElementId to that element's ID e.preventDefault(); if (document.dir == "rtl") { fxviewTabRow.moveFocusRight(); } else { fxviewTabRow.moveFocusLeft(); } } } async focusIndex(index) { // Focus link or button of item if ( ((this.pinnedTabsGridView && index > this.pinnedTabs.length) || !this.pinnedTabsGridView) && lazy.virtualListEnabledPref ) { let row = this.rootVirtualListEl.getItem(index - this.pinnedTabs.length); if (!row) { return; } let subList = this.rootVirtualListEl.getSubListForItem( index - this.pinnedTabs.length ); if (!subList) { return; } this.activeIndex = index; // In Bug 1866845, these manual updates to the sublists should be removed // and scrollIntoView() should also be iterated on so that we aren't constantly // moving the focused item to the center of the viewport for (const sublist of Array.from(this.rootVirtualListEl.children)) { await sublist.requestUpdate(); await sublist.updateComplete; } row.scrollIntoView({ block: "center" }); row.focus(); } else if (index >= 0 && index < this.rowEls?.length) { this.rowEls[index].focus(); this.activeIndex = index; } } #getTabListWrapperClasses() { let wrapperClasses = ["fxview-tab-list"]; let tabsToCheck = this.pinnedTabsGridView ? this.unpinnedTabs : this.tabItems; if (tabsToCheck.some(tab => tab.containerObj)) { wrapperClasses.push(`hasContainerTab`); } return wrapperClasses; } itemTemplate = (tabItem, i) => { let time; if (tabItem.time || tabItem.closedAt) { let stringTime = (tabItem.time || tabItem.closedAt).toString(); // Different APIs return time in different units, so we use // the length to decide if it's milliseconds or nanoseconds. if (stringTime.length === 16) { time = (tabItem.time || tabItem.closedAt) / 1000; } else { time = tabItem.time || tabItem.closedAt; } } return html``; }; render() { if (this.searchQuery && this.tabItems.length === 0) { return this.emptySearchResultsTemplate(); } return html` ${this.stylesheets()} ${when( this.pinnedTabsGridView && this.pinnedTabs.length, () => html`
${this.pinnedTabs.map((tabItem, i) => this.customItemTemplate ? this.customItemTemplate(tabItem, i) : this.itemTemplate(tabItem, i) )}
` )}
${when( lazy.virtualListEnabledPref, () => html` `, () => html`${this.tabItems.map((tabItem, i) => this.itemTemplate(tabItem, i) )}` )}
`; } } customElements.define("opentabs-tab-list", OpenTabsTabList); /** * A tab item that displays favicon, title, url, and time of last access * * @property {object} containerObj - Info about an open tab's container if within one * @property {string} indicators - An array of tab indicators if any are present * @property {boolean} pinnedTabsGridView - Whether the show pinned tabs in a grid view */ export class OpenTabsTabRow extends FxviewTabRowBase { constructor() { super(); this.indicators = []; this.pinnedTabsGridView = false; } static properties = { ...FxviewTabRowBase.properties, containerObj: { type: Object }, indicators: { type: Array }, pinnedTabsGridView: { type: Boolean }, }; static queries = { ...FxviewTabRowBase.queries, mediaButtonEl: "#fxview-tab-row-media-button", pinnedTabButtonEl: "moz-button#fxview-tab-row-main", }; connectedCallback() { super.connectedCallback(); this.addEventListener("keydown", this.handleKeydown); } disconnectedCallback() { super.disconnectedCallback(); this.removeEventListener("keydown", this.handleKeydown); } handleKeydown(e) { if ( this.active && this.pinnedTabsGridView && this.indicators?.includes("pinned") && e.key === "m" && e.ctrlKey ) { this.muteOrUnmuteTab(); } } moveFocusRight() { let tabList = this.getRootNode().host; if (this.pinnedTabsGridView && this.indicators?.includes("pinned")) { tabList.focusNextRow(); } else if ( (this.indicators?.includes("soundplaying") || this.indicators?.includes("muted")) && this.currentActiveElementId === "fxview-tab-row-main" ) { this.focusMediaButton(); } else if ( this.currentActiveElementId === "fxview-tab-row-media-button" || this.currentActiveElementId === "fxview-tab-row-main" ) { this.focusSecondaryButton(); } else if ( this.tertiaryButtonEl && this.currentActiveElementId === "fxview-tab-row-secondary-button" ) { this.focusTertiaryButton(); } } moveFocusLeft() { let tabList = this.getRootNode().host; if ( this.pinnedTabsGridView && (this.indicators?.includes("pinned") || (tabList.currentActiveElementId === "fxview-tab-row-main" && tabList.activeIndex === tabList.pinnedTabs.length)) ) { tabList.focusPrevRow(); } else if ( tabList.currentActiveElementId === "fxview-tab-row-tertiary-button" ) { this.focusSecondaryButton(); } else if ( (this.indicators?.includes("soundplaying") || this.indicators?.includes("muted")) && tabList.currentActiveElementId === "fxview-tab-row-secondary-button" ) { this.focusMediaButton(); } else { this.focusLink(); } } focusMediaButton() { let tabList = this.getRootNode().host; this.mediaButtonEl.focus(); tabList.currentActiveElementId = this.mediaButtonEl.id; } #secondaryActionHandler(event) { if ( (this.pinnedTabsGridView && this.indicators?.includes("pinned") && event.type == "contextmenu") || (event.type == "click" && event.detail && !event.altKey) || // detail=0 is from keyboard (event.type == "click" && !event.detail) ) { event.preventDefault(); this.dispatchEvent( new CustomEvent("fxview-tab-list-secondary-action", { bubbles: true, composed: true, detail: { originalEvent: event, item: this }, }) ); } } #faviconTemplate() { return html` ${when( this.pinnedTabsGridView && this.indicators?.includes("pinned") && (this.indicators?.includes("muted") || this.indicators?.includes("soundplaying")), () => html` ` )} `; } #getContainerClasses() { let containerClasses = ["fxview-tab-row-container-indicator", "icon"]; if (this.containerObj) { let { icon, color } = this.containerObj; containerClasses.push(`identity-icon-${icon}`); containerClasses.push(`identity-color-${color}`); } return containerClasses; } muteOrUnmuteTab(e) { e?.preventDefault(); // If the tab has no sound playing, the mute/unmute button will be removed when toggled. // We should move the focus to the right in that case. This does not apply to pinned tabs // on the Open Tabs page. let shouldMoveFocus = (!this.pinnedTabsGridView || (!this.indicators.includes("pinned") && this.pinnedTabsGridView)) && this.mediaButtonEl && !this.indicators.includes("soundplaying") && this.currentActiveElementId === "fxview-tab-row-media-button"; // detail=0 is from keyboard if (e?.type == "click" && !e?.detail && shouldMoveFocus) { if (document.dir == "rtl") { this.moveFocusLeft(); } else { this.moveFocusRight(); } } this.tabElement.toggleMuteAudio(); } #mediaButtonTemplate() { return html`${when( this.indicators?.includes("soundplaying") || this.indicators?.includes("muted"), () => html``, () => html`` )}`; } #containerIndicatorTemplate() { let tabList = this.getRootNode().host; let tabsToCheck = tabList.pinnedTabsGridView ? tabList.unpinnedTabs : tabList.tabItems; return html`${when( tabsToCheck.some(tab => tab.containerObj), () => html`` )}`; } #pinnedTabItemTemplate() { return html` ${this.#faviconTemplate()} `; } #unpinnedTabItemTemplate() { return html` ${this.#faviconTemplate()} ${this.titleTemplate()} ${when( !this.compact, () => html`${this.#containerIndicatorTemplate()} ${this.urlTemplate()} ${this.dateTemplate()} ${this.timeTemplate()}` )} ${this.#mediaButtonTemplate()} ${this.secondaryButtonTemplate()} ${this.tertiaryButtonTemplate()}`; } render() { return html` ${this.stylesheets()} ${when( this.containerObj, () => html` ` )} ${when( this.pinnedTabsGridView && this.indicators?.includes("pinned"), this.#pinnedTabItemTemplate.bind(this), this.#unpinnedTabItemTemplate.bind(this) )} `; } } customElements.define("opentabs-tab-row", OpenTabsTabRow);