forked from mirrors/gecko-dev
Initial open tabs implementation in Firefox View Next. Note that this only lists out the open tabs. It does not yet provide the ability to switch to an open tab or window. See bug description for a full list of expected behaviors. [[ https://bugzilla.mozilla.org/attachment.cgi?id=9324749&action=diff | This patch ]] from @mkaply was used as a starting point. https://treeherder.mozilla.org/jobs?repo=try&revision=789cd12b31cc76c4f9532560fd10727af6b264d5 Differential Revision: https://phabricator.services.mozilla.com/D178970
318 lines
9.6 KiB
JavaScript
318 lines
9.6 KiB
JavaScript
/* 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,
|
|
map,
|
|
when,
|
|
} from "chrome://global/content/vendor/lit.all.mjs";
|
|
import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
|
|
import { ViewPage } from "./viewpage.mjs";
|
|
// eslint-disable-next-line import/no-unassigned-import
|
|
import "chrome://browser/content/firefoxview/card-container.mjs";
|
|
// eslint-disable-next-line import/no-unassigned-import
|
|
import "chrome://browser/content/firefoxview/fxview-tab-list.mjs";
|
|
|
|
const { XPCOMUtils } = ChromeUtils.importESModule(
|
|
"resource://gre/modules/XPCOMUtils.sys.mjs"
|
|
);
|
|
|
|
const lazy = {};
|
|
|
|
XPCOMUtils.defineLazyModuleGetters(lazy, {
|
|
EveryWindow: "resource:///modules/EveryWindow.jsm",
|
|
});
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
|
|
});
|
|
|
|
/**
|
|
* A collection of open tabs grouped by window.
|
|
*
|
|
* @property {Map<Window, MozTabbrowserTab[]>} windows
|
|
* A mapping of windows to their respective list of open tabs.
|
|
*/
|
|
class OpenTabsInView extends ViewPage {
|
|
static properties = {
|
|
windows: { type: Map },
|
|
};
|
|
static TAB_ATTRS_TO_WATCH = Object.freeze(["image", "label"]);
|
|
|
|
constructor() {
|
|
super();
|
|
this.everyWindowCallbackId = `firefoxview-${Services.uuid.generateUUID()}`;
|
|
this.windows = new Map();
|
|
}
|
|
|
|
connectedCallback() {
|
|
super.connectedCallback();
|
|
lazy.EveryWindow.registerCallback(
|
|
this.everyWindowCallbackId,
|
|
win => {
|
|
if (
|
|
win.gBrowser &&
|
|
!lazy.PrivateBrowsingUtils.isWindowPrivate(win) &&
|
|
!win.closed
|
|
) {
|
|
const { tabContainer } = win.gBrowser;
|
|
tabContainer.addEventListener("TabAttrModified", this);
|
|
tabContainer.addEventListener("TabClose", this);
|
|
tabContainer.addEventListener("TabMove", this);
|
|
tabContainer.addEventListener("TabOpen", this);
|
|
tabContainer.addEventListener("TabPinned", this);
|
|
tabContainer.addEventListener("TabUnpinned", this);
|
|
this._updateOpenTabsList();
|
|
}
|
|
},
|
|
win => {
|
|
if (win.gBrowser && !lazy.PrivateBrowsingUtils.isWindowPrivate(win)) {
|
|
const { tabContainer } = win.gBrowser;
|
|
tabContainer.removeEventListener("TabAttrModified", this);
|
|
tabContainer.removeEventListener("TabClose", this);
|
|
tabContainer.removeEventListener("TabMove", this);
|
|
tabContainer.removeEventListener("TabOpen", this);
|
|
tabContainer.removeEventListener("TabPinned", this);
|
|
tabContainer.removeEventListener("TabUnpinned", this);
|
|
this._updateOpenTabsList();
|
|
}
|
|
}
|
|
);
|
|
this._updateOpenTabsList();
|
|
}
|
|
|
|
disconnectedCallback() {
|
|
lazy.EveryWindow.unregisterCallback(this.everyWindowCallbackId);
|
|
}
|
|
|
|
render() {
|
|
if (!this.selectedTab && !this.overview) {
|
|
return null;
|
|
}
|
|
if (this.overview) {
|
|
return this._getOverviewTemplate();
|
|
}
|
|
const { window: currentWindow } =
|
|
window.browsingContext.embedderWindowGlobal.browsingContext;
|
|
let currentWindowIndex, currentWindowTabs;
|
|
let index = 1;
|
|
const otherWindows = [];
|
|
this.windows.forEach((tabs, win) => {
|
|
if (win === currentWindow) {
|
|
currentWindowIndex = index++;
|
|
currentWindowTabs = tabs;
|
|
} else {
|
|
otherWindows.push([index++, tabs, win]);
|
|
}
|
|
});
|
|
const cardClasses = classMap({
|
|
"height-limited": this.windows.size > 3,
|
|
"width-limited": this.windows.size > 1,
|
|
});
|
|
return html`
|
|
<link
|
|
rel="stylesheet"
|
|
href="chrome://browser/content/firefoxview/view-opentabs.css"
|
|
/>
|
|
<div
|
|
class=${classMap({
|
|
"view-opentabs-card-container": true,
|
|
"one-column": this.windows.size <= 1,
|
|
"two-columns": this.windows.size === 2,
|
|
"three-columns": this.windows.size >= 3,
|
|
})}
|
|
>
|
|
${when(
|
|
currentWindowIndex && currentWindowTabs,
|
|
() =>
|
|
html`
|
|
<view-opentabs-card
|
|
class=${cardClasses}
|
|
.tabs=${currentWindowTabs}
|
|
data-inner-id="${currentWindow.windowGlobalChild.innerWindowId}"
|
|
data-l10n-id="firefoxview-opentabs-current-window-header"
|
|
data-l10n-args="${JSON.stringify({
|
|
winID: currentWindowIndex,
|
|
})}"
|
|
></view-opentabs-card>
|
|
`
|
|
)}
|
|
${map(
|
|
otherWindows,
|
|
([winID, tabs, win]) => html`
|
|
<view-opentabs-card
|
|
class=${cardClasses}
|
|
.tabs=${tabs}
|
|
data-inner-id="${win.windowGlobalChild.innerWindowId}"
|
|
data-l10n-id="firefoxview-opentabs-window-header"
|
|
data-l10n-args="${JSON.stringify({ winID })}"
|
|
></view-opentabs-card>
|
|
`
|
|
)}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* Render a template for the overview page, which shows a single list of
|
|
* recently accessed tabs, rather than a list of tabs per window.
|
|
*
|
|
* @returns {TemplateResult}
|
|
* The overview template.
|
|
*/
|
|
_getOverviewTemplate() {
|
|
const recentTabs = Array.from(this.windows.values())
|
|
.flat()
|
|
.sort((a, b) => b.lastAccessed - a.lastAccessed);
|
|
return html`<card-container preserveCollapseState viewAllPage="opentabs">
|
|
<h2 data-l10n-id="firefoxview-opentabs-header" slot="header"></h2>
|
|
<fxview-tab-list
|
|
@fxview-tab-list-primary-action=${onTabListRowClick}
|
|
maxTabsLength="5"
|
|
slot="main"
|
|
.tabItems=${getTabListItems(recentTabs)}
|
|
></fxview-tab-list>
|
|
</card-container>`;
|
|
}
|
|
|
|
handleEvent({ detail, target, type }) {
|
|
const win = target.ownerGlobal;
|
|
const tabs = this.windows.get(win);
|
|
switch (type) {
|
|
case "TabAttrModified":
|
|
if (
|
|
!detail.changed.some(attr =>
|
|
OpenTabsInView.TAB_ATTRS_TO_WATCH.includes(attr)
|
|
)
|
|
) {
|
|
// We don't care about this attr, bail out to avoid change detection.
|
|
return;
|
|
}
|
|
break;
|
|
case "TabClose":
|
|
tabs.splice(target._tPos, 1);
|
|
break;
|
|
case "TabMove":
|
|
[tabs[detail], tabs[target._tPos]] = [tabs[target._tPos], tabs[detail]];
|
|
break;
|
|
case "TabOpen":
|
|
tabs.splice(target._tPos, 0, target);
|
|
break;
|
|
case "TabPinned":
|
|
case "TabUnpinned":
|
|
this.windows.set(win, [...win.gBrowser.tabs]);
|
|
break;
|
|
}
|
|
this.requestUpdate();
|
|
if (!this.overview) {
|
|
const selector = `view-opentabs-card[data-inner-id="${win.windowGlobalChild.innerWindowId}"]`;
|
|
this.shadowRoot.querySelector(selector)?.requestUpdate();
|
|
}
|
|
}
|
|
|
|
_updateOpenTabsList() {
|
|
this.windows = this._getOpenTabsPerWindow();
|
|
}
|
|
|
|
/**
|
|
* Get a list of open tabs for each window.
|
|
*
|
|
* @returns {Map<Window, MozTabbrowserTab[]>}
|
|
*/
|
|
_getOpenTabsPerWindow() {
|
|
return new Map(
|
|
Array.from(Services.wm.getEnumerator("navigator:browser"))
|
|
.filter(
|
|
win =>
|
|
win.gBrowser &&
|
|
!lazy.PrivateBrowsingUtils.isWindowPrivate(win) &&
|
|
!win.closed
|
|
)
|
|
.map(win => [win, [...win.gBrowser.tabs]])
|
|
);
|
|
}
|
|
}
|
|
customElements.define("view-opentabs", OpenTabsInView);
|
|
|
|
/**
|
|
* A card which displays a list of open tabs for a window.
|
|
*
|
|
* @property {boolean} showMore
|
|
* Whether to force all tabs to be shown, regardless of available space.
|
|
* @property {MozTabbrowserTab[]} tabs
|
|
* The open tabs to show.
|
|
* @property {string} title
|
|
* The window title.
|
|
*/
|
|
class OpenTabsInViewCard extends MozLitElement {
|
|
static properties = {
|
|
showMore: { type: Boolean },
|
|
tabs: { type: Array },
|
|
title: { type: String },
|
|
};
|
|
static MAX_TABS_FOR_COMPACT_HEIGHT = 7;
|
|
|
|
constructor() {
|
|
super();
|
|
this.showMore = false;
|
|
this.tabs = [];
|
|
this.title = "";
|
|
}
|
|
|
|
render() {
|
|
return html`
|
|
<card-container>
|
|
<h2 slot="header">${this.title}</h2>
|
|
<div class="fxview-tab-list-container" slot="main">
|
|
<fxview-tab-list
|
|
?compactRows=${this.classList.contains("width-limited")}
|
|
@fxview-tab-list-primary-action=${onTabListRowClick}
|
|
.maxTabsLength=${ifDefined(
|
|
this.classList.contains("height-limited") && !this.showMore
|
|
? OpenTabsInViewCard.MAX_TABS_FOR_COMPACT_HEIGHT
|
|
: null
|
|
)}
|
|
.tabItems=${ifDefined(getTabListItems(this.tabs))}
|
|
></fxview-tab-list>
|
|
</div>
|
|
<div
|
|
@click=${() => (this.showMore = !this.showMore)}
|
|
data-l10n-id="${this.showMore
|
|
? "firefoxview-show-less"
|
|
: "firefoxview-show-more"}"
|
|
?hidden=${!this.classList.contains("height-limited") ||
|
|
this.tabs.length <= OpenTabsInViewCard.MAX_TABS_FOR_COMPACT_HEIGHT}
|
|
slot="footer"
|
|
></div>
|
|
</card-container>
|
|
`;
|
|
}
|
|
}
|
|
customElements.define("view-opentabs-card", OpenTabsInViewCard);
|
|
|
|
/**
|
|
* Convert a list of tabs into the format expected by the fxview-tab-list
|
|
* component.
|
|
*
|
|
* @param {MozTabbrowserTab[]} tabs
|
|
* Tabs to format.
|
|
* @returns {object[]}
|
|
* Formatted objects.
|
|
*/
|
|
function getTabListItems(tabs) {
|
|
return tabs
|
|
?.filter(tab => !tab.closing && !tab.hidden && !tab.pinned)
|
|
.map(tab => ({
|
|
icon: tab.getAttribute("image"),
|
|
title: tab.label,
|
|
time: tab.lastAccessed,
|
|
url: tab.linkedBrowser?.currentURI?.spec,
|
|
primaryL10nId: "firefoxview-opentabs-focus-tab",
|
|
}));
|
|
}
|
|
|
|
// TODO: Bug 1831118 - Open tabs additional functionality
|
|
function onTabListRowClick(event) {}
|