diff options
Diffstat (limited to '')
-rw-r--r-- | browser/components/sidebar/browser-sidebar.js (renamed from browser/base/content/browser-sidebar.js) | 120 | ||||
-rw-r--r-- | browser/components/sidebar/jar.mn | 9 | ||||
-rw-r--r-- | browser/components/sidebar/sidebar-history.html | 34 | ||||
-rw-r--r-- | browser/components/sidebar/sidebar-history.mjs | 201 | ||||
-rw-r--r-- | browser/components/sidebar/sidebar-launcher.css | 34 | ||||
-rw-r--r-- | browser/components/sidebar/sidebar-launcher.mjs | 169 | ||||
-rw-r--r-- | browser/components/sidebar/sidebar-page.mjs | 45 | ||||
-rw-r--r-- | browser/components/sidebar/sidebar-syncedtabs.html | 45 | ||||
-rw-r--r-- | browser/components/sidebar/sidebar-syncedtabs.mjs | 191 | ||||
-rw-r--r-- | browser/components/sidebar/sidebar.css | 27 | ||||
-rw-r--r-- | browser/components/sidebar/sidebar.ftl | 26 |
11 files changed, 876 insertions, 25 deletions
diff --git a/browser/base/content/browser-sidebar.js b/browser/components/sidebar/browser-sidebar.js index 2d730700a6..55664f8cfc 100644 --- a/browser/base/content/browser-sidebar.js +++ b/browser/components/sidebar/browser-sidebar.js @@ -36,7 +36,9 @@ var SidebarUI = { "viewHistorySidebar", makeSidebar({ elementId: "sidebar-switcher-history", - url: "chrome://browser/content/places/historySidebar.xhtml", + url: this.sidebarRevampEnabled + ? "chrome://browser/content/sidebar/sidebar-history.html" + : "chrome://browser/content/places/historySidebar.xhtml", menuId: "menu_historySidebar", triggerButtonId: "appMenuViewHistorySidebar", }), @@ -45,10 +47,20 @@ var SidebarUI = { "viewTabsSidebar", makeSidebar({ elementId: "sidebar-switcher-tabs", - url: "chrome://browser/content/syncedtabs/sidebar.xhtml", + url: this.sidebarRevampEnabled + ? "chrome://browser/content/sidebar/sidebar-syncedtabs.html" + : "chrome://browser/content/syncedtabs/sidebar.xhtml", menuId: "menu_tabsSidebar", }), ], + [ + "viewMegalistSidebar", + makeSidebar({ + elementId: "sidebar-switcher-megalist", + url: "chrome://global/content/megalist/megalist.html", + menuId: "menu_megalistSidebar", + }), + ], ])); }, @@ -98,7 +110,7 @@ var SidebarUI = { return this._inited; }, - init() { + async init() { this._box = document.getElementById("sidebar-box"); this._splitter = document.getElementById("sidebar-splitter"); this._reversePositionButton = document.getElementById( @@ -108,12 +120,18 @@ var SidebarUI = { this._switcherTarget = document.getElementById("sidebar-switcher-target"); this._switcherArrow = document.getElementById("sidebar-switcher-arrow"); - this._switcherTarget.addEventListener("command", () => { - this.toggleSwitcherPanel(); - }); - this._switcherTarget.addEventListener("keydown", event => { - this.handleKeydown(event); - }); + if (this.sidebarRevampEnabled) { + await import("chrome://browser/content/sidebar/sidebar-launcher.mjs"); + document.getElementById("sidebar-launcher").hidden = false; + document.getElementById("sidebar-header").hidden = true; + } else { + this._switcherTarget.addEventListener("command", () => { + this.toggleSwitcherPanel(); + }); + this._switcherTarget.addEventListener("keydown", event => { + this.handleKeydown(event); + }); + } this._inited = true; @@ -122,6 +140,32 @@ var SidebarUI = { this._initDeferred.resolve(); }, + toggleMegalistItem() { + const sideMenuPopupItem = document.getElementById( + "sidebar-switcher-megalist" + ); + sideMenuPopupItem.style.display = Services.prefs.getBoolPref( + "browser.megalist.enabled", + false + ) + ? "" + : "none"; + }, + + setMegalistMenubarVisibility(aEvent) { + const popup = aEvent.target; + if (popup != aEvent.currentTarget) { + return; + } + + // Show the megalist item if enabled + const megalistItem = popup.querySelector("#menu_megalistSidebar"); + megalistItem.hidden = !Services.prefs.getBoolPref( + "browser.megalist.enabled", + false + ); + }, + uninit() { // If this is the last browser window, persist various values that should be // remembered for after a restart / reopening a browser window. @@ -159,7 +203,7 @@ var SidebarUI = { /** * The handler for Services.obs.addObserver. - **/ + */ observe(_subject, topic, _data) { switch (topic) { case "intl:app-locales-changed": { @@ -216,6 +260,7 @@ var SidebarUI = { /** * Handles keydown on the the switcherTarget button + * * @param {Event} event */ handleKeydown(event) { @@ -241,6 +286,7 @@ var SidebarUI = { }, showSwitcherPanel() { + this.toggleMegalistItem(); this._switcherPanel.addEventListener( "popuphiding", () => { @@ -292,19 +338,25 @@ var SidebarUI = { [...browser.children].forEach((node, i) => { node.style.order = i + 1; }); + let sidebarLauncher = document.querySelector("sidebar-launcher"); if (!this._positionStart) { - // DOM ordering is: | sidebar-box | splitter | appcontent | - // Want to display as: | appcontent | splitter | sidebar-box | - // So we just swap box and appcontent ordering + // DOM ordering is: sidebar-launcher | sidebar-box | splitter | appcontent | + // Want to display as: | appcontent | splitter | sidebar-box | sidebar-launcher + // So we just swap box and appcontent ordering and move sidebar-launcher to the end let appcontent = document.getElementById("appcontent"); let boxOrdinal = this._box.style.order; this._box.style.order = appcontent.style.order; + appcontent.style.order = boxOrdinal; + // the launcher should be on the right of the sidebar-box + sidebarLauncher.style.order = parseInt(this._box.style.order) + 1; // Indicate we've switched ordering to the box this._box.setAttribute("positionend", true); + sidebarLauncher.setAttribute("positionend", true); } else { this._box.removeAttribute("positionend"); + sidebarLauncher.removeAttribute("positionend"); } this.hideSwitcherPanel(); @@ -317,8 +369,9 @@ var SidebarUI = { /** * Try and adopt the status of the sidebar from another window. + * * @param {Window} sourceWindow - Window to use as a source for sidebar status. - * @return true if we adopted the state, or false if the caller should + * @returns {boolean} true if we adopted the state, or false if the caller should * initialize the state itself. */ adoptFromWindow(sourceWindow) { @@ -461,7 +514,7 @@ var SidebarUI = { * @param {string} commandID ID of the sidebar. * @param {DOMNode} [triggerNode] Node, usually a button, that triggered the * visibility toggling of the sidebar. - * @return {Promise} + * @returns {Promise} */ toggle(commandID = this.lastOpenedId, triggerNode) { if ( @@ -508,7 +561,7 @@ var SidebarUI = { * @param {string} commandID ID of the sidebar to use. * @param {DOMNode} [triggerNode] Node, usually a button, that triggered the * showing of the sidebar. - * @return {Promise<boolean>} + * @returns {Promise<boolean>} */ async show(commandID, triggerNode) { let panelType = commandID.substring(4, commandID.length - 7); @@ -537,7 +590,7 @@ var SidebarUI = { * when a window opens (not triggered by user interaction). * * @param {string} commandID ID of the sidebar. - * @return {Promise<boolean>} + * @returns {Promise<boolean>} */ async showInitially(commandID) { let panelType = commandID.substring(4, commandID.length - 7); @@ -559,31 +612,40 @@ var SidebarUI = { * when a window is opened and we don't want to ping telemetry. * * @param {string} commandID ID of the sidebar. - * @return {Promise<void>} + * @returns {Promise<void>} */ _show(commandID) { return new Promise(resolve => { - this.selectMenuItem(commandID); + if (this.sidebarRevampEnabled) { + this._box.dispatchEvent( + new CustomEvent("sidebar-show", { detail: { viewId: commandID } }) + ); + } else { + this.hideSwitcherPanel(); + } + this.selectMenuItem(commandID); this._box.hidden = this._splitter.hidden = false; + // sets the sidebar to the left or right, based on a pref this.setPosition(); - this.hideSwitcherPanel(); - this._box.setAttribute("checked", "true"); this._box.setAttribute("sidebarcommand", commandID); - this.lastOpenedId = commandID; let { url, title, sourceL10nEl } = this.sidebars.get(commandID); + + // use to live update <tree> elements if the locale changes + this.lastOpenedId = commandID; this.title = title; - // Keep the title element in sync with any l10n changes. + // Keep the title element in the switcher in sync with any l10n changes. this.observeTitleChanges(sourceL10nEl); + this.browser.setAttribute("src", url); // kick off async load if (this.browser.contentDocument.location.href != url) { this.browser.addEventListener( "load", - event => { + () => { // We're handling the 'load' event before it bubbles up to the usual // (non-capturing) event handlers. Let it bubble up before resolving. setTimeout(() => { @@ -616,7 +678,9 @@ var SidebarUI = { } this.hideSwitcherPanel(); - + if (this.sidebarRevampEnabled) { + this._box.dispatchEvent(new CustomEvent("sidebar-hide")); + } this.selectMenuItem(""); // Replace the document currently displayed in the sidebar with about:blank @@ -672,3 +736,9 @@ XPCOMUtils.defineLazyPreferenceGetter( true, SidebarUI.setPosition.bind(SidebarUI) ); +XPCOMUtils.defineLazyPreferenceGetter( + SidebarUI, + "sidebarRevampEnabled", + "sidebar.revamp", + false +); diff --git a/browser/components/sidebar/jar.mn b/browser/components/sidebar/jar.mn index c3d7f0cbcf..8a7071ca72 100644 --- a/browser/components/sidebar/jar.mn +++ b/browser/components/sidebar/jar.mn @@ -3,3 +3,12 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. browser.jar: + content/browser/sidebar/browser-sidebar.js + content/browser/sidebar/sidebar-launcher.css + content/browser/sidebar/sidebar-launcher.mjs + content/browser/sidebar/sidebar-history.html + content/browser/sidebar/sidebar-history.mjs + content/browser/sidebar/sidebar-page.mjs + content/browser/sidebar/sidebar-syncedtabs.html + content/browser/sidebar/sidebar-syncedtabs.mjs + content/browser/sidebar/sidebar.css diff --git a/browser/components/sidebar/sidebar-history.html b/browser/components/sidebar/sidebar-history.html new file mode 100644 index 0000000000..f1df5c507a --- /dev/null +++ b/browser/components/sidebar/sidebar-history.html @@ -0,0 +1,34 @@ +<!-- 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/. --> + +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8" /> + <meta + http-equiv="Content-Security-Policy" + content="default-src resource: chrome:; object-src 'none'; img-src data: chrome:;" + /> + <meta name="color-scheme" content="light dark" /> + <title data-l10n-id="firefoxview-page-title"></title> + <link rel="localization" href="branding/brand.ftl" /> + <link rel="localization" href="toolkit/branding/accounts.ftl" /> + <link rel="localization" href="browser/firefoxView.ftl" /> + <link rel="localization" href="preview/sidebar.ftl" /> + <link rel="localization" href="toolkit/branding/brandings.ftl" /> + <link + rel="stylesheet" + href="chrome://browser/content/sidebar/sidebar.css" + /> + <script + type="module" + src="chrome://browser/content/sidebar/sidebar-history.mjs" + ></script> + <script src="chrome://browser/content/contentTheme.js"></script> + </head> + + <body> + <sidebar-history /> + </body> +</html> diff --git a/browser/components/sidebar/sidebar-history.mjs b/browser/components/sidebar/sidebar-history.mjs new file mode 100644 index 0000000000..6c662b2c6f --- /dev/null +++ b/browser/components/sidebar/sidebar-history.mjs @@ -0,0 +1,201 @@ +/* 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/. */ + +import { html, when } from "chrome://global/content/vendor/lit.all.mjs"; + +import { SidebarPage } from "./sidebar-page.mjs"; + +// eslint-disable-next-line import/no-unassigned-import +import "chrome://browser/content/firefoxview/fxview-search-textbox.mjs"; +// eslint-disable-next-line import/no-unassigned-import +import "chrome://browser/content/firefoxview/fxview-tab-list.mjs"; +// eslint-disable-next-line import/no-unassigned-import +import "chrome://global/content/elements/moz-card.mjs"; +import { HistoryController } from "chrome://browser/content/firefoxview/HistoryController.mjs"; +import { navigateToLink } from "chrome://browser/content/firefoxview/helpers.mjs"; + +const NEVER_REMEMBER_HISTORY_PREF = "browser.privatebrowsing.autostart"; + +export class SidebarHistory extends SidebarPage { + constructor() { + super(); + this._started = false; + // Setting maxTabsLength to -1 for no max + this.maxTabsLength = -1; + } + + controller = new HistoryController(this, { + component: "sidebar", + }); + + connectedCallback() { + super.connectedCallback(); + this.controller.updateAllHistoryItems(); + } + + onPrimaryAction(e) { + navigateToLink(e); + } + + deleteFromHistory() { + this.controller.deleteFromHistory(); + } + + /** + * The template to use for cards-container. + */ + get cardsTemplate() { + if (this.controller.searchResults) { + return this.#searchResultsTemplate(); + } else if (this.controller.allHistoryItems.size) { + return this.#historyCardsTemplate(); + } + return this.#emptyMessageTemplate(); + } + + #historyCardsTemplate() { + let cardsTemplate = []; + this.controller.historyMapByDate.forEach(historyItem => { + if (historyItem.items.length) { + let dateArg = JSON.stringify({ date: historyItem.items[0].time }); + cardsTemplate.push(html`<moz-card + type="accordion" + data-l10n-attrs="heading" + data-l10n-id=${historyItem.l10nId} + data-l10n-args=${dateArg} + > + <div> + <fxview-tab-list + compactRows + class="with-context-menu" + maxTabsLength=${this.maxTabsLength} + .tabItems=${this.getTabItems(historyItem.items)} + @fxview-tab-list-primary-action=${this.onPrimaryAction} + .updatesPaused=${false} + > + </fxview-tab-list> + </div> + </moz-card>`); + } + }); + return cardsTemplate; + } + + #emptyMessageTemplate() { + let descriptionHeader; + let descriptionLabels; + let descriptionLink; + if (Services.prefs.getBoolPref(NEVER_REMEMBER_HISTORY_PREF, false)) { + // History pref set to never remember history + descriptionHeader = "firefoxview-dont-remember-history-empty-header"; + descriptionLabels = [ + "firefoxview-dont-remember-history-empty-description", + "firefoxview-dont-remember-history-empty-description-two", + ]; + descriptionLink = { + url: "about:preferences#privacy", + name: "history-settings-url-two", + }; + } else { + descriptionHeader = "firefoxview-history-empty-header"; + descriptionLabels = [ + "firefoxview-history-empty-description", + "firefoxview-history-empty-description-two", + ]; + descriptionLink = { + url: "about:preferences#privacy", + name: "history-settings-url", + }; + } + return html` + <fxview-empty-state + headerLabel=${descriptionHeader} + .descriptionLabels=${descriptionLabels} + .descriptionLink=${descriptionLink} + class="empty-state history" + ?isSelectedTab=${this.selectedTab} + mainImageUrl="chrome://browser/content/firefoxview/history-empty.svg" + > + </fxview-empty-state> + `; + } + + #searchResultsTemplate() { + return html` <moz-card + data-l10n-attrs="heading" + data-l10n-id="sidebar-search-results-header" + data-l10n-args=${JSON.stringify({ + query: this.controller.searchQuery, + })} + > + <div> + ${when( + this.controller.searchResults.length, + () => + html`<h3 + slot="secondary-header" + data-l10n-id="firefoxview-search-results-count" + data-l10n-args="${JSON.stringify({ + count: this.controller.searchResults.length, + })}" + ></h3>` + )} + <fxview-tab-list + compactRows + maxTabsLength="-1" + .searchQuery=${this.controller.searchQuery} + .tabItems=${this.getTabItems(this.controller.searchResults)} + @fxview-tab-list-primary-action=${this.onPrimaryAction} + .updatesPaused=${false} + > + </fxview-tab-list> + </div> + </moz-card>`; + } + + async onChangeSortOption(e) { + await this.controller.onChangeSortOption(e); + } + + async onSearchQuery(e) { + await this.controller.onSearchQuery(e); + } + + getTabItems(items) { + return items.map(item => ({ + ...item, + secondaryL10nId: null, + secondaryL10nArgs: null, + })); + } + + render() { + return html` + ${this.stylesheet()} + <div class="container"> + <div class="history-sort-option"> + <div class="history-sort-option"> + <fxview-search-textbox + data-l10n-id="firefoxview-search-text-box-history" + data-l10n-attrs="placeholder" + @fxview-search-textbox-query=${this.onSearchQuery} + .size=${15} + ></fxview-search-textbox> + </div> + </div> + ${this.cardsTemplate} + </div> + `; + } + + willUpdate() { + if (this.controller.allHistoryItems.size) { + // onChangeSortOption() will update history data once it has been fetched + // from the API. + this.controller.createHistoryMaps(); + } + } +} + +customElements.define("sidebar-history", SidebarHistory); diff --git a/browser/components/sidebar/sidebar-launcher.css b/browser/components/sidebar/sidebar-launcher.css new file mode 100644 index 0000000000..b033a650b3 --- /dev/null +++ b/browser/components/sidebar/sidebar-launcher.css @@ -0,0 +1,34 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +.wrapper { + display: grid; + grid-template-rows: auto 1fr; + box-sizing: border-box; + height: 100%; + padding: var(--space-medium); + border-inline-end: 1px solid var(--chrome-content-separator-color); + background-color: var(--sidebar-background-color); + color: var(--sidebar-text-color); + :host([positionend]) & { + border-inline-start: 1px solid var(--chrome-content-separator-color); + border-inline-end: none;; + } +} + +:host([positionend]) { + .wrapper { + border-inline-start: 1px solid var(--chrome-content-separator-color); + } +} + +.actions-list { + display: flex; + flex-direction: column; + justify-content: end; +} + +.icon-button::part(button) { + background-image: var(--action-icon); +} diff --git a/browser/components/sidebar/sidebar-launcher.mjs b/browser/components/sidebar/sidebar-launcher.mjs new file mode 100644 index 0000000000..85eb94b6ca --- /dev/null +++ b/browser/components/sidebar/sidebar-launcher.mjs @@ -0,0 +1,169 @@ +/* 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/. */ + +import { + html, + ifDefined, + styleMap, +} from "chrome://global/content/vendor/lit.all.mjs"; +import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; + +// eslint-disable-next-line import/no-unassigned-import +import "chrome://global/content/elements/moz-button.mjs"; + +/** + * Vertical strip attached to the launcher that provides an entry point + * to various sidebar panels. + * + */ +export default class SidebarLauncher extends MozLitElement { + static properties = { + topActions: { type: Array }, + bottomActions: { type: Array }, + selectedView: { type: String }, + open: { type: Boolean }, + }; + + constructor() { + super(); + this.topActions = [ + { + icon: `url("chrome://browser/skin/insights.svg")`, + view: null, + l10nId: "sidebar-launcher-insights", + }, + ]; + + this.bottomActions = [ + { + l10nId: "sidebar-menu-history", + icon: `url("chrome://browser/content/firefoxview/view-history.svg")`, + view: "viewHistorySidebar", + }, + { + l10nId: "sidebar-menu-bookmarks", + icon: `url("chrome://browser/skin/bookmark-hollow.svg")`, + view: "viewBookmarksSidebar", + }, + { + l10nId: "sidebar-menu-synced-tabs", + icon: `url("chrome://browser/skin/device-phone.svg")`, + view: "viewTabsSidebar", + }, + ]; + + this.selectedView = window.SidebarUI.currentID; + this.open = window.SidebarUI.isOpen; + this.menuMutationObserver = new MutationObserver(() => + this.#setExtensionItems() + ); + } + + connectedCallback() { + super.connectedCallback(); + this._sidebarBox = document.getElementById("sidebar-box"); + this._sidebarBox.addEventListener("sidebar-show", this); + this._sidebarBox.addEventListener("sidebar-hide", this); + this._sidebarMenu = document.getElementById("viewSidebarMenu"); + + this.menuMutationObserver.observe(this._sidebarMenu, { + childList: true, + subtree: true, + }); + this.#setExtensionItems(); + } + + disconnectedCallback() { + super.disconnectedCallback(); + this._sidebarBox.removeEventListener("sidebar-show", this); + this._sidebarBox.removeEventListener("sidebar-hide", this); + this.menuMutationObserver.disconnect(); + } + + getImageUrl(icon, targetURI) { + if (window.IS_STORYBOOK) { + return `chrome://global/skin/icons/defaultFavicon.svg`; + } + if (!icon) { + if (targetURI?.startsWith("moz-extension")) { + return "chrome://mozapps/skin/extensions/extension.svg"; + } + return `chrome://global/skin/icons/defaultFavicon.svg`; + } + // If the icon is not for website (doesn't begin with http), we + // display it directly. Otherwise we go through the page-icon + // protocol to try to get a cached version. We don't load + // favicons directly. + if (icon.startsWith("http")) { + return `page-icon:${targetURI}`; + } + return icon; + } + + #setExtensionItems() { + for (let item of this._sidebarMenu.children) { + if (item.id.endsWith("-sidebar-action")) { + this.topActions.push({ + tooltiptext: item.label, + icon: item.style.getPropertyValue("--webextension-menuitem-image"), + view: item.id.slice("menubar_menu_".length), + }); + } + } + } + + handleEvent(e) { + switch (e.type) { + case "sidebar-show": + this.selectedView = e.detail.viewId; + this.open = true; + break; + case "sidebar-hide": + this.open = false; + break; + } + } + + showView(e) { + let view = e.target.getAttribute("view"); + window.SidebarUI.toggle(view); + } + + buttonType(action) { + return this.open && action.view == this.selectedView + ? "icon" + : "icon ghost"; + } + + entrypointTemplate(action) { + return html`<moz-button + class="icon-button" + type=${this.buttonType(action)} + view=${action.view} + @click=${action.view ? this.showView : null} + title=${ifDefined(action.tooltiptext)} + data-l10n-id=${ifDefined(action.l10nId)} + style=${styleMap({ "--action-icon": action.icon })} + > + </moz-button>`; + } + + render() { + return html` + <link + rel="stylesheet" + href="chrome://browser/content/sidebar/sidebar-launcher.css" + /> + <div class="wrapper"> + <div class="top-actions actions-list"> + ${this.topActions.map(action => this.entrypointTemplate(action))} + </div> + <div class="bottom-actions actions-list"> + ${this.bottomActions.map(action => this.entrypointTemplate(action))} + </div> + </div> + `; + } +} +customElements.define("sidebar-launcher", SidebarLauncher); diff --git a/browser/components/sidebar/sidebar-page.mjs b/browser/components/sidebar/sidebar-page.mjs new file mode 100644 index 0000000000..157298a561 --- /dev/null +++ b/browser/components/sidebar/sidebar-page.mjs @@ -0,0 +1,45 @@ +/* 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/. */ + +import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; +import { html } from "chrome://global/content/vendor/lit.all.mjs"; + +export class SidebarPage extends MozLitElement { + constructor() { + super(); + this.clearDocument = this.clearDocument.bind(this); + } + + connectedCallback() { + super.connectedCallback(); + this.ownerGlobal.addEventListener("beforeunload", this.clearDocument); + } + + disconnectedCallback() { + super.disconnectedCallback(); + this.ownerGlobal.removeEventListener("beforeunload", this.clearDocument); + } + + /** + * Clear out the document so the disconnectedCallback() will trigger properly + * and all of the custom elements can cleanup. + */ + clearDocument() { + this.ownerGlobal.document.body.textContent = ""; + } + + /** + * The common stylesheet for all sidebar pages. + * + * @returns {TemplateResult} + */ + stylesheet() { + return html` + <link + rel="stylesheet" + href="chrome://browser/content/sidebar/sidebar.css" + /> + `; + } +} diff --git a/browser/components/sidebar/sidebar-syncedtabs.html b/browser/components/sidebar/sidebar-syncedtabs.html new file mode 100644 index 0000000000..6c4874b9ea --- /dev/null +++ b/browser/components/sidebar/sidebar-syncedtabs.html @@ -0,0 +1,45 @@ +<!-- 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/. --> + +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8" /> + <meta + http-equiv="Content-Security-Policy" + content="default-src resource: chrome:; object-src 'none'; img-src data: chrome:;" + /> + <meta name="color-scheme" content="light dark" /> + <link rel="localization" href="branding/brand.ftl" /> + <link rel="localization" href="browser/firefoxView.ftl" /> + <link rel="localization" href="toolkit/branding/accounts.ftl" /> + <link rel="localization" href="toolkit/branding/brandings.ftl" /> + <link rel="stylesheet" href="chrome://global/skin/global.css" /> + <script + type="module" + src="chrome://global/content/elements/moz-card.mjs" + ></script> + <script + type="module" + src="chrome://browser/content/firefoxview/fxview-empty-state.mjs" + ></script> + <script + type="module" + src="chrome://browser/content/firefoxview/fxview-search-textbox.mjs" + ></script> + <script + type="module" + src="chrome://browser/content/firefoxview/fxview-tab-list.mjs" + ></script> + <script + type="module" + src="chrome://browser/content/sidebar/sidebar-syncedtabs.mjs" + ></script> + <script src="chrome://browser/content/contentTheme.js"></script> + </head> + + <body> + <sidebar-syncedtabs /> + </body> +</html> diff --git a/browser/components/sidebar/sidebar-syncedtabs.mjs b/browser/components/sidebar/sidebar-syncedtabs.mjs new file mode 100644 index 0000000000..4c3bd9dc46 --- /dev/null +++ b/browser/components/sidebar/sidebar-syncedtabs.mjs @@ -0,0 +1,191 @@ +/* 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, { + SyncedTabsController: "resource:///modules/SyncedTabsController.sys.mjs", +}); + +import { html, ifDefined } from "chrome://global/content/vendor/lit.all.mjs"; +import { + escapeHtmlEntities, + navigateToLink, +} from "chrome://browser/content/firefoxview/helpers.mjs"; + +import { SidebarPage } from "./sidebar-page.mjs"; + +class SyncedTabsInSidebar extends SidebarPage { + controller = new lazy.SyncedTabsController(this); + + constructor() { + super(); + this.onSearchQuery = this.onSearchQuery.bind(this); + } + + connectedCallback() { + super.connectedCallback(); + this.controller.addSyncObservers(); + this.controller.updateStates(); + } + + disconnectedCallback() { + super.disconnectedCallback(); + this.controller.removeSyncObservers(); + } + + /** + * The template shown when the list of synced devices is currently + * unavailable. + * + * @param {object} options + * @param {string} options.action + * @param {string} options.buttonLabel + * @param {string[]} options.descriptionArray + * @param {string} options.descriptionLink + * @param {boolean} options.error + * @param {string} options.header + * @param {string} options.headerIconUrl + * @param {string} options.mainImageUrl + * @returns {TemplateResult} + */ + messageCardTemplate({ + action, + buttonLabel, + descriptionArray, + descriptionLink, + error, + header, + headerIconUrl, + mainImageUrl, + }) { + return html` + <fxview-empty-state + headerLabel=${header} + .descriptionLabels=${descriptionArray} + .descriptionLink=${ifDefined(descriptionLink)} + class="empty-state synced-tabs error" + isSelectedTab + mainImageUrl="${ifDefined(mainImageUrl)}" + ?errorGrayscale=${error} + headerIconUrl="${ifDefined(headerIconUrl)}" + 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)} + aria-details="empty-container" + ></button> + </fxview-empty-state> + `; + } + + /** + * The template shown for a device that has tabs. + * + * @param {string} deviceName + * @param {string} deviceType + * @param {Array} tabItems + * @returns {TemplateResult} + */ + deviceTemplate(deviceName, deviceType, tabItems) { + return html`<moz-card + type="accordion" + .heading=${deviceName} + icon + class=${deviceType} + > + <fxview-tab-list + compactRows + .tabItems=${ifDefined(tabItems)} + .updatesPaused=${false} + .searchQuery=${this.controller.searchQuery} + @fxview-tab-list-primary-action=${navigateToLink} + /> + </moz-card>`; + } + + /** + * The template shown for a device that has no tabs. + * + * @param {string} deviceName + * @param {string} deviceType + * @returns {TemplateResult} + */ + noDeviceTabsTemplate(deviceName, deviceType) { + return html`<moz-card + .heading=${deviceName} + icon + class=${deviceType} + data-l10n-id="firefoxview-syncedtabs-device-notabs" + > + </moz-card>`; + } + + /** + * The template shown for a device that has tabs, but no tabs that match the + * current search query. + * + * @param {string} deviceName + * @param {string} deviceType + * @returns {TemplateResult} + */ + noSearchResultsTemplate(deviceName, deviceType) { + return html`<moz-card + .heading=${deviceName} + icon + class=${deviceType} + data-l10n-id="firefoxview-search-results-empty" + data-l10n-args=${JSON.stringify({ + query: escapeHtmlEntities(this.controller.searchQuery), + })} + > + </moz-card>`; + } + + /** + * The template shown for the list of synced devices. + * + * @returns {TemplateResult[]} + */ + deviceListTemplate() { + return Object.values(this.controller.getRenderInfo()).map( + ({ name: deviceName, deviceType, tabItems, tabs }) => { + if (tabItems.length) { + return this.deviceTemplate(deviceName, deviceType, tabItems); + } else if (tabs.length) { + return this.noSearchResultsTemplate(deviceName, deviceType); + } + return this.noDeviceTabsTemplate(deviceName, deviceType); + } + ); + } + + render() { + const messageCard = this.controller.getMessageCard(); + if (messageCard) { + return [this.stylesheet(), this.messageCardTemplate(messageCard)]; + } + return html` + ${this.stylesheet()} + <fxview-search-textbox + data-l10n-id="firefoxview-search-text-box-syncedtabs" + data-l10n-attrs="placeholder" + @fxview-search-textbox-query=${this.onSearchQuery} + size="15" + ></fxview-search-textbox> + ${this.deviceListTemplate()} + `; + } + + onSearchQuery(e) { + this.controller.searchQuery = e.detail.query; + this.requestUpdate(); + } +} + +customElements.define("sidebar-syncedtabs", SyncedTabsInSidebar); diff --git a/browser/components/sidebar/sidebar.css b/browser/components/sidebar/sidebar.css new file mode 100644 index 0000000000..34d43aa850 --- /dev/null +++ b/browser/components/sidebar/sidebar.css @@ -0,0 +1,27 @@ +/* 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/. */ + +@import url("chrome://global/skin/global.css"); + +:root { + background-color: var(--lwt-sidebar-background-color); + color: var(--lwt-sidebar-text-color); +} + +moz-card { + margin-block-start: var(--space-medium); + + &.phone::part(icon), + &.mobile::part(icon) { + background-image: url('chrome://browser/skin/device-phone.svg'); + } + + &.desktop::part(icon) { + background-image: url('chrome://browser/skin/device-desktop.svg'); + } + + &.tablet::part(icon) { + background-image: url('chrome://browser/skin/device-tablet.svg'); + } +} diff --git a/browser/components/sidebar/sidebar.ftl b/browser/components/sidebar/sidebar.ftl new file mode 100644 index 0000000000..2a5ef75d83 --- /dev/null +++ b/browser/components/sidebar/sidebar.ftl @@ -0,0 +1,26 @@ +# 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/. + +sidebar-launcher-insights = + .title = Insights + +## Variables: +## $date (string) - Date to be formatted based on locale + +sidebar-history-date-today = + .heading = Today — { DATETIME($date, dateStyle: "full") } +sidebar-history-date-yesterday = + .heading = Yesterday — { DATETIME($date, dateStyle: "full") } +sidebar-history-date-this-month = + .heading = { DATETIME($date, dateStyle: "full") } +sidebar-history-date-prev-month = + .heading = { DATETIME($date, month: "long", year: "numeric") } + +## + +# "Search" is a noun (as in "Results of the search for") +# Variables: +# $query (String) - The search query used for searching through browser history. +sidebar-search-results-header = + .heading = Search results for “{ $query }” |