1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/metro/base/content/startui/TopSitesView.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,320 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +"use strict" 1.9 + 1.10 +Cu.import("resource://gre/modules/PageThumbs.jsm"); 1.11 + 1.12 +function TopSitesView(aGrid) { 1.13 + View.call(this, aGrid); 1.14 + // View monitors this for maximum tile display counts 1.15 + this.tilePrefName = "browser.display.startUI.topsites.maxresults"; 1.16 + this.showing = this.maxTiles > 0 && !this.isFirstRun(); 1.17 + 1.18 + // clean up state when the appbar closes 1.19 + StartUI.chromeWin.addEventListener('MozAppbarDismissing', this, false); 1.20 + let history = Cc["@mozilla.org/browser/nav-history-service;1"]. 1.21 + getService(Ci.nsINavHistoryService); 1.22 + history.addObserver(this, false); 1.23 + 1.24 + Services.obs.addObserver(this, "Metro:RefreshTopsiteThumbnail", false); 1.25 + 1.26 + NewTabUtils.allPages.register(this); 1.27 + TopSites.prepareCache().then(function(){ 1.28 + this.populateGrid(); 1.29 + }.bind(this)); 1.30 +} 1.31 + 1.32 +TopSitesView.prototype = Util.extend(Object.create(View.prototype), { 1.33 + _set:null, 1.34 + // _lastSelectedSites used to temporarily store blocked/removed sites for undo/restore-ing 1.35 + _lastSelectedSites: null, 1.36 + // isUpdating used only for testing currently 1.37 + isUpdating: false, 1.38 + 1.39 + // For View's showing property 1.40 + get vbox() { 1.41 + return document.getElementById("start-topsites"); 1.42 + }, 1.43 + 1.44 + destruct: function destruct() { 1.45 + Services.obs.removeObserver(this, "Metro:RefreshTopsiteThumbnail"); 1.46 + NewTabUtils.allPages.unregister(this); 1.47 + if (StartUI.chromeWin) { 1.48 + StartUI.chromeWin.removeEventListener('MozAppbarDismissing', this, false); 1.49 + } 1.50 + View.prototype.destruct.call(this); 1.51 + }, 1.52 + 1.53 + handleItemClick: function tabview_handleItemClick(aItem) { 1.54 + let url = aItem.getAttribute("value"); 1.55 + StartUI.goToURI(url); 1.56 + }, 1.57 + 1.58 + doActionOnSelectedTiles: function(aActionName, aEvent) { 1.59 + let tileGroup = this._set; 1.60 + let selectedTiles = tileGroup.selectedItems; 1.61 + let sites = Array.map(selectedTiles, TopSites._linkFromNode); 1.62 + let nextContextActions = new Set(); 1.63 + 1.64 + switch (aActionName){ 1.65 + case "delete": 1.66 + for (let aNode of selectedTiles) { 1.67 + // add some class to transition element before deletion? 1.68 + aNode.contextActions.delete('delete'); 1.69 + // we need new context buttons to show (the tile node will go away though) 1.70 + } 1.71 + this._lastSelectedSites = (this._lastSelectedSites || []).concat(sites); 1.72 + // stop the appbar from dismissing 1.73 + aEvent.preventDefault(); 1.74 + nextContextActions.add('restore'); 1.75 + TopSites.hideSites(sites); 1.76 + break; 1.77 + case "restore": 1.78 + // usually restore is an undo action, so we have to recreate the tiles and grid selection 1.79 + if (this._lastSelectedSites) { 1.80 + let selectedUrls = this._lastSelectedSites.map((site) => site.url); 1.81 + // re-select the tiles once the tileGroup is done populating and arranging 1.82 + tileGroup.addEventListener("arranged", function _onArranged(aEvent){ 1.83 + for (let url of selectedUrls) { 1.84 + let tileNode = tileGroup.querySelector("richgriditem[value='"+url+"']"); 1.85 + if (tileNode) { 1.86 + tileNode.setAttribute("selected", true); 1.87 + } 1.88 + } 1.89 + tileGroup.removeEventListener("arranged", _onArranged, false); 1.90 + // <sfoster> we can't just call selectItem n times on tileGroup as selecting means trigger the default action 1.91 + // for seltype="single" grids. 1.92 + // so we toggle the attributes and raise the selectionchange "manually" 1.93 + let event = tileGroup.ownerDocument.createEvent("Events"); 1.94 + event.initEvent("selectionchange", true, true); 1.95 + tileGroup.dispatchEvent(event); 1.96 + }, false); 1.97 + 1.98 + TopSites.restoreSites(this._lastSelectedSites); 1.99 + // stop the appbar from dismissing, 1.100 + // the selectionchange event will trigger re-population of the context appbar 1.101 + aEvent.preventDefault(); 1.102 + } 1.103 + break; 1.104 + case "pin": 1.105 + let pinIndices = []; 1.106 + Array.forEach(selectedTiles, function(aNode) { 1.107 + pinIndices.push( Array.indexOf(aNode.control.items, aNode) ); 1.108 + aNode.contextActions.delete('pin'); 1.109 + aNode.contextActions.add('unpin'); 1.110 + }); 1.111 + TopSites.pinSites(sites, pinIndices); 1.112 + break; 1.113 + case "unpin": 1.114 + Array.forEach(selectedTiles, function(aNode) { 1.115 + aNode.contextActions.delete('unpin'); 1.116 + aNode.contextActions.add('pin'); 1.117 + }); 1.118 + TopSites.unpinSites(sites); 1.119 + break; 1.120 + // default: no action 1.121 + } 1.122 + if (nextContextActions.size) { 1.123 + // at next tick, re-populate the context appbar 1.124 + setTimeout(function(){ 1.125 + // fire a MozContextActionsChange event to update the context appbar 1.126 + let event = document.createEvent("Events"); 1.127 + event.actions = [...nextContextActions]; 1.128 + event.initEvent("MozContextActionsChange", true, false); 1.129 + tileGroup.dispatchEvent(event); 1.130 + },0); 1.131 + } 1.132 + }, 1.133 + 1.134 + handleEvent: function(aEvent) { 1.135 + switch (aEvent.type){ 1.136 + case "MozAppbarDismissing": 1.137 + // clean up when the context appbar is dismissed - we don't remember selections 1.138 + this._lastSelectedSites = null; 1.139 + break; 1.140 + } 1.141 + }, 1.142 + 1.143 + update: function() { 1.144 + // called by the NewTabUtils.allPages.update, notifying us of data-change in topsites 1.145 + let grid = this._set, 1.146 + dirtySites = TopSites.dirty(); 1.147 + 1.148 + if (dirtySites.size) { 1.149 + // we can just do a partial update and refresh the node representing each dirty tile 1.150 + for (let site of dirtySites) { 1.151 + let tileNode = grid.querySelector("[value='"+site.url+"']"); 1.152 + if (tileNode) { 1.153 + this.updateTile(tileNode, new Site(site)); 1.154 + } 1.155 + } 1.156 + } else { 1.157 + // flush, recreate all 1.158 + this.isUpdating = true; 1.159 + // destroy and recreate all item nodes, skip calling arrangeItems 1.160 + this.populateGrid(); 1.161 + } 1.162 + }, 1.163 + 1.164 + updateTile: function(aTileNode, aSite, aArrangeGrid) { 1.165 + if (!(aSite && aSite.url)) { 1.166 + throw new Error("Invalid Site object passed to TopSitesView updateTile"); 1.167 + } 1.168 + this._updateFavicon(aTileNode, Util.makeURI(aSite.url)); 1.169 + 1.170 + Task.spawn(function() { 1.171 + let filepath = PageThumbsStorage.getFilePathForURL(aSite.url); 1.172 + if (yield OS.File.exists(filepath)) { 1.173 + aSite.backgroundImage = 'url("'+PageThumbs.getThumbnailURL(aSite.url)+'")'; 1.174 + // use the setter when available to update the backgroundImage value 1.175 + if ('backgroundImage' in aTileNode && 1.176 + aTileNode.backgroundImage != aSite.backgroundImage) { 1.177 + aTileNode.backgroundImage = aSite.backgroundImage; 1.178 + } else { 1.179 + // just update the attribute for when the node gets the binding applied 1.180 + aTileNode.setAttribute("customImage", aSite.backgroundImage); 1.181 + } 1.182 + } 1.183 + }); 1.184 + 1.185 + aSite.applyToTileNode(aTileNode); 1.186 + if (aTileNode.refresh) { 1.187 + aTileNode.refresh(); 1.188 + } 1.189 + if (aArrangeGrid) { 1.190 + this._set.arrangeItems(); 1.191 + } 1.192 + }, 1.193 + 1.194 + populateGrid: function populateGrid() { 1.195 + this.isUpdating = true; 1.196 + 1.197 + let sites = TopSites.getSites(); 1.198 + 1.199 + let tileset = this._set; 1.200 + tileset.clearAll(true); 1.201 + 1.202 + if (!this.maxTiles) { 1.203 + this.isUpdating = false; 1.204 + return; 1.205 + } else { 1.206 + sites = sites.slice(0, this.maxTiles); 1.207 + } 1.208 + 1.209 + for (let site of sites) { 1.210 + let slot = tileset.nextSlot(); 1.211 + this.updateTile(slot, site); 1.212 + } 1.213 + tileset.arrangeItems(); 1.214 + this.isUpdating = false; 1.215 + }, 1.216 + 1.217 + forceReloadOfThumbnail: function forceReloadOfThumbnail(url) { 1.218 + let nodes = this._set.querySelectorAll('richgriditem[value="'+url+'"]'); 1.219 + for (let item of nodes) { 1.220 + if ("isBound" in item && item.isBound) { 1.221 + item.refreshBackgroundImage(); 1.222 + } 1.223 + } 1.224 + }, 1.225 + 1.226 + isFirstRun: function isFirstRun() { 1.227 + return Services.prefs.getBoolPref("browser.firstrun.show.localepicker"); 1.228 + }, 1.229 + 1.230 + _adjustDOMforViewState: function _adjustDOMforViewState(aState) { 1.231 + if (!this._set) 1.232 + return; 1.233 + if (!aState) 1.234 + aState = this._set.getAttribute("viewstate"); 1.235 + 1.236 + View.prototype._adjustDOMforViewState.call(this, aState); 1.237 + 1.238 + // Don't show thumbnails in snapped view. 1.239 + if (aState == "snapped") { 1.240 + document.getElementById("start-topsites-grid").removeAttribute("tiletype"); 1.241 + } else { 1.242 + document.getElementById("start-topsites-grid").setAttribute("tiletype", "thumbnail"); 1.243 + } 1.244 + 1.245 + // propogate tiletype changes down to tile children 1.246 + let tileType = this._set.getAttribute("tiletype"); 1.247 + for (let item of this._set.children) { 1.248 + if (tileType) { 1.249 + item.setAttribute("tiletype", tileType); 1.250 + } else { 1.251 + item.removeAttribute("tiletype"); 1.252 + } 1.253 + } 1.254 + }, 1.255 + 1.256 + refreshView: function () { 1.257 + this.populateGrid(); 1.258 + }, 1.259 + 1.260 + // nsIObservers 1.261 + observe: function (aSubject, aTopic, aState) { 1.262 + switch (aTopic) { 1.263 + case "Metro:RefreshTopsiteThumbnail": 1.264 + this.forceReloadOfThumbnail(aState); 1.265 + break; 1.266 + } 1.267 + View.prototype.observe.call(this, aSubject, aTopic, aState); 1.268 + this.showing = this.maxTiles > 0 && !this.isFirstRun(); 1.269 + }, 1.270 + 1.271 + // nsINavHistoryObserver 1.272 + onBeginUpdateBatch: function() { 1.273 + }, 1.274 + 1.275 + onEndUpdateBatch: function() { 1.276 + }, 1.277 + 1.278 + onVisit: function(aURI, aVisitID, aTime, aSessionID, 1.279 + aReferringID, aTransitionType) { 1.280 + }, 1.281 + 1.282 + onTitleChanged: function(aURI, aPageTitle) { 1.283 + }, 1.284 + 1.285 + onDeleteURI: function(aURI) { 1.286 + }, 1.287 + 1.288 + onClearHistory: function() { 1.289 + if ('clearAll' in this._set) 1.290 + this._set.clearAll(); 1.291 + }, 1.292 + 1.293 + onPageChanged: function(aURI, aWhat, aValue) { 1.294 + }, 1.295 + 1.296 + onDeleteVisits: function (aURI, aVisitTime, aGUID, aReason, aTransitionType) { 1.297 + }, 1.298 + 1.299 + QueryInterface: function(iid) { 1.300 + if (iid.equals(Components.interfaces.nsINavHistoryObserver) || 1.301 + iid.equals(Components.interfaces.nsISupports)) { 1.302 + return this; 1.303 + } 1.304 + throw Cr.NS_ERROR_NO_INTERFACE; 1.305 + } 1.306 + 1.307 +}); 1.308 + 1.309 +let TopSitesStartView = { 1.310 + _view: null, 1.311 + get _grid() { return document.getElementById("start-topsites-grid"); }, 1.312 + 1.313 + init: function init() { 1.314 + this._view = new TopSitesView(this._grid); 1.315 + this._grid.removeAttribute("fade"); 1.316 + }, 1.317 + 1.318 + uninit: function uninit() { 1.319 + if (this._view) { 1.320 + this._view.destruct(); 1.321 + } 1.322 + }, 1.323 +};