forked from mirrors/gecko-dev
488 lines
14 KiB
JavaScript
488 lines
14 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/. */
|
|
|
|
const lazy = {};
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
|
|
});
|
|
|
|
import {
|
|
formatURIForDisplay,
|
|
convertTimestamp,
|
|
createFaviconElement,
|
|
onToggleContainer,
|
|
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";
|
|
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 HTMLElement {
|
|
constructor() {
|
|
super();
|
|
this.maxTabsLength = 25;
|
|
this.closedTabsData = new Map();
|
|
|
|
// 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.altKey) ||
|
|
(event.type == "keydown" && event.keyCode == KeyEvent.DOM_VK_RETURN) ||
|
|
(event.type == "keydown" && event.keyCode == KeyEvent.DOM_VK_SPACE)
|
|
) {
|
|
if (!event.target.classList.contains("closed-tab-li-dismiss")) {
|
|
this.openTabAndUpdate(event);
|
|
} else {
|
|
this.dismissTabAndUpdate(event);
|
|
}
|
|
}
|
|
}
|
|
|
|
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 = tab.state.index - 1;
|
|
|
|
if (activeIndex >= 0 && tabEntries[activeIndex]) {
|
|
value = tabEntries[activeIndex][key];
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
focusFirstItemOrHeader(dismissedIndex) {
|
|
// When a tab is removed from the list, the focus should
|
|
// remain on the list or the list header. This prevents context
|
|
// switching when navigating back to Firefox View.
|
|
let recentlyClosedList = [...this.tabsList.children];
|
|
if (recentlyClosedList.length) {
|
|
recentlyClosedList.forEach(element =>
|
|
element.setAttribute("tabindex", "-1")
|
|
);
|
|
let mainContent;
|
|
if (dismissedIndex) {
|
|
// Select the item above the one that was just dismissed
|
|
mainContent = recentlyClosedList[dismissedIndex - 1].querySelector(
|
|
".closed-tab-li-main"
|
|
);
|
|
} else {
|
|
mainContent = recentlyClosedList[0].querySelector(
|
|
".closed-tab-li-main"
|
|
);
|
|
}
|
|
mainContent.setAttribute("tabindex", "0");
|
|
mainContent.focus();
|
|
} else {
|
|
document.getElementById("recently-closed-tabs-header-section").focus();
|
|
}
|
|
}
|
|
|
|
openTabAndUpdate(event) {
|
|
event.preventDefault();
|
|
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);
|
|
this.tabsList.removeChild(item);
|
|
|
|
this.focusFirstItemOrHeader();
|
|
|
|
// 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");
|
|
let recentlyClosedList = lazy.SessionStore.getClosedTabData(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;
|
|
}
|
|
this.tabsList.removeChild(item);
|
|
lazy.SessionStore.forgetClosedTab(getWindow(), closedTabIndex);
|
|
|
|
this.focusFirstItemOrHeader(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(),
|
|
}
|
|
);
|
|
}
|
|
|
|
updateTabsList() {
|
|
let newClosedTabs = lazy.SessionStore.getClosedTabData(getWindow());
|
|
newClosedTabs = newClosedTabs.slice(0, this.maxTabsLength);
|
|
|
|
if (this.closedTabsData.size && !newClosedTabs.length) {
|
|
// if a user purges history, clear the list
|
|
while (this.tabsList.lastElementChild) {
|
|
this.tabsList.lastElementChild.remove();
|
|
}
|
|
document
|
|
.getElementById("recently-closed-tabs-container")
|
|
.togglePlaceholderVisibility(true);
|
|
this.tabsList.hidden = true;
|
|
this.closedTabsData = new Map();
|
|
return;
|
|
}
|
|
|
|
// First purge obsolete items out of the map so we don't leak them forever:
|
|
for (let id of this.closedTabsData.keys()) {
|
|
if (!newClosedTabs.some(t => t.closedId == id)) {
|
|
this.closedTabsData.delete(id);
|
|
}
|
|
}
|
|
|
|
// Then work out which of the new closed tabs are additions and which update
|
|
// existing items:
|
|
let tabsToAdd = [];
|
|
let tabsToUpdate = [];
|
|
for (let newTab of newClosedTabs) {
|
|
let oldTab = this.closedTabsData.get(newTab.closedId);
|
|
this.closedTabsData.set(newTab.closedId, newTab);
|
|
if (!oldTab) {
|
|
tabsToAdd.push(newTab);
|
|
} else if (
|
|
this.getTabStateValue(oldTab, "url") !=
|
|
this.getTabStateValue(newTab, "url")
|
|
) {
|
|
tabsToUpdate.push(newTab);
|
|
}
|
|
}
|
|
|
|
// Remove existing tabs from tabsList if not in latest closedTabsData
|
|
// which is necessary when using "Reopen Closed Tab" from the toolbar
|
|
// or when selecting "Forget this site" in History
|
|
[...this.tabsList.children].forEach(existingTab => {
|
|
if (!this.closedTabsData.get(parseInt(existingTab.dataset.tabid, 10))) {
|
|
this.tabsList.removeChild(existingTab);
|
|
}
|
|
});
|
|
|
|
// If there's nothing to add/update, return.
|
|
if (!tabsToAdd.length && !tabsToUpdate.length) {
|
|
return;
|
|
}
|
|
|
|
// Add new tabs.
|
|
for (let tab of tabsToAdd.reverse()) {
|
|
if (this.tabsList.children.length == this.maxTabsLength) {
|
|
this.tabsList.lastChild.remove();
|
|
}
|
|
let li = this.generateListItem(tab);
|
|
let mainContent = li.querySelector(".closed-tab-li-main");
|
|
// Only the first item in the list should be focusable
|
|
if (!this.tabsList.children.length) {
|
|
mainContent.setAttribute("tabindex", "0");
|
|
} else if (this.tabsList.children.length) {
|
|
mainContent.setAttribute("tabindex", "0");
|
|
this.tabsList.children[0].setAttribute("tabindex", "-1");
|
|
}
|
|
this.tabsList.prepend(li);
|
|
}
|
|
|
|
// Update any recently closed tabs that now have different URLs:
|
|
for (let tab of tabsToUpdate) {
|
|
let tabElement = this.querySelector(
|
|
`.closed-tab-li[data-tabid="${tab.closedId}"]`
|
|
);
|
|
let url = this.getTabStateValue(tab, "url");
|
|
this.updateURLForListItem(tabElement, url);
|
|
}
|
|
|
|
// Now unhide the list if necessary:
|
|
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.dataset.tabid = tab.closedId;
|
|
|
|
const title = document.createElement("span");
|
|
title.textContent = `${tab.title}`;
|
|
title.classList.add("closed-tab-li-title");
|
|
|
|
const targetURI = this.getTabStateValue(tab, "url");
|
|
const image = tab.image;
|
|
const favicon = createFaviconElement(image, targetURI);
|
|
|
|
const urlElement = document.createElement("span");
|
|
urlElement.classList.add("closed-tab-li-url");
|
|
|
|
const time = document.createElement("span");
|
|
const convertedTime = convertTimestamp(tab.closedAt, this.fluentStrings);
|
|
time.textContent = convertedTime;
|
|
time.setAttribute("data-timestamp", tab.closedAt);
|
|
time.classList.add("closed-tab-li-time");
|
|
|
|
const mainContent = document.createElement("span");
|
|
mainContent.classList.add("closed-tab-li-main");
|
|
mainContent.setAttribute("role", "link");
|
|
mainContent.setAttribute("tabindex", 0);
|
|
mainContent.append(favicon, title, urlElement, time);
|
|
|
|
const dismissButton = document.createElement("button");
|
|
let tabTitle = tab.title ?? "";
|
|
document.l10n.setAttributes(
|
|
dismissButton,
|
|
"firefoxview-closed-tabs-dismiss-tab",
|
|
{
|
|
tabTitle,
|
|
}
|
|
);
|
|
dismissButton.classList.add("closed-tab-li-dismiss");
|
|
|
|
li.append(mainContent, dismissButton);
|
|
this.updateURLForListItem(li, targetURI);
|
|
return li;
|
|
}
|
|
|
|
// 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);
|
|
this.open = Services.prefs.getBoolPref(UI_OPEN_STATE, true);
|
|
}
|
|
|
|
cleanup() {
|
|
getWindow().gBrowser.tabContainer.removeEventListener("TabSelect", 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.updateTabsList();
|
|
this.maybeUpdateFocus();
|
|
} 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.updateTabsList();
|
|
}
|
|
}
|
|
|
|
onLoad() {
|
|
if (this.getClosedTabCount() == 0) {
|
|
this.togglePlaceholderVisibility(true);
|
|
} else {
|
|
this.list.updateTabsList();
|
|
}
|
|
this.addObserversIfNeeded();
|
|
}
|
|
|
|
handleEvent(event) {
|
|
if (event.type == "toggle") {
|
|
onToggleContainer(this);
|
|
} else if (event.type == "TabSelect") {
|
|
this.handleObservers(event.target.linkedBrowser.contentDocument);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Manages focus when returning to the Firefox View tab
|
|
*
|
|
* @memberof RecentlyClosedTabsContainer
|
|
*/
|
|
maybeUpdateFocus() {
|
|
// Check if focus is in the container element
|
|
if (this.contains(document.activeElement)) {
|
|
let listItems = this.list.querySelectorAll("li");
|
|
// More tabs may have been added to the list, so we'll refocus
|
|
// the first item in the list.
|
|
if (listItems.length) {
|
|
listItems[0].querySelector(".closed-tab-li-main").focus();
|
|
} else {
|
|
this.querySelector("summary").focus();
|
|
}
|
|
}
|
|
}
|
|
|
|
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,
|
|
{
|
|
extends: "details",
|
|
}
|
|
);
|