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/. */
6 ChromeUtils.defineESModuleGetters(lazy, {
7 SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
16 } from "./helpers.mjs";
22 } from "chrome://global/content/vendor/lit.all.mjs";
23 import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
25 const { XPCOMUtils } = ChromeUtils.importESModule(
26 "resource://gre/modules/XPCOMUtils.sys.mjs"
29 const SS_NOTIFY_CLOSED_OBJECTS_CHANGED = "sessionstore-closed-objects-changed";
30 const SS_NOTIFY_BROWSER_SHUTDOWN_FLUSH = "sessionstore-browser-shutdown-flush";
32 "browser.tabs.firefox-view.ui-state.recently-closed-tabs.open";
34 function getWindow() {
35 return window.browsingContext.embedderWindowGlobal.browsingContext.window;
38 class RecentlyClosedTabsList extends MozLitElement {
41 this.maxTabsLength = 25;
42 this.recentlyClosedTabs = [];
43 this.lastFocusedIndex = -1;
45 // The recency timestamp update period is stored in a pref to allow tests to easily change it
46 XPCOMUtils.defineLazyPreferenceGetter(
49 "browser.tabs.firefox-view.updateTimeMs",
52 clearInterval(this.intervalID);
53 this.intervalID = setInterval(() => this.requestUpdate(), timeMsPref);
65 timeElements: { all: "span.closed-tab-li-time" },
69 if (!this._fluentStrings) {
70 this._fluentStrings = new Localization(["browser/firefoxView.ftl"], true);
72 return this._fluentStrings;
76 super.connectedCallback();
77 this.intervalID = setInterval(() => this.requestUpdate(), lazy.timeMsPref);
80 disconnectedCallback() {
81 super.disconnectedCallback();
82 clearInterval(this.intervalID);
85 getTabStateValue(tab, key) {
87 const tabEntries = tab.entries;
88 const activeIndex = tab.index - 1;
90 if (activeIndex >= 0 && tabEntries[activeIndex]) {
91 value = tabEntries[activeIndex][key];
97 openTabAndUpdate(event) {
99 (event.type == "click" && !event.altKey) ||
100 (event.type == "keydown" && event.code == "Enter") ||
101 (event.type == "keydown" && event.code == "Space")
103 const item = event.target.closest(".closed-tab-li");
104 // only used for telemetry
105 const position = [...this.tabsList.children].indexOf(item) + 1;
106 const closedId = item.dataset.tabid;
108 lazy.SessionStore.undoCloseById(closedId, undefined, getWindow());
111 let tabClosedAt = parseInt(
112 item.querySelector(".closed-tab-li-time").getAttribute("data-timestamp")
115 let now = Date.now();
116 let deltaSeconds = (now - tabClosedAt) / 1000;
117 Services.telemetry.recordEvent(
123 position: position.toString(),
124 delta: deltaSeconds.toString(),
130 dismissTabAndUpdate(event) {
131 event.preventDefault();
132 const item = event.target.closest(".closed-tab-li");
133 this.dismissTabAndUpdateForElement(item);
136 dismissTabAndUpdateForElement(item) {
137 const sourceWindow = lazy.SessionStore.getWindowById(
138 item.dataset.sourceWindowId
140 let recentlyClosedList =
141 lazy.SessionStore.getClosedTabDataForWindow(sourceWindow);
142 let closedTabIndex = recentlyClosedList.findIndex(closedTab => {
143 return closedTab.closedId === parseInt(item.dataset.tabid, 10);
145 if (closedTabIndex < 0) {
146 // Tab not found in recently closed list
149 // in order forget a closed tab, we need to pass the window the closed tab data is associated with
150 lazy.SessionStore.forgetClosedTab(sourceWindow, closedTabIndex);
153 let tabClosedAt = parseInt(
154 item.querySelector(".closed-tab-li-time").dataset.timestamp
157 let now = Date.now();
158 let deltaSeconds = (now - tabClosedAt) / 1000;
159 Services.telemetry.recordEvent(
161 "dismiss_closed_tab",
165 delta: deltaSeconds.toString(),
170 getClosedTabsDataForOpenWindows() {
171 // get closed tabs in currently-open windows
172 const closedTabsData = lazy.SessionStore.getClosedTabData(getWindow()).map(
174 // flatten the object; move properties of `.state` into the top-level object
175 const stateData = tabData.state;
176 delete tabData.state;
183 return closedTabsData;
186 updateRecentlyClosedTabs() {
187 // add recently-closed tabs from currently-open windows
188 const recentlyClosedTabsData = this.getClosedTabsDataForOpenWindows();
190 recentlyClosedTabsData.sort((a, b) => a.closedAt < b.closedAt);
191 this.recentlyClosedTabs = recentlyClosedTabsData.slice(
195 this.requestUpdate();
199 let { recentlyClosedTabs } = this;
200 let closedTabsContainer = document.getElementById(
201 "recently-closed-tabs-container"
204 if (!recentlyClosedTabs.length) {
205 // Show empty message if no recently closed tabs
206 closedTabsContainer.toggleContainerStyleForEmptyMsg(true);
207 return html` ${this.emptyMessageTemplate()} `;
210 closedTabsContainer.toggleContainerStyleForEmptyMsg(false);
213 <ol class="closed-tabs-list">
214 ${recentlyClosedTabs.map((tab, i) =>
215 this.recentlyClosedTabTemplate(tab, !i)
222 if (this.tabsList && this.tabsList.contains(document.activeElement)) {
223 let activeLi = document.activeElement.closest(".closed-tab-li");
224 this.lastFocusedIndex = [...this.tabsList.children].indexOf(activeLi);
226 this.lastFocusedIndex = -1;
231 let focusRestored = false;
233 this.lastFocusedIndex >= 0 &&
234 (!this.tabsList || this.lastFocusedIndex >= this.tabsList.children.length)
237 let items = [...this.tabsList.children];
238 let newFocusIndex = items.length - 1;
239 let newFocus = items[newFocusIndex];
241 focusRestored = true;
242 newFocus.querySelector(".closed-tab-li-main").focus();
245 if (!focusRestored) {
246 document.getElementById("recently-closed-tabs-header-section").focus();
249 this.lastFocusedIndex = -1;
252 emptyMessageTemplate() {
255 id="recently-closed-tabs-placeholder"
256 class="placeholder-content"
260 id="recently-closed-empty-image"
261 src="chrome://browser/content/firefoxview/recently-closed-empty.svg"
265 <div class="placeholder-text">
267 data-l10n-id="firefoxview-closed-tabs-placeholder-header"
268 class="placeholder-header"
271 data-l10n-id="firefoxview-closed-tabs-placeholder-body2"
272 class="placeholder-body"
279 recentlyClosedTabTemplate(tab, primary) {
280 const targetURI = this.getTabStateValue(tab, "url");
281 const convertedTime = convertTimestamp(
288 class="closed-tab-li"
289 data-tabid=${tab.closedId}
290 data-source-window-id=${tab.sourceWindowId}
291 data-targeturi=${targetURI}
292 tabindex=${ifDefined(primary ? null : "-1")}
293 @contextmenu=${e => (this.contextTriggerNode = e.currentTarget)}
296 class="closed-tab-li-main"
299 @click=${e => this.openTabAndUpdate(e)}
300 @keydown=${e => this.openTabAndUpdate(e)}
305 backgroundImage: `url(${getImageUrl(tab.icon, targetURI)})`,
310 class="closed-tab-li-title"
312 @click=${e => e.preventDefault()}
318 class="closed-tab-li-url"
319 data-l10n-id="firefoxview-tabs-list-tab-button"
320 data-l10n-args=${JSON.stringify({ targetURI })}
322 @click=${e => e.preventDefault()}
324 ${formatURIForDisplay(targetURI)}
326 <span class="closed-tab-li-time" data-timestamp=${tab.closedAt}>
331 class="closed-tab-li-dismiss"
332 data-l10n-id="firefoxview-closed-tabs-dismiss-tab"
333 data-l10n-args=${JSON.stringify({ tabTitle: tab.title })}
334 @click=${e => this.dismissTabAndUpdate(e)}
340 // Update the URL for a new or previously-populated list item.
341 // This is needed because when tabs get closed we don't necessarily
342 // have all the requisite information for them immediately.
343 updateURLForListItem(li, targetURI) {
344 li.dataset.targetURI = targetURI;
345 let urlElement = li.querySelector(".closed-tab-li-url");
346 document.l10n.setAttributes(
348 "firefoxview-tabs-list-tab-button",
354 urlElement.textContent = formatURIForDisplay(targetURI);
355 urlElement.title = targetURI;
357 urlElement.textContent = urlElement.title = "";
361 customElements.define("recently-closed-tabs-list", RecentlyClosedTabsList);
363 class RecentlyClosedTabsContainer extends HTMLDetailsElement {
366 this.observerAdded = false;
367 this.boundObserve = (...args) => this.observe(...args);
370 connectedCallback() {
371 this.noTabsElement = this.querySelector(
372 "#recently-closed-tabs-placeholder"
374 this.list = this.querySelector("recently-closed-tabs-list");
375 this.collapsibleContainer = this.querySelector(
376 "#collapsible-tabs-container"
378 this.addEventListener("toggle", this);
379 getWindow().gBrowser.tabContainer.addEventListener("TabSelect", this);
380 getWindow().addEventListener("command", this, true);
382 .document.getElementById("contentAreaContextMenu")
383 .addEventListener("popuphiding", this);
384 this.open = Services.prefs.getBoolPref(UI_OPEN_STATE, true);
388 getWindow().gBrowser.tabContainer.removeEventListener("TabSelect", this);
389 getWindow().removeEventListener("command", this, true);
391 .document.getElementById("contentAreaContextMenu")
392 .removeEventListener("popuphiding", this);
393 this.removeObserversIfNeeded();
396 addObserversIfNeeded() {
397 if (!this.observerAdded) {
398 Services.obs.addObserver(
400 SS_NOTIFY_CLOSED_OBJECTS_CHANGED
402 Services.obs.addObserver(
404 SS_NOTIFY_BROWSER_SHUTDOWN_FLUSH
406 this.observerAdded = true;
410 removeObserversIfNeeded() {
411 if (this.observerAdded) {
412 Services.obs.removeObserver(
414 SS_NOTIFY_CLOSED_OBJECTS_CHANGED
416 Services.obs.removeObserver(
418 SS_NOTIFY_BROWSER_SHUTDOWN_FLUSH
420 this.observerAdded = false;
424 // we observe when a tab closes but since this notification fires more frequently and on
425 // all windows, we remove the observer when another tab is selected; we check for changes
426 // to the session store once the user return to this tab.
427 handleObservers(contentDocument) {
428 if (contentDocument?.URL == "about:firefoxview") {
429 this.addObserversIfNeeded();
430 this.list.updateRecentlyClosedTabs();
432 this.removeObserversIfNeeded();
436 observe(subject, topic, data) {
438 topic == SS_NOTIFY_CLOSED_OBJECTS_CHANGED ||
439 (topic == SS_NOTIFY_BROWSER_SHUTDOWN_FLUSH &&
440 subject.ownerGlobal == getWindow())
442 this.list.updateRecentlyClosedTabs();
447 this.list.updateRecentlyClosedTabs();
448 this.addObserversIfNeeded();
452 if (event.type == "toggle") {
453 onToggleContainer(this);
454 } else if (event.type == "TabSelect") {
455 this.handleObservers(event.target.linkedBrowser.contentDocument);
457 event.type === "command" &&
458 event.target.closest(".context-menu-open-link") &&
459 this.list.contextTriggerNode
461 this.list.dismissTabAndUpdateForElement(this.list.contextTriggerNode);
462 } else if (event.type === "popuphiding") {
463 delete this.list.contextTriggerNode;
467 toggleContainerStyleForEmptyMsg(visible) {
468 this.collapsibleContainer.classList.toggle("empty-container", visible);
471 customElements.define(
472 "recently-closed-tabs-container",
473 RecentlyClosedTabsContainer,