Source code
Revision control
Copy as Markdown
Other Tools
/* 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
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
});
const { TabsSetupFlowManager } = ChromeUtils.importESModule(
);
import {
html,
ifDefined,
when,
} from "chrome://global/content/vendor/lit.all.mjs";
import { ViewPage } from "./viewpage.mjs";
import {
escapeHtmlEntities,
MAX_TABS_FOR_RECENT_BROWSING,
navigateToLink,
} from "./helpers.mjs";
// eslint-disable-next-line import/no-unassigned-import
import "chrome://browser/content/firefoxview/syncedtabs-tab-list.mjs";
const UI_OPEN_STATE = "browser.tabs.firefox-view.ui-state.tab-pickup.open";
class SyncedTabsInView extends ViewPage {
controller = new lazy.SyncedTabsController(this, {
contextMenu: true,
pairDeviceCallback: () =>
Glean.firefoxviewNext.fxaMobileSync.record({
has_devices: TabsSetupFlowManager.secondaryDeviceConnected,
}),
signupCallback: () => Glean.firefoxviewNext.fxaContinueSync.record(),
});
constructor() {
super();
this._started = false;
this._id = Math.floor(Math.random() * 10e6);
if (this.recentBrowsing) {
this.maxTabsLength = MAX_TABS_FOR_RECENT_BROWSING;
} else {
// Setting maxTabsLength to -1 for no max
this.maxTabsLength = -1;
}
this.fullyUpdated = false;
this.showAll = false;
this.cumulativeSearches = 0;
this.onSearchQuery = this.onSearchQuery.bind(this);
}
static properties = {
...ViewPage.properties,
showAll: { type: Boolean },
cumulativeSearches: { type: Number },
};
static queries = {
cardEls: { all: "card-container" },
emptyState: "fxview-empty-state",
searchTextbox: "fxview-search-textbox",
tabLists: { all: "syncedtabs-tab-list" },
};
start() {
if (this._started) {
return;
}
this._started = true;
this.controller.addSyncObservers();
this.controller.updateStates();
this.onVisibilityChange();
if (this.recentBrowsing) {
this.recentBrowsingElement.addEventListener(
"fxview-search-textbox-query",
this.onSearchQuery
);
}
}
stop() {
if (!this._started) {
return;
}
this._started = false;
TabsSetupFlowManager.updateViewVisibility(this._id, "unloaded");
this.onVisibilityChange();
this.controller.removeSyncObservers();
if (this.recentBrowsing) {
this.recentBrowsingElement.removeEventListener(
"fxview-search-textbox-query",
this.onSearchQuery
);
}
}
disconnectedCallback() {
super.disconnectedCallback();
this.stop();
}
viewVisibleCallback() {
this.start();
}
viewHiddenCallback() {
this.stop();
}
onVisibilityChange() {
const isOpen = this.open;
const isVisible = this.isVisible;
if (isVisible && isOpen) {
this.update();
TabsSetupFlowManager.updateViewVisibility(this._id, "visible");
} else {
TabsSetupFlowManager.updateViewVisibility(
this._id,
isVisible ? "closed" : "hidden"
);
}
this.toggleVisibilityInCardContainer();
}
generateMessageCard({
action,
buttonLabel,
descriptionArray,
descriptionLink,
header,
mainImageUrl,
}) {
return html`
<fxview-empty-state
headerLabel=${header}
.descriptionLabels=${descriptionArray}
.descriptionLink=${ifDefined(descriptionLink)}
class="empty-state synced-tabs error"
?isSelectedTab=${this.selectedTab}
?isInnerCard=${this.recentBrowsing}
mainImageUrl="${ifDefined(mainImageUrl)}"
id="empty-container"
>
<button
class="primary"
slot="primary-action"
?hidden=${!buttonLabel}
data-l10n-id="${ifDefined(buttonLabel)}"
data-action="${action}"
@click=${e => this.controller.handleEvent(e)}
></button>
</fxview-empty-state>
`;
}
onOpenLink(event) {
navigateToLink(event);
Glean.firefoxviewNext.syncedTabsTabs.record({
page: this.recentBrowsing ? "recentbrowsing" : "syncedtabs",
});
if (this.controller.searchQuery) {
const searchesHistogram = Services.telemetry.getKeyedHistogramById(
"FIREFOX_VIEW_CUMULATIVE_SEARCHES"
);
searchesHistogram.add(
this.recentBrowsing ? "recentbrowsing" : "syncedtabs",
this.cumulativeSearches
);
this.cumulativeSearches = 0;
}
}
onContextMenu(e) {
this.triggerNode = e.originalTarget;
e.target.querySelector("panel-list").toggle(e.detail.originalEvent);
}
onCloseTab(e) {
const { url, fxaDeviceId, tertiaryActionClass } = e.originalTarget;
if (tertiaryActionClass === "dismiss-button") {
// Set new pending close tab
this.controller.requestCloseRemoteTab(fxaDeviceId, url);
} else if (tertiaryActionClass === "undo-button") {
// User wants to undo
this.controller.removePendingTabToClose(fxaDeviceId, url);
}
this.requestUpdate();
}
panelListTemplate() {
return html`
<panel-list slot="menu" data-tab-type="syncedtabs">
<panel-item
@click=${this.openInNewWindow}
data-l10n-id="fxviewtabrow-open-in-window"
data-l10n-attrs="accesskey"
></panel-item>
<panel-item
@click=${this.openInNewPrivateWindow}
data-l10n-id="fxviewtabrow-open-in-private-window"
data-l10n-attrs="accesskey"
></panel-item>
<hr />
<panel-item
@click=${this.copyLink}
data-l10n-id="fxviewtabrow-copy-link"
data-l10n-attrs="accesskey"
></panel-item>
</panel-list>
`;
}
noDeviceTabsTemplate(deviceName, deviceType, isSearchResultsEmpty = false) {
const template = html`<h3
slot=${ifDefined(this.recentBrowsing ? null : "header")}
class="device-header"
>
<span class="icon ${deviceType}" role="presentation"></span>
${deviceName}
</h3>
${when(
isSearchResultsEmpty,
() => html`
<div
slot=${ifDefined(this.recentBrowsing ? null : "main")}
class="blackbox notabs search-results-empty"
data-l10n-id="firefoxview-search-results-empty"
data-l10n-args=${JSON.stringify({
query: escapeHtmlEntities(this.controller.searchQuery),
})}
></div>
`,
() => html`
<div
slot=${ifDefined(this.recentBrowsing ? null : "main")}
class="blackbox notabs"
data-l10n-id="firefoxview-syncedtabs-device-notabs"
></div>
`
)}`;
return this.recentBrowsing
? template
: html`<card-container
shortPageName=${this.recentBrowsing ? "syncedtabs" : null}
>${template}</card-container
>`;
}
onSearchQuery(e) {
this.controller.searchQuery = e.detail.query;
this.cumulativeSearches = e.detail.query ? this.cumulativeSearches + 1 : 0;
this.showAll = false;
}
deviceTemplate(deviceName, deviceType, tabItems) {
return html`<h3
slot=${!this.recentBrowsing ? "header" : null}
class="device-header"
>
<span class="icon ${deviceType}" role="presentation"></span>
${deviceName}
</h3>
<syncedtabs-tab-list
slot="main"
.hasPopup=${"menu"}
.tabItems=${ifDefined(tabItems)}
.searchQuery=${this.controller.searchQuery}
.maxTabsLength=${this.showAll ? -1 : this.maxTabsLength}
@fxview-tab-list-primary-action=${this.onOpenLink}
@fxview-tab-list-secondary-action=${this.onContextMenu}
@fxview-tab-list-tertiary-action=${this.onCloseTab}
secondaryActionClass="options-button"
>
${this.panelListTemplate()}
</syncedtabs-tab-list>`;
}
generateTabList() {
let renderArray = [];
let renderInfo = this.controller.getRenderInfo();
for (let id in renderInfo) {
let tabItems = renderInfo[id].tabItems;
if (tabItems.length) {
const template = this.recentBrowsing
? this.deviceTemplate(
renderInfo[id].name,
renderInfo[id].deviceType,
tabItems
)
: html`<card-container
shortPageName=${this.recentBrowsing ? "syncedtabs" : null}
>${this.deviceTemplate(
renderInfo[id].name,
renderInfo[id].deviceType,
tabItems
)}
</card-container>`;
renderArray.push(template);
if (this.isShowAllLinkVisible(tabItems)) {
renderArray.push(
html` <div class="show-all-link-container">
<div
class="show-all-link"
@click=${this.enableShowAll}
@keydown=${this.enableShowAll}
data-l10n-id="firefoxview-show-all"
tabindex="0"
role="link"
></div>
</div>`
);
}
} else {
// Check renderInfo[id].tabs.length to determine whether to display an
// empty tab list message or empty search results message.
// If there are no synced tabs, we always display the empty tab list
// message, even if there is an active search query.
renderArray.push(
this.noDeviceTabsTemplate(
renderInfo[id].name,
renderInfo[id].deviceType,
Boolean(renderInfo[id].tabs.length)
)
);
}
}
return renderArray;
}
isShowAllLinkVisible(tabItems) {
return (
this.recentBrowsing &&
this.controller.searchQuery &&
tabItems.length > this.maxTabsLength &&
!this.showAll
);
}
enableShowAll(event) {
if (
event.type == "click" ||
(event.type == "keydown" && event.code == "Enter") ||
(event.type == "keydown" && event.code == "Space")
) {
event.preventDefault();
this.showAll = true;
Glean.firefoxviewNext.searchShowAllShowallbutton.record({
section: "syncedtabs",
});
}
}
generateCardContent() {
const cardProperties = this.controller.getMessageCard();
return cardProperties
? this.generateMessageCard(cardProperties)
: this.generateTabList();
}
render() {
this.open =
!TabsSetupFlowManager.isTabSyncSetupComplete ||
Services.prefs.getBoolPref(UI_OPEN_STATE, true);
let renderArray = [];
renderArray.push(
html` <link
rel="stylesheet"
href="chrome://browser/content/firefoxview/view-syncedtabs.css"
/>`
);
renderArray.push(
html` <link
rel="stylesheet"
href="chrome://browser/content/firefoxview/firefoxview.css"
/>`
);
if (!this.recentBrowsing) {
renderArray.push(
html`<div class="sticky-container bottom-fade">
<h2
class="page-header"
data-l10n-id="firefoxview-synced-tabs-header"
></h2>
<div class="syncedtabs-header">
<div>
<fxview-search-textbox
data-l10n-id="firefoxview-search-text-box-tabs"
data-l10n-attrs="placeholder"
@fxview-search-textbox-query=${this.onSearchQuery}
.size=${this.searchTextboxSize}
pageName=${this.recentBrowsing
? "recentbrowsing"
: "syncedtabs"}
></fxview-search-textbox>
</div>
${when(
this.controller.currentSetupStateIndex === 4,
() => html`
<button
class="small-button"
data-action="add-device"
@click=${e => this.controller.handleEvent(e)}
>
<img
class="icon"
role="presentation"
src="chrome://global/skin/icons/plus.svg"
alt="plus sign"
/><span
data-l10n-id="firefoxview-syncedtabs-connect-another-device"
data-action="add-device"
></span>
</button>
`
)}
</div>
</div>`
);
}
if (this.recentBrowsing) {
renderArray.push(
html`<card-container
preserveCollapseState
shortPageName="syncedtabs"
?showViewAll=${this.controller.currentSetupStateIndex == 4 &&
this.controller.currentSyncedTabs.length}
?isEmptyState=${!this.controller.currentSyncedTabs.length}
>
>
<h3
slot="header"
data-l10n-id="firefoxview-synced-tabs-header"
class="recentbrowsing-header"
></h3>
<div slot="main">${this.generateCardContent()}</div>
</card-container>`
);
} else {
renderArray.push(
html`<div class="cards-container">${this.generateCardContent()}</div>`
);
}
return renderArray;
}
updated() {
this.fullyUpdated = true;
this.toggleVisibilityInCardContainer();
}
}
customElements.define("view-syncedtabs", SyncedTabsInView);