siteDataSettings.js (10261B)
1 /* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */ 2 /* This Source Code Form is subject to the terms of the Mozilla Public 3 * License, v. 2.0. If a copy of the MPL was not distributed with this 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 6 "use strict"; 7 8 var { AppConstants } = ChromeUtils.importESModule( 9 "resource://gre/modules/AppConstants.sys.mjs" 10 ); 11 12 ChromeUtils.defineESModuleGetters(this, { 13 DownloadUtils: "resource://gre/modules/DownloadUtils.sys.mjs", 14 SiteDataManager: "resource:///modules/SiteDataManager.sys.mjs", 15 }); 16 17 let gSiteDataSettings = { 18 // Array of metadata of sites. Each array element is object holding: 19 // - uri: uri of site; instance of nsIURI 20 // - baseDomain: base domain of the site 21 // - cookies: array of cookies of that site 22 // - usage: disk usage which site uses 23 // - userAction: "remove" or "update-permission"; the action user wants to take. 24 _sites: null, 25 26 _list: null, 27 _searchBox: null, 28 29 _createSiteListItem(site) { 30 let item = document.createXULElement("richlistitem"); 31 item.setAttribute("host", site.baseDomain); 32 let container = document.createXULElement("hbox"); 33 34 // Creates a new column item with the specified relative width. 35 function addColumnItem(l10n, flexWidth, tooltipText) { 36 let box = document.createXULElement("hbox"); 37 box.className = "item-box"; 38 box.setAttribute("style", `flex: ${flexWidth} ${flexWidth};`); 39 let label = document.createXULElement("label"); 40 label.setAttribute("crop", "end"); 41 if (l10n) { 42 if (l10n.hasOwnProperty("raw")) { 43 box.setAttribute("tooltiptext", l10n.raw); 44 label.setAttribute("value", l10n.raw); 45 } else { 46 document.l10n.setAttributes(label, l10n.id, l10n.args); 47 } 48 } 49 if (tooltipText) { 50 box.setAttribute("tooltiptext", tooltipText); 51 } 52 box.appendChild(label); 53 container.appendChild(box); 54 } 55 56 // Add "Host" column. 57 let hostData = site.baseDomain 58 ? { raw: site.baseDomain } 59 : { id: "site-data-local-file-host" }; 60 addColumnItem(hostData, "4"); 61 62 // Add "Cookies" column. 63 addColumnItem({ raw: site.cookies.length }, "1"); 64 65 // Add "Storage" column 66 if (site.usage > 0 || site.persisted) { 67 let [value, unit] = DownloadUtils.convertByteUnits(site.usage); 68 let strName = site.persisted 69 ? "site-storage-persistent" 70 : "site-storage-usage"; 71 addColumnItem( 72 { 73 id: strName, 74 args: { value, unit }, 75 }, 76 "2" 77 ); 78 } else { 79 // Pass null to avoid showing "0KB" when there is no site data stored. 80 addColumnItem(null, "2"); 81 } 82 83 // Add "Last Used" column. 84 let formattedLastAccessed = 85 site.lastAccessed > 0 86 ? this._relativeTimeFormat.formatBestUnit(site.lastAccessed) 87 : null; 88 let formattedFullDate = 89 site.lastAccessed > 0 90 ? this._absoluteTimeFormat.format(site.lastAccessed) 91 : null; 92 addColumnItem( 93 site.lastAccessed > 0 ? { raw: formattedLastAccessed } : null, 94 "2", 95 formattedFullDate 96 ); 97 98 item.appendChild(container); 99 return item; 100 }, 101 102 init() { 103 function setEventListener(id, eventType, callback) { 104 document 105 .getElementById(id) 106 .addEventListener(eventType, callback.bind(gSiteDataSettings)); 107 } 108 109 this._absoluteTimeFormat = new Services.intl.DateTimeFormat(undefined, { 110 dateStyle: "short", 111 timeStyle: "short", 112 }); 113 114 this._relativeTimeFormat = new Services.intl.RelativeTimeFormat( 115 undefined, 116 {} 117 ); 118 119 this._list = document.getElementById("sitesList"); 120 this._searchBox = document.getElementById("searchBox"); 121 SiteDataManager.getSites().then(sites => { 122 this._sites = sites; 123 let sortCol = document.querySelector( 124 "treecol[data-isCurrentSortCol=true]" 125 ); 126 this._sortSites(this._sites, sortCol); 127 this._buildSitesList(this._sites); 128 Services.obs.notifyObservers(null, "sitedata-settings-init"); 129 }); 130 131 setEventListener("sitesList", "select", this.onSelect); 132 setEventListener("hostCol", "click", this.onClickTreeCol); 133 setEventListener("usageCol", "click", this.onClickTreeCol); 134 setEventListener("lastAccessedCol", "click", this.onClickTreeCol); 135 setEventListener("cookiesCol", "click", this.onClickTreeCol); 136 setEventListener("searchBox", "MozInputSearch:search", this.onInputSearch); 137 setEventListener("removeAll", "command", this.onClickRemoveAll); 138 setEventListener("removeSelected", "command", this.removeSelected); 139 140 document.addEventListener("dialogaccept", e => this.saveChanges(e)); 141 window.addEventListener("keypress", e => this.onKeyPress(e)); 142 }, 143 144 _updateButtonsState() { 145 let items = this._list.getElementsByTagName("richlistitem"); 146 let removeSelectedBtn = document.getElementById("removeSelected"); 147 let removeAllBtn = document.getElementById("removeAll"); 148 removeSelectedBtn.disabled = !this._list.selectedItems.length; 149 removeAllBtn.disabled = !items.length; 150 151 let l10nId = this._searchBox.value 152 ? "site-data-remove-shown" 153 : "site-data-remove-all"; 154 document.l10n.setAttributes(removeAllBtn, l10nId); 155 }, 156 157 /** 158 * @param sites {Array} 159 * @param col {XULElement} the <treecol> being sorted on 160 */ 161 _sortSites(sites, col) { 162 let isCurrentSortCol = col.getAttribute("data-isCurrentSortCol"); 163 let sortDirection = 164 col.getAttribute("data-last-sortDirection") || "ascending"; 165 if (isCurrentSortCol) { 166 // Sort on the current column, flip the sorting direction 167 sortDirection = 168 sortDirection === "ascending" ? "descending" : "ascending"; 169 } 170 171 let sortFunc = null; 172 switch (col.id) { 173 case "hostCol": 174 sortFunc = (a, b) => { 175 let aHost = a.baseDomain.toLowerCase(); 176 let bHost = b.baseDomain.toLowerCase(); 177 return aHost.localeCompare(bHost); 178 }; 179 break; 180 181 case "cookiesCol": 182 sortFunc = (a, b) => a.cookies.length - b.cookies.length; 183 break; 184 185 case "usageCol": 186 sortFunc = (a, b) => a.usage - b.usage; 187 break; 188 189 case "lastAccessedCol": 190 sortFunc = (a, b) => a.lastAccessed - b.lastAccessed; 191 break; 192 } 193 if (sortDirection === "descending") { 194 sites.sort((a, b) => sortFunc(b, a)); 195 } else { 196 sites.sort(sortFunc); 197 } 198 199 let cols = this._list.previousElementSibling.querySelectorAll("treecol"); 200 cols.forEach(c => { 201 c.removeAttribute("sortDirection"); 202 c.removeAttribute("data-isCurrentSortCol"); 203 }); 204 col.setAttribute("data-isCurrentSortCol", true); 205 col.setAttribute("sortDirection", sortDirection); 206 col.setAttribute("data-last-sortDirection", sortDirection); 207 }, 208 209 /** 210 * @param sites {Array} array of metadata of sites 211 */ 212 _buildSitesList(sites) { 213 // Clear old entries. 214 let oldItems = this._list.querySelectorAll("richlistitem"); 215 for (let item of oldItems) { 216 item.remove(); 217 } 218 219 let keyword = this._searchBox.value.toLowerCase().trim(); 220 let fragment = document.createDocumentFragment(); 221 for (let site of sites) { 222 if (keyword && !site.baseDomain.includes(keyword)) { 223 continue; 224 } 225 226 if (site.userAction === "remove") { 227 continue; 228 } 229 230 let item = this._createSiteListItem(site); 231 fragment.appendChild(item); 232 } 233 this._list.appendChild(fragment); 234 this._updateButtonsState(); 235 }, 236 237 _removeSiteItems(items) { 238 for (let i = items.length - 1; i >= 0; --i) { 239 let item = items[i]; 240 let baseDomain = item.getAttribute("host"); 241 let siteForBaseDomain = this._sites.find( 242 site => site.baseDomain == baseDomain 243 ); 244 if (siteForBaseDomain) { 245 siteForBaseDomain.userAction = "remove"; 246 } 247 item.remove(); 248 } 249 this._updateButtonsState(); 250 }, 251 252 async saveChanges(event) { 253 let removals = this._sites 254 .filter(site => site.userAction == "remove") 255 .map(site => site.baseDomain); 256 257 if (removals.length) { 258 let removeAll = removals.length == this._sites.length; 259 let promptArg = removeAll ? undefined : removals; 260 if (!SiteDataManager.promptSiteDataRemoval(window, promptArg)) { 261 // If the user cancelled the confirm dialog keep the site data window open, 262 // they can still press cancel again to exit. 263 event.preventDefault(); 264 return; 265 } 266 try { 267 if (removeAll) { 268 await SiteDataManager.removeAll(); 269 } else { 270 await SiteDataManager.remove(removals); 271 } 272 } catch (e) { 273 console.error(e); 274 } 275 } 276 }, 277 278 removeSelected() { 279 let lastIndex = this._list.selectedItems.length - 1; 280 let lastSelectedItem = this._list.selectedItems[lastIndex]; 281 let lastSelectedItemPosition = this._list.getIndexOfItem(lastSelectedItem); 282 let nextSelectedItem = this._list.getItemAtIndex( 283 lastSelectedItemPosition + 1 284 ); 285 286 this._removeSiteItems(this._list.selectedItems); 287 this._list.clearSelection(); 288 289 if (nextSelectedItem) { 290 this._list.selectedItem = nextSelectedItem; 291 } else { 292 this._list.selectedIndex = this._list.itemCount - 1; 293 } 294 }, 295 296 onClickTreeCol(e) { 297 this._sortSites(this._sites, e.target); 298 this._buildSitesList(this._sites); 299 this._list.clearSelection(); 300 }, 301 302 onInputSearch() { 303 this._buildSitesList(this._sites); 304 this._list.clearSelection(); 305 }, 306 307 onClickRemoveAll() { 308 let siteItems = this._list.getElementsByTagName("richlistitem"); 309 if (siteItems.length) { 310 this._removeSiteItems(siteItems); 311 } 312 }, 313 314 onKeyPress(e) { 315 if ( 316 e.keyCode == KeyEvent.DOM_VK_DELETE || 317 (AppConstants.platform == "macosx" && 318 e.keyCode == KeyEvent.DOM_VK_BACK_SPACE) 319 ) { 320 if (!e.target.closest("#sitesList")) { 321 // The user is typing or has not selected an item from the list to remove 322 return; 323 } 324 // The users intention is to delete site data 325 this.removeSelected(); 326 } 327 }, 328 329 onSelect() { 330 this._updateButtonsState(); 331 }, 332 }; 333 334 window.addEventListener("load", () => gSiteDataSettings.init());