sidebar-history.mjs (13182B)
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/. */ 4 5 const lazy = {}; 6 7 import { 8 classMap, 9 html, 10 ifDefined, 11 when, 12 nothing, 13 } from "chrome://global/content/vendor/lit.all.mjs"; 14 import { navigateToLink } from "chrome://browser/content/firefoxview/helpers.mjs"; 15 16 import { SidebarPage } from "./sidebar-page.mjs"; 17 18 ChromeUtils.defineESModuleGetters(lazy, { 19 HistoryController: "resource:///modules/HistoryController.sys.mjs", 20 Sanitizer: "resource:///modules/Sanitizer.sys.mjs", 21 SidebarTreeView: 22 "moz-src:///browser/components/sidebar/SidebarTreeView.sys.mjs", 23 PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", 24 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", 25 }); 26 27 const NEVER_REMEMBER_HISTORY_PREF = "browser.privatebrowsing.autostart"; 28 const DAYS_EXPANDED_INITIALLY = 2; 29 30 export class SidebarHistory extends SidebarPage { 31 static queries = { 32 cards: { all: "moz-card" }, 33 emptyState: "fxview-empty-state", 34 lists: { all: "sidebar-tab-list" }, 35 menuButton: ".menu-button", 36 searchTextbox: "moz-input-search", 37 }; 38 39 constructor() { 40 super(); 41 this.handlePopupEvent = this.handlePopupEvent.bind(this); 42 this.controller = new lazy.HistoryController(this, { 43 component: "sidebar", 44 }); 45 this.treeView = new lazy.SidebarTreeView(this); 46 } 47 48 connectedCallback() { 49 super.connectedCallback(); 50 const { document: doc } = this.topWindow; 51 this._menu = doc.getElementById("sidebar-history-menu"); 52 this._menuSortByDate = doc.getElementById("sidebar-history-sort-by-date"); 53 this._menuSortBySite = doc.getElementById("sidebar-history-sort-by-site"); 54 this._menuSortByDateSite = doc.getElementById( 55 "sidebar-history-sort-by-date-and-site" 56 ); 57 this._menuSortByLastVisited = doc.getElementById( 58 "sidebar-history-sort-by-last-visited" 59 ); 60 this._menu.addEventListener("command", this); 61 this._menu.addEventListener("popuphidden", this.handlePopupEvent); 62 this._contextMenu.addEventListener("popupshowing", this); 63 this.addContextMenuListeners(); 64 this.addSidebarFocusedListeners(); 65 this.controller.updateCache(); 66 } 67 68 disconnectedCallback() { 69 super.disconnectedCallback(); 70 this._menu.removeEventListener("command", this); 71 this._menu.removeEventListener("popuphidden", this.handlePopupEvent); 72 this._contextMenu.removeEventListener("popupshowing", this); 73 this.removeContextMenuListeners(); 74 this.removeSidebarFocusedListeners(); 75 } 76 77 handleEvent(e) { 78 switch (e.type) { 79 case "popupshowing": 80 this.updateContextMenu(); 81 break; 82 default: 83 super.handleEvent(e); 84 } 85 } 86 87 get isMultipleRowsSelected() { 88 return !!this.treeView.selectedLists.size; 89 } 90 91 /** 92 * Only show multiselect commands when multiple items are selected. 93 */ 94 updateContextMenu() { 95 for (const child of this._contextMenu.children) { 96 const isMultiSelectCommand = child.classList.contains( 97 "sidebar-history-multiselect-command" 98 ); 99 if (this.isMultipleRowsSelected) { 100 child.hidden = !isMultiSelectCommand; 101 } else { 102 child.hidden = isMultiSelectCommand; 103 } 104 } 105 let privateWindowMenuItem = this._contextMenu.querySelector( 106 "#sidebar-history-context-open-in-private-window" 107 ); 108 privateWindowMenuItem.hidden = !lazy.PrivateBrowsingUtils.enabled; 109 } 110 111 handleContextMenuEvent(e) { 112 this.triggerNode = 113 this.findTriggerNode(e, "sidebar-tab-row") || 114 this.findTriggerNode(e, "moz-input-search"); 115 if (!this.triggerNode) { 116 e.preventDefault(); 117 } 118 } 119 120 handleCommandEvent(e) { 121 switch (e.target.id) { 122 case "sidebar-history-sort-by-date": 123 this.controller.onChangeSortOption(e, "date"); 124 break; 125 case "sidebar-history-sort-by-site": 126 this.controller.onChangeSortOption(e, "site"); 127 break; 128 case "sidebar-history-sort-by-date-and-site": 129 this.controller.onChangeSortOption(e, "datesite"); 130 break; 131 case "sidebar-history-sort-by-last-visited": 132 this.controller.onChangeSortOption(e, "lastvisited"); 133 break; 134 case "sidebar-history-clear": 135 lazy.Sanitizer.showUI(this.topWindow); 136 break; 137 case "sidebar-history-context-delete-page": 138 this.controller.deleteFromHistory().catch(console.error); 139 break; 140 case "sidebar-history-context-delete-pages": 141 this.#deleteMultipleFromHistory().catch(console.error); 142 break; 143 default: 144 super.handleCommandEvent(e); 145 break; 146 } 147 } 148 149 #deleteMultipleFromHistory() { 150 const pageGuids = [...this.treeView.selectedLists].flatMap( 151 ({ selectedGuids }) => [...selectedGuids] 152 ); 153 return lazy.PlacesUtils.history.remove(pageGuids); 154 } 155 156 // We should let moz-button handle this, see bug 1875374. 157 handlePopupEvent(e) { 158 if (e.type == "popuphidden") { 159 this.menuButton.setAttribute("aria-expanded", false); 160 } 161 } 162 163 handleSidebarFocusedEvent() { 164 this.searchTextbox?.focus(); 165 } 166 167 onPrimaryAction(e) { 168 if (this.isMultipleRowsSelected) { 169 // Avoid opening multiple links at once. 170 return; 171 } 172 navigateToLink(e, e.originalTarget.url, { forceNewTab: false }); 173 this.treeView.clearSelection(); 174 } 175 176 onSecondaryAction(e) { 177 this.triggerNode = e.detail.item; 178 this.controller.deleteFromHistory().catch(console.error); 179 } 180 181 /** 182 * The template to use for cards-container. 183 */ 184 get cardsTemplate() { 185 if (this.controller.isHistoryPending) { 186 // don't render cards until initial history visits entries are available 187 return ""; 188 } else if (this.controller.searchResults) { 189 return this.#searchResultsTemplate(); 190 } else if (!this.controller.isHistoryEmpty) { 191 return this.#historyCardsTemplate(); 192 } 193 return this.#emptyMessageTemplate(); 194 } 195 196 #historyCardsTemplate() { 197 const { historyVisits } = this.controller; 198 switch (this.controller.sortOption) { 199 case "date": 200 return historyVisits.map(({ l10nId, items }, i) => 201 this.#dateCardTemplate(l10nId, i, items) 202 ); 203 case "site": 204 return historyVisits.map(({ domain, items }, i) => 205 this.#siteCardTemplate(domain, i, items) 206 ); 207 case "datesite": 208 return historyVisits.map(({ l10nId, items }, i) => 209 this.#dateCardTemplate(l10nId, i, items, true) 210 ); 211 case "lastvisited": 212 return historyVisits.map( 213 ({ items }) => 214 html`<moz-card> 215 ${this.#tabListTemplate(this.getTabItems(items))} 216 </moz-card>` 217 ); 218 default: 219 return []; 220 } 221 } 222 223 #dateCardTemplate(l10nId, index, items, isDateSite = false) { 224 const tabIndex = index > 0 ? "-1" : undefined; 225 return html` <moz-card 226 type="accordion" 227 class="date-card" 228 ?expanded=${index < DAYS_EXPANDED_INITIALLY} 229 data-l10n-id=${l10nId} 230 data-l10n-args=${JSON.stringify({ 231 date: isDateSite ? items[0][1][0].time : items[0].time, 232 })} 233 @keydown=${e => this.treeView.handleCardKeydown(e)} 234 tabindex=${ifDefined(tabIndex)} 235 > 236 ${isDateSite 237 ? items.map(([domain, visits], i) => 238 this.#siteCardTemplate( 239 domain, 240 i, 241 visits, 242 true, 243 i == items.length - 1 244 ) 245 ) 246 : this.#tabListTemplate(this.getTabItems(items))} 247 </moz-card>`; 248 } 249 250 #siteCardTemplate( 251 domain, 252 index, 253 items, 254 isDateSite = false, 255 isLastCard = false 256 ) { 257 let tabIndex = index > 0 || isDateSite ? "-1" : undefined; 258 return html` <moz-card 259 class=${classMap({ 260 "last-card": isLastCard, 261 "nested-card": isDateSite, 262 "site-card": true, 263 })} 264 type="accordion" 265 ?expanded=${!isDateSite} 266 heading=${domain} 267 @keydown=${e => this.treeView.handleCardKeydown(e)} 268 tabindex=${ifDefined(tabIndex)} 269 data-l10n-id=${domain ? nothing : "sidebar-history-site-localhost"} 270 data-l10n-attrs=${domain ? nothing : "heading"} 271 > 272 ${this.#tabListTemplate(this.getTabItems(items))} 273 </moz-card>`; 274 } 275 276 #emptyMessageTemplate() { 277 let descriptionHeader; 278 let descriptionLabels; 279 let descriptionLink; 280 if (Services.prefs.getBoolPref(NEVER_REMEMBER_HISTORY_PREF, false)) { 281 // History pref set to never remember history 282 descriptionHeader = "firefoxview-dont-remember-history-empty-header-2"; 283 descriptionLabels = [ 284 "firefoxview-dont-remember-history-empty-description-one", 285 ]; 286 descriptionLink = { 287 url: "about:preferences#privacy", 288 name: "history-settings-url-two", 289 }; 290 } else { 291 descriptionHeader = "firefoxview-history-empty-header"; 292 descriptionLabels = [ 293 "firefoxview-history-empty-description", 294 "firefoxview-history-empty-description-two", 295 ]; 296 descriptionLink = { 297 url: "about:preferences#privacy", 298 name: "history-settings-url", 299 }; 300 } 301 return html` 302 <fxview-empty-state 303 headerLabel=${descriptionHeader} 304 .descriptionLabels=${descriptionLabels} 305 .descriptionLink=${descriptionLink} 306 class="empty-state history" 307 isSelectedTab 308 mainImageUrl="chrome://browser/content/firefoxview/history-empty.svg" 309 openLinkInParentWindow 310 > 311 </fxview-empty-state> 312 `; 313 } 314 315 #searchResultsTemplate() { 316 return html` <moz-card 317 data-l10n-id="sidebar-search-results-header" 318 data-l10n-args=${JSON.stringify({ 319 query: this.controller.searchQuery, 320 })} 321 > 322 <div> 323 ${when( 324 this.controller.searchResults.length, 325 () => 326 html`<h3 327 slot="secondary-header" 328 data-l10n-id="firefoxview-search-results-count" 329 data-l10n-args=${JSON.stringify({ 330 count: this.controller.searchResults.length, 331 })} 332 ></h3>` 333 )} 334 ${this.#tabListTemplate( 335 this.getTabItems(this.controller.searchResults), 336 this.controller.searchQuery 337 )} 338 </div> 339 </moz-card>`; 340 } 341 342 #tabListTemplate(tabItems, searchQuery) { 343 return html`<sidebar-tab-list 344 .handleFocusElementToCard=${this.handleFocusElementToCard} 345 maxTabsLength="-1" 346 .searchQuery=${searchQuery} 347 secondaryActionClass="delete-button" 348 .sortOption=${this.controller.sortOption} 349 .tabItems=${tabItems} 350 @fxview-tab-list-primary-action=${this.onPrimaryAction} 351 @fxview-tab-list-secondary-action=${this.onSecondaryAction} 352 > 353 </sidebar-tab-list>`; 354 } 355 356 onSearchQuery(e) { 357 this.controller.onSearchQuery(e); 358 } 359 360 getTabItems(items) { 361 return items.map(item => ({ 362 ...item, 363 secondaryL10nId: "sidebar-history-delete", 364 secondaryL10nArgs: null, 365 })); 366 } 367 368 openMenu(e) { 369 const menuPos = this.sidebarController._positionStart 370 ? "after_start" // Sidebar is on the left. Open menu to the right. 371 : "after_end"; // Sidebar is on the right. Open menu to the left. 372 this._menu.openPopup(e.target, menuPos, 0, 0, false, false, e); 373 this.menuButton.setAttribute("aria-expanded", true); 374 } 375 376 willUpdate() { 377 this._menuSortByDate.setAttribute( 378 "checked", 379 this.controller.sortOption == "date" 380 ); 381 this._menuSortBySite.setAttribute( 382 "checked", 383 this.controller.sortOption == "site" 384 ); 385 this._menuSortByDateSite.setAttribute( 386 "checked", 387 this.controller.sortOption == "datesite" 388 ); 389 this._menuSortByLastVisited.setAttribute( 390 "checked", 391 this.controller.sortOption == "lastvisited" 392 ); 393 } 394 395 render() { 396 return html` 397 ${this.stylesheet()} 398 <link 399 rel="stylesheet" 400 href="chrome://browser/content/sidebar/sidebar-history.css" 401 /> 402 <div class="sidebar-panel"> 403 <sidebar-panel-header 404 data-l10n-id="sidebar-menu-history-header" 405 data-l10n-attrs="heading" 406 view="viewHistorySidebar" 407 > 408 <div class="options-container"> 409 <moz-input-search 410 data-l10n-id="firefoxview-search-text-box-history" 411 data-l10n-attrs="placeholder" 412 @MozInputSearch:search=${this.onSearchQuery} 413 ></moz-input-search> 414 <moz-button 415 class="menu-button" 416 @click=${this.openMenu} 417 data-l10n-id="sidebar-options-menu-button" 418 aria-haspopup="menu" 419 aria-expanded="false" 420 view=${this.view} 421 type="icon ghost" 422 iconsrc="chrome://global/skin/icons/more.svg" 423 > 424 </moz-button> 425 </div> 426 </sidebar-panel-header> 427 <div class="sidebar-panel-scrollable-content"> 428 ${this.cardsTemplate} 429 </div> 430 </div> 431 `; 432 } 433 } 434 435 customElements.define("sidebar-history", SidebarHistory);