1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 ChromeUtils.defineESModuleGetters(lazy, {
7 SyncedTabsController: "resource:///modules/SyncedTabsController.sys.mjs",
10 const { TabsSetupFlowManager } = ChromeUtils.importESModule(
11 "resource:///modules/firefox-view-tabs-setup-manager.sys.mjs"
18 } from "chrome://global/content/vendor/lit.all.mjs";
19 import { ViewPage } from "./viewpage.mjs";
22 MAX_TABS_FOR_RECENT_BROWSING,
24 } from "./helpers.mjs";
25 // eslint-disable-next-line import/no-unassigned-import
26 import "chrome://browser/content/firefoxview/syncedtabs-tab-list.mjs";
28 const UI_OPEN_STATE = "browser.tabs.firefox-view.ui-state.tab-pickup.open";
30 class SyncedTabsInView extends ViewPage {
31 controller = new lazy.SyncedTabsController(this, {
33 pairDeviceCallback: () =>
34 Glean.firefoxviewNext.fxaMobileSync.record({
35 has_devices: TabsSetupFlowManager.secondaryDeviceConnected,
37 signupCallback: () => Glean.firefoxviewNext.fxaContinueSync.record(),
42 this._started = false;
43 this._id = Math.floor(Math.random() * 10e6);
44 if (this.recentBrowsing) {
45 this.maxTabsLength = MAX_TABS_FOR_RECENT_BROWSING;
47 // Setting maxTabsLength to -1 for no max
48 this.maxTabsLength = -1;
50 this.fullyUpdated = false;
52 this.cumulativeSearches = 0;
53 this.onSearchQuery = this.onSearchQuery.bind(this);
57 ...ViewPage.properties,
58 showAll: { type: Boolean },
59 cumulativeSearches: { type: Number },
63 cardEls: { all: "card-container" },
64 emptyState: "fxview-empty-state",
65 searchTextbox: "fxview-search-textbox",
66 tabLists: { all: "syncedtabs-tab-list" },
74 this.controller.addSyncObservers();
75 this.controller.updateStates();
76 this.onVisibilityChange();
78 if (this.recentBrowsing) {
79 this.recentBrowsingElement.addEventListener(
80 "fxview-search-textbox-query",
90 this._started = false;
91 TabsSetupFlowManager.updateViewVisibility(this._id, "unloaded");
92 this.onVisibilityChange();
93 this.controller.removeSyncObservers();
95 if (this.recentBrowsing) {
96 this.recentBrowsingElement.removeEventListener(
97 "fxview-search-textbox-query",
103 disconnectedCallback() {
104 super.disconnectedCallback();
108 viewVisibleCallback() {
112 viewHiddenCallback() {
116 onVisibilityChange() {
117 const isOpen = this.open;
118 const isVisible = this.isVisible;
119 if (isVisible && isOpen) {
121 TabsSetupFlowManager.updateViewVisibility(this._id, "visible");
123 TabsSetupFlowManager.updateViewVisibility(
125 isVisible ? "closed" : "hidden"
129 this.toggleVisibilityInCardContainer();
132 generateMessageCard({
142 headerLabel=${header}
143 .descriptionLabels=${descriptionArray}
144 .descriptionLink=${ifDefined(descriptionLink)}
145 class="empty-state synced-tabs error"
146 ?isSelectedTab=${this.selectedTab}
147 ?isInnerCard=${this.recentBrowsing}
148 mainImageUrl="${ifDefined(mainImageUrl)}"
153 slot="primary-action"
154 ?hidden=${!buttonLabel}
155 data-l10n-id="${ifDefined(buttonLabel)}"
156 data-action="${action}"
157 @click=${e => this.controller.handleEvent(e)}
159 </fxview-empty-state>
164 navigateToLink(event);
166 Glean.firefoxviewNext.syncedTabsTabs.record({
167 page: this.recentBrowsing ? "recentbrowsing" : "syncedtabs",
170 if (this.controller.searchQuery) {
172 .getKeyedHistogramById("FIREFOX_VIEW_CUMULATIVE_SEARCHES")
174 this.recentBrowsing ? "recentbrowsing" : "syncedtabs",
175 this.cumulativeSearches
177 this.cumulativeSearches = 0;
182 this.triggerNode = e.originalTarget;
183 e.target.querySelector("panel-list").toggle(e.detail.originalEvent);
187 const { url, fxaDeviceId, tertiaryActionClass } = e.originalTarget;
188 if (tertiaryActionClass === "dismiss-button") {
189 // Set new pending close tab
190 this.controller.requestCloseRemoteTab(fxaDeviceId, url);
191 } else if (tertiaryActionClass === "undo-button") {
192 // User wants to undo
193 this.controller.removePendingTabToClose(fxaDeviceId, url);
195 this.requestUpdate();
198 panelListTemplate() {
200 <panel-list slot="menu" data-tab-type="syncedtabs">
202 @click=${this.openInNewWindow}
203 data-l10n-id="fxviewtabrow-open-in-window"
204 data-l10n-attrs="accesskey"
207 @click=${this.openInNewPrivateWindow}
208 data-l10n-id="fxviewtabrow-open-in-private-window"
209 data-l10n-attrs="accesskey"
213 @click=${this.copyLink}
214 data-l10n-id="fxviewtabrow-copy-link"
215 data-l10n-attrs="accesskey"
221 noDeviceTabsTemplate(deviceName, deviceType, isSearchResultsEmpty = false) {
222 const template = html`<h3
223 slot=${ifDefined(this.recentBrowsing ? null : "header")}
224 class="device-header"
226 <span class="icon ${deviceType}" role="presentation"></span>
230 isSearchResultsEmpty,
233 slot=${ifDefined(this.recentBrowsing ? null : "main")}
234 class="blackbox notabs search-results-empty"
235 data-l10n-id="firefoxview-search-results-empty"
236 data-l10n-args=${JSON.stringify({
237 query: escapeHtmlEntities(this.controller.searchQuery),
243 slot=${ifDefined(this.recentBrowsing ? null : "main")}
244 class="blackbox notabs"
245 data-l10n-id="firefoxview-syncedtabs-device-notabs"
249 return this.recentBrowsing
251 : html`<card-container
252 shortPageName=${this.recentBrowsing ? "syncedtabs" : null}
253 >${template}</card-container
258 this.controller.searchQuery = e.detail.query;
259 this.cumulativeSearches = e.detail.query ? this.cumulativeSearches + 1 : 0;
260 this.showAll = false;
263 deviceTemplate(deviceName, deviceType, tabItems) {
265 slot=${!this.recentBrowsing ? "header" : null}
266 class="device-header"
268 <span class="icon ${deviceType}" role="presentation"></span>
274 .tabItems=${ifDefined(tabItems)}
275 .searchQuery=${this.controller.searchQuery}
276 .maxTabsLength=${this.showAll ? -1 : this.maxTabsLength}
277 @fxview-tab-list-primary-action=${this.onOpenLink}
278 @fxview-tab-list-secondary-action=${this.onContextMenu}
279 @fxview-tab-list-tertiary-action=${this.onCloseTab}
280 secondaryActionClass="options-button"
282 ${this.panelListTemplate()}
283 </syncedtabs-tab-list>`;
287 let renderArray = [];
288 let renderInfo = this.controller.getRenderInfo();
289 for (let id in renderInfo) {
290 let tabItems = renderInfo[id].tabItems;
291 if (tabItems.length) {
292 const template = this.recentBrowsing
293 ? this.deviceTemplate(
295 renderInfo[id].deviceType,
298 : html`<card-container
299 shortPageName=${this.recentBrowsing ? "syncedtabs" : null}
300 >${this.deviceTemplate(
302 renderInfo[id].deviceType,
306 renderArray.push(template);
307 if (this.isShowAllLinkVisible(tabItems)) {
309 html` <div class="show-all-link-container">
311 class="show-all-link"
312 @click=${this.enableShowAll}
313 @keydown=${this.enableShowAll}
314 data-l10n-id="firefoxview-show-all"
322 // Check renderInfo[id].tabs.length to determine whether to display an
323 // empty tab list message or empty search results message.
324 // If there are no synced tabs, we always display the empty tab list
325 // message, even if there is an active search query.
327 this.noDeviceTabsTemplate(
329 renderInfo[id].deviceType,
330 Boolean(renderInfo[id].tabs.length)
338 isShowAllLinkVisible(tabItems) {
340 this.recentBrowsing &&
341 this.controller.searchQuery &&
342 tabItems.length > this.maxTabsLength &&
347 enableShowAll(event) {
349 event.type == "click" ||
350 (event.type == "keydown" && event.code == "Enter") ||
351 (event.type == "keydown" && event.code == "Space")
353 event.preventDefault();
355 Glean.firefoxviewNext.searchShowAllShowallbutton.record({
356 section: "syncedtabs",
361 generateCardContent() {
362 const cardProperties = this.controller.getMessageCard();
363 return cardProperties
364 ? this.generateMessageCard(cardProperties)
365 : this.generateTabList();
370 !TabsSetupFlowManager.isTabSyncSetupComplete ||
371 Services.prefs.getBoolPref(UI_OPEN_STATE, true);
373 let renderArray = [];
377 href="chrome://browser/content/firefoxview/view-syncedtabs.css"
383 href="chrome://browser/content/firefoxview/firefoxview.css"
387 if (!this.recentBrowsing) {
389 html`<div class="sticky-container bottom-fade">
392 data-l10n-id="firefoxview-synced-tabs-header"
394 <div class="syncedtabs-header">
396 <fxview-search-textbox
397 data-l10n-id="firefoxview-search-text-box-tabs"
398 data-l10n-attrs="placeholder"
399 @fxview-search-textbox-query=${this.onSearchQuery}
400 .size=${this.searchTextboxSize}
401 pageName=${this.recentBrowsing
404 ></fxview-search-textbox>
407 this.controller.currentSetupStateIndex === 4,
411 data-action="add-device"
412 @click=${e => this.controller.handleEvent(e)}
417 src="chrome://global/skin/icons/plus.svg"
420 data-l10n-id="firefoxview-syncedtabs-connect-another-device"
421 data-action="add-device"
431 if (this.recentBrowsing) {
434 preserveCollapseState
435 shortPageName="syncedtabs"
436 ?showViewAll=${this.controller.currentSetupStateIndex == 4 &&
437 this.controller.currentSyncedTabs.length}
438 ?isEmptyState=${!this.controller.currentSyncedTabs.length}
443 data-l10n-id="firefoxview-synced-tabs-header"
444 class="recentbrowsing-header"
446 <div slot="main">${this.generateCardContent()}</div>
451 html`<div class="cards-container">${this.generateCardContent()}</div>`
458 this.fullyUpdated = true;
459 this.toggleVisibilityInCardContainer();
462 customElements.define("view-syncedtabs", SyncedTabsInView);