/* 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/. */
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
});
import {
formatURIForDisplay,
convertTimestamp,
getImageUrl,
onToggleContainer,
NOW_THRESHOLD_MS,
} from "./helpers.mjs";
import {
html,
ifDefined,
styleMap,
} from "chrome://global/content/vendor/lit.all.mjs";
import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
const { XPCOMUtils } = ChromeUtils.importESModule(
"resource://gre/modules/XPCOMUtils.sys.mjs"
);
const SS_NOTIFY_CLOSED_OBJECTS_CHANGED = "sessionstore-closed-objects-changed";
const SS_NOTIFY_BROWSER_SHUTDOWN_FLUSH = "sessionstore-browser-shutdown-flush";
const UI_OPEN_STATE =
"browser.tabs.firefox-view.ui-state.recently-closed-tabs.open";
function getWindow() {
return window.browsingContext.embedderWindowGlobal.browsingContext.window;
}
class RecentlyClosedTabsList extends MozLitElement {
constructor() {
super();
this.maxTabsLength = 25;
this.recentlyClosedTabs = [];
this.lastFocusedIndex = -1;
// The recency timestamp update period is stored in a pref to allow tests to easily change it
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"timeMsPref",
"browser.tabs.firefox-view.updateTimeMs",
NOW_THRESHOLD_MS,
timeMsPref => {
clearInterval(this.intervalID);
this.intervalID = setInterval(() => this.requestUpdate(), timeMsPref);
this.requestUpdate();
}
);
}
createRenderRoot() {
return this;
}
static queries = {
tabsList: "ol",
timeElements: { all: "span.closed-tab-li-time" },
};
get fluentStrings() {
if (!this._fluentStrings) {
this._fluentStrings = new Localization(["browser/firefoxView.ftl"], true);
}
return this._fluentStrings;
}
connectedCallback() {
super.connectedCallback();
this.intervalID = setInterval(() => this.requestUpdate(), lazy.timeMsPref);
}
disconnectedCallback() {
clearInterval(this.intervalID);
}
getTabStateValue(tab, key) {
let value = "";
const tabEntries = tab.state.entries;
const activeIndex = tab.state.index - 1;
if (activeIndex >= 0 && tabEntries[activeIndex]) {
value = tabEntries[activeIndex][key];
}
return value;
}
openTabAndUpdate(event) {
if (
(event.type == "click" && !event.altKey) ||
(event.type == "keydown" && event.code == "Enter") ||
(event.type == "keydown" && event.code == "Space")
) {
const item = event.target.closest(".closed-tab-li");
// only used for telemetry
const position = [...this.tabsList.children].indexOf(item) + 1;
const closedId = item.dataset.tabid;
lazy.SessionStore.undoCloseById(closedId);
// record telemetry
let tabClosedAt = parseInt(
item.querySelector(".closed-tab-li-time").getAttribute("data-timestamp")
);
let now = Date.now();
let deltaSeconds = (now - tabClosedAt) / 1000;
Services.telemetry.recordEvent(
"firefoxview",
"recently_closed",
"tabs",
null,
{
position: position.toString(),
delta: deltaSeconds.toString(),
}
);
}
}
dismissTabAndUpdate(event) {
event.preventDefault();
const item = event.target.closest(".closed-tab-li");
this.dismissTabAndUpdateForElement(item);
}
dismissTabAndUpdateForElement(item) {
let recentlyClosedList = lazy.SessionStore.getClosedTabDataForWindow(
getWindow()
);
let closedTabIndex = recentlyClosedList.findIndex(closedTab => {
return closedTab.closedId === parseInt(item.dataset.tabid, 10);
});
if (closedTabIndex < 0) {
// Tab not found in recently closed list
return;
}
lazy.SessionStore.forgetClosedTab(getWindow(), closedTabIndex);
// record telemetry
let tabClosedAt = parseInt(
item.querySelector(".closed-tab-li-time").dataset.timestamp
);
let now = Date.now();
let deltaSeconds = (now - tabClosedAt) / 1000;
Services.telemetry.recordEvent(
"firefoxview",
"dismiss_closed_tab",
"tabs",
null,
{
delta: deltaSeconds.toString(),
}
);
}
updateRecentlyClosedTabs() {
let recentlyClosedTabsData = lazy.SessionStore.getClosedTabDataForWindow(
getWindow()
);
this.recentlyClosedTabs = recentlyClosedTabsData.slice(
0,
this.maxTabsLength
);
this.requestUpdate();
}
render() {
let { recentlyClosedTabs } = this;
let closedTabsContainer = document.getElementById(
"recently-closed-tabs-container"
);
if (!recentlyClosedTabs.length) {
// Show empty message if no recently closed tabs
closedTabsContainer.toggleContainerStyleForEmptyMsg(true);
return html` ${this.emptyMessageTemplate()} `;
}
closedTabsContainer.toggleContainerStyleForEmptyMsg(false);
return html`
${recentlyClosedTabs.map((tab, i) =>
this.recentlyClosedTabTemplate(tab, !i)
)}
`;
}
willUpdate() {
if (this.tabsList && this.tabsList.contains(document.activeElement)) {
let activeLi = document.activeElement.closest(".closed-tab-li");
this.lastFocusedIndex = [...this.tabsList.children].indexOf(activeLi);
} else {
this.lastFocusedIndex = -1;
}
}
updated() {
let focusRestored = false;
if (
this.lastFocusedIndex >= 0 &&
(!this.tabsList || this.lastFocusedIndex >= this.tabsList.children.length)
) {
if (this.tabsList) {
let items = [...this.tabsList.children];
let newFocusIndex = items.length - 1;
let newFocus = items[newFocusIndex];
if (newFocus) {
focusRestored = true;
newFocus.querySelector(".closed-tab-li-main").focus();
}
}
if (!focusRestored) {
document.getElementById("recently-closed-tabs-header-section").focus();
}
}
this.lastFocusedIndex = -1;
}
emptyMessageTemplate() {
return html`
`;
}
recentlyClosedTabTemplate(tab, primary) {
const targetURI = this.getTabStateValue(tab, "url");
const convertedTime = convertTimestamp(
tab.closedAt,
this.fluentStrings,
lazy.timeMsPref
);
return html`
(this.contextTriggerNode = e.currentTarget)}
>
this.openTabAndUpdate(e)}
@keydown=${e => this.openTabAndUpdate(e)}
>
e.preventDefault()}
>
${tab.title}
e.preventDefault()}
>
${formatURIForDisplay(targetURI)}
${convertedTime}
`;
}
// Update the URL for a new or previously-populated list item.
// This is needed because when tabs get closed we don't necessarily
// have all the requisite information for them immediately.
updateURLForListItem(li, targetURI) {
li.dataset.targetURI = targetURI;
let urlElement = li.querySelector(".closed-tab-li-url");
document.l10n.setAttributes(
urlElement,
"firefoxview-tabs-list-tab-button",
{
targetURI,
}
);
if (targetURI) {
urlElement.textContent = formatURIForDisplay(targetURI);
urlElement.title = targetURI;
} else {
urlElement.textContent = urlElement.title = "";
}
}
}
customElements.define("recently-closed-tabs-list", RecentlyClosedTabsList);
class RecentlyClosedTabsContainer extends HTMLDetailsElement {
constructor() {
super();
this.observerAdded = false;
this.boundObserve = (...args) => this.observe(...args);
}
connectedCallback() {
this.noTabsElement = this.querySelector(
"#recently-closed-tabs-placeholder"
);
this.list = this.querySelector("recently-closed-tabs-list");
this.collapsibleContainer = this.querySelector(
"#collapsible-tabs-container"
);
this.addEventListener("toggle", this);
getWindow().gBrowser.tabContainer.addEventListener("TabSelect", this);
getWindow().addEventListener("command", this, true);
getWindow()
.document.getElementById("contentAreaContextMenu")
.addEventListener("popuphiding", this);
this.open = Services.prefs.getBoolPref(UI_OPEN_STATE, true);
}
cleanup() {
getWindow().gBrowser.tabContainer.removeEventListener("TabSelect", this);
getWindow().removeEventListener("command", this, true);
getWindow()
.document.getElementById("contentAreaContextMenu")
.removeEventListener("popuphiding", this);
this.removeObserversIfNeeded();
}
addObserversIfNeeded() {
if (!this.observerAdded) {
Services.obs.addObserver(
this.boundObserve,
SS_NOTIFY_CLOSED_OBJECTS_CHANGED
);
Services.obs.addObserver(
this.boundObserve,
SS_NOTIFY_BROWSER_SHUTDOWN_FLUSH
);
this.observerAdded = true;
}
}
removeObserversIfNeeded() {
if (this.observerAdded) {
Services.obs.removeObserver(
this.boundObserve,
SS_NOTIFY_CLOSED_OBJECTS_CHANGED
);
Services.obs.removeObserver(
this.boundObserve,
SS_NOTIFY_BROWSER_SHUTDOWN_FLUSH
);
this.observerAdded = false;
}
}
// we observe when a tab closes but since this notification fires more frequently and on
// all windows, we remove the observer when another tab is selected; we check for changes
// to the session store once the user return to this tab.
handleObservers(contentDocument) {
if (contentDocument?.URL == "about:firefoxview") {
this.addObserversIfNeeded();
this.list.updateRecentlyClosedTabs();
} else {
this.removeObserversIfNeeded();
}
}
observe(subject, topic, data) {
if (
topic == SS_NOTIFY_CLOSED_OBJECTS_CHANGED ||
(topic == SS_NOTIFY_BROWSER_SHUTDOWN_FLUSH &&
subject.ownerGlobal == getWindow())
) {
this.list.updateRecentlyClosedTabs();
}
}
onLoad() {
this.list.updateRecentlyClosedTabs();
this.addObserversIfNeeded();
}
handleEvent(event) {
if (event.type == "toggle") {
onToggleContainer(this);
} else if (event.type == "TabSelect") {
this.handleObservers(event.target.linkedBrowser.contentDocument);
} else if (
event.type === "command" &&
event.target.closest(".context-menu-open-link") &&
this.list.contextTriggerNode
) {
this.list.dismissTabAndUpdateForElement(this.list.contextTriggerNode);
} else if (event.type === "popuphiding") {
delete this.list.contextTriggerNode;
}
}
toggleContainerStyleForEmptyMsg(visible) {
this.collapsibleContainer.classList.toggle("empty-container", visible);
}
getClosedTabCount = () => {
try {
return lazy.SessionStore.getClosedTabCountForWindow(getWindow());
} catch (ex) {
return 0;
}
};
}
customElements.define(
"recently-closed-tabs-container",
RecentlyClosedTabsContainer,
{
extends: "details",
}
);