/* 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", } );