forked from mirrors/gecko-dev
461 lines
13 KiB
JavaScript
461 lines
13 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,
|
|
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.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;
|
|
}
|
|
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.getClosedTabData(
|
|
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`
|
|
<ol class="closed-tabs-list">
|
|
${recentlyClosedTabs.map((tab, i) =>
|
|
this.recentlyClosedTabTemplate(tab, !i)
|
|
)}
|
|
</ol>
|
|
`;
|
|
}
|
|
|
|
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`
|
|
<div
|
|
id="recently-closed-tabs-placeholder"
|
|
class="placeholder-content"
|
|
role="presentation"
|
|
>
|
|
<img
|
|
id="recently-closed-empty-image"
|
|
src="chrome://browser/content/firefoxview/recently-closed-empty.svg"
|
|
role="presentation"
|
|
alt=""
|
|
/>
|
|
<div class="placeholder-text">
|
|
<h4
|
|
data-l10n-id="firefoxview-closed-tabs-placeholder-header"
|
|
class="placeholder-header"
|
|
></h4>
|
|
<p
|
|
data-l10n-id="firefoxview-closed-tabs-placeholder-body"
|
|
class="placeholder-body"
|
|
></p>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
recentlyClosedTabTemplate(tab, primary) {
|
|
const targetURI = this.getTabStateValue(tab, "url");
|
|
const convertedTime = convertTimestamp(
|
|
tab.closedAt,
|
|
this.fluentStrings,
|
|
lazy.timeMsPref
|
|
);
|
|
return html`
|
|
<li
|
|
class="closed-tab-li"
|
|
data-tabid=${tab.closedId}
|
|
data-targeturi=${targetURI}
|
|
tabindex=${ifDefined(primary ? null : "-1")}
|
|
@contextmenu=${e => (this.contextTriggerNode = e.currentTarget)}
|
|
>
|
|
<span
|
|
class="closed-tab-li-main"
|
|
role="button"
|
|
tabindex="0"
|
|
@click=${e => this.openTabAndUpdate(e)}
|
|
@keydown=${e => this.openTabAndUpdate(e)}
|
|
>
|
|
<div
|
|
class="favicon"
|
|
style=${styleMap({
|
|
backgroundImage: `url(${getImageUrl(tab.icon, targetURI)})`,
|
|
})}
|
|
></div>
|
|
<a
|
|
href=${targetURI}
|
|
class="closed-tab-li-title"
|
|
tabindex="-1"
|
|
@click=${e => e.preventDefault()}
|
|
>
|
|
${tab.title}
|
|
</a>
|
|
<a
|
|
href=${targetURI}
|
|
class="closed-tab-li-url"
|
|
data-l10n-id="firefoxview-tabs-list-tab-button"
|
|
data-l10n-args=${JSON.stringify({ targetURI })}
|
|
tabindex="-1"
|
|
@click=${e => e.preventDefault()}
|
|
>
|
|
${formatURIForDisplay(targetURI)}
|
|
</a>
|
|
<span class="closed-tab-li-time" data-timestamp=${tab.closedAt}>
|
|
${convertedTime}
|
|
</span>
|
|
</span>
|
|
<button
|
|
class="closed-tab-li-dismiss"
|
|
data-l10n-id="firefoxview-closed-tabs-dismiss-tab"
|
|
data-l10n-args=${JSON.stringify({ tabTitle: tab.title })}
|
|
@click=${e => this.dismissTabAndUpdate(e)}
|
|
></button>
|
|
</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);
|
|
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.getClosedTabCount(getWindow());
|
|
} catch (ex) {
|
|
return 0;
|
|
}
|
|
};
|
|
}
|
|
customElements.define(
|
|
"recently-closed-tabs-container",
|
|
RecentlyClosedTabsContainer,
|
|
{
|
|
extends: "details",
|
|
}
|
|
);
|