diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /browser/components/firefoxview/recently-closed-tabs.mjs | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'browser/components/firefoxview/recently-closed-tabs.mjs')
-rw-r--r-- | browser/components/firefoxview/recently-closed-tabs.mjs | 463 |
1 files changed, 463 insertions, 0 deletions
diff --git a/browser/components/firefoxview/recently-closed-tabs.mjs b/browser/components/firefoxview/recently-closed-tabs.mjs new file mode 100644 index 0000000000..295fc03d27 --- /dev/null +++ b/browser/components/firefoxview/recently-closed-tabs.mjs @@ -0,0 +1,463 @@ +/* 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` + <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.getClosedTabCountForWindow(getWindow()); + } catch (ex) { + return 0; + } + }; +} +customElements.define( + "recently-closed-tabs-container", + RecentlyClosedTabsContainer, + { + extends: "details", + } +); |