gecko-dev/browser/components/firefoxview/recently-closed-tabs.mjs
Bernard Igiri 2355313063 Bug 1770467 - Add arrow key nav to Recently Closed Tabs r=sfoster,sclements
Added arrow key navigation to recently closed tabs.

Differential Revision: https://phabricator.services.mozilla.com/D154402
2022-08-15 16:45:22 +00:00

342 lines
9.2 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/. */
"use strict";
const lazy = {};
ChromeUtils.defineModuleGetter(
lazy,
"SessionStore",
"resource:///modules/sessionstore/SessionStore.jsm"
);
import {
formatURIForDisplay,
convertTimestamp,
createFaviconElement,
toggleContainer,
NOW_THRESHOLD_MS,
} from "./helpers.mjs";
const { XPCOMUtils } = ChromeUtils.importESModule(
"resource://gre/modules/XPCOMUtils.sys.mjs"
);
const SS_NOTIFY_CLOSED_OBJECTS_CHANGED = "sessionstore-closed-objects-changed";
function getWindow() {
return window.browsingContext.embedderWindowGlobal.browsingContext.window;
}
class RecentlyClosedTabsList extends HTMLElement {
constructor() {
super();
this.maxTabsLength = 25;
this.closedTabsData = [];
// 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,
() => this.updateTime()
);
}
get tabsList() {
return this.querySelector("ol");
}
get fluentStrings() {
if (!this._fluentStrings) {
this._fluentStrings = new Localization(["browser/firefoxView.ftl"], true);
}
return this._fluentStrings;
}
get timeElements() {
return this.querySelectorAll("span.closed-tab-li-time");
}
connectedCallback() {
this.addEventListener("click", this);
this.addEventListener("keydown", this);
this.intervalID = setInterval(() => this.updateTime(), lazy.timeMsPref);
}
disconnectedCallback() {
clearInterval(this.intervalID);
}
handleEvent(event) {
if (
event.type == "click" ||
(event.type == "keydown" && event.keyCode == KeyEvent.DOM_VK_RETURN)
) {
this.openTabAndUpdate(event);
} else if (
event.type == "keydown" &&
!event.shiftKey &&
!event.ctrlKey &&
event.target.classList.contains("closed-tab-li")
) {
switch (event.key) {
case "ArrowDown":
event.target.nextSibling?.focus();
break;
case "ArrowUp":
event.target.previousSibling?.focus();
break;
}
}
}
updateTime() {
for (let timeEl of this.timeElements) {
timeEl.textContent = convertTimestamp(
parseInt(timeEl.getAttribute("data-timestamp")),
this.fluentStrings,
lazy.timeMsPref
);
}
}
getTabStateValue(tab, key) {
let value = "";
const tabEntries = tab.state.entries;
const activeIndex = tabEntries.length - 1;
if (activeIndex >= 0 && tabEntries[activeIndex]) {
value = tabEntries[activeIndex][key];
}
return value;
}
openTabAndUpdate(event) {
event.preventDefault();
const item = event.target.closest(".closed-tab-li");
let index = [...this.tabsList.children].indexOf(item);
lazy.SessionStore.undoCloseTab(getWindow(), index);
this.tabsList.removeChild(item);
// 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: (++index).toString(),
delta: deltaSeconds.toString(),
}
);
}
initiateTabsList() {
let closedTabs = lazy.SessionStore.getClosedTabData(getWindow());
closedTabs = closedTabs.slice(0, this.maxTabsLength);
this.closedTabsData = closedTabs;
for (const tab of closedTabs) {
const li = this.generateListItem(tab);
this.tabsList.append(li);
}
this.tabsList.hidden = false;
}
updateTabsList() {
let newClosedTabs = lazy.SessionStore.getClosedTabData(getWindow());
newClosedTabs = newClosedTabs.slice(0, this.maxTabsLength);
if (this.closedTabsData.length && !newClosedTabs.length) {
// if a user purges history, clear the list
[...this.tabsList.children].forEach(node =>
this.tabsList.removeChild(node)
);
document
.getElementById("recently-closed-tabs-container")
.togglePlaceholderVisibility(true);
this.tabsList.hidden = true;
this.closedTabsData = [];
return;
}
const tabsToAdd = newClosedTabs.filter(
newTab =>
!this.closedTabsData.some(tab => {
return (
this.getTabStateValue(tab, "ID") ==
this.getTabStateValue(newTab, "ID")
);
})
);
if (!tabsToAdd.length) {
return;
}
for (let tab of tabsToAdd.reverse()) {
if (this.tabsList.children.length == this.maxTabsLength) {
this.tabsList.lastChild.remove();
}
let li = this.generateListItem(tab);
this.tabsList.prepend(li);
}
this.closedTabsData = newClosedTabs;
// for situations where the tab list will initially be empty (such as
// with new profiles or automatic session restore is disabled) and
// this.initiateTabsList won't be called
if (this.tabsList.hidden) {
this.tabsList.hidden = false;
document
.getElementById("recently-closed-tabs-container")
.togglePlaceholderVisibility(false);
}
}
generateListItem(tab) {
const li = document.createElement("li");
li.classList.add("closed-tab-li");
li.setAttribute("tabindex", 0);
li.setAttribute("role", "button");
const title = document.createElement("span");
title.textContent = `${tab.title}`;
title.classList.add("closed-tab-li-title");
const favicon = createFaviconElement(tab.image);
li.append(favicon);
const targetURI = this.getTabStateValue(tab, "url");
li.dataset.targetURI = targetURI;
document.l10n.setAttributes(li, "firefoxview-tabs-list-tab-button", {
targetURI,
});
const url = document.createElement("span");
if (targetURI) {
url.textContent = formatURIForDisplay(targetURI);
url.title = targetURI;
url.classList.add("closed-tab-li-url");
}
const time = document.createElement("span");
time.textContent = convertTimestamp(tab.closedAt, this.fluentStrings);
time.setAttribute("data-timestamp", tab.closedAt);
time.classList.add("closed-tab-li-time");
li.append(title, url, time);
return li;
}
}
customElements.define("recently-closed-tabs-list", RecentlyClosedTabsList);
class RecentlyClosedTabsContainer extends HTMLElement {
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.collapsibleButton = this.querySelector("#collapsible-tabs-button");
this.collapsibleButton.addEventListener("click", this);
getWindow().gBrowser.tabContainer.addEventListener("TabSelect", this);
}
cleanup() {
getWindow().gBrowser.tabContainer.removeEventListener("TabSelect", this);
if (this.observerAdded) {
Services.obs.removeObserver(
this.boundObserve,
SS_NOTIFY_CLOSED_OBJECTS_CHANGED
);
}
}
// 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 (
!this.observerAdded &&
contentDocument &&
contentDocument.URL == "about:firefoxview"
) {
Services.obs.addObserver(
this.boundObserve,
SS_NOTIFY_CLOSED_OBJECTS_CHANGED
);
this.observerAdded = true;
this.list.updateTabsList();
} else if (this.observerAdded) {
Services.obs.removeObserver(
this.boundObserve,
SS_NOTIFY_CLOSED_OBJECTS_CHANGED
);
this.observerAdded = false;
}
}
observe = () => this.list.updateTabsList();
onLoad() {
if (this.getClosedTabCount() == 0) {
this.togglePlaceholderVisibility(true);
} else {
this.list.initiateTabsList();
}
Services.obs.addObserver(
this.boundObserve,
SS_NOTIFY_CLOSED_OBJECTS_CHANGED
);
this.observerAdded = true;
}
handleEvent(event) {
if (event.type == "click" && event.target == this.collapsibleButton) {
toggleContainer(this.collapsibleButton, this.collapsibleContainer);
} else if (event.type == "TabSelect") {
this.handleObservers(event.target.linkedBrowser.contentDocument);
}
}
togglePlaceholderVisibility(visible) {
this.noTabsElement.toggleAttribute("hidden", !visible);
this.collapsibleContainer.classList.toggle("empty-container", visible);
}
getClosedTabCount = () => {
try {
return lazy.SessionStore.getClosedTabCount(getWindow());
} catch (ex) {
return 0;
}
};
}
customElements.define(
"recently-closed-tabs-container",
RecentlyClosedTabsContainer
);