|
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 "use strict" |
|
6 |
|
7 Cu.import("resource://gre/modules/PageThumbs.jsm"); |
|
8 |
|
9 function TopSitesView(aGrid) { |
|
10 View.call(this, aGrid); |
|
11 // View monitors this for maximum tile display counts |
|
12 this.tilePrefName = "browser.display.startUI.topsites.maxresults"; |
|
13 this.showing = this.maxTiles > 0 && !this.isFirstRun(); |
|
14 |
|
15 // clean up state when the appbar closes |
|
16 StartUI.chromeWin.addEventListener('MozAppbarDismissing', this, false); |
|
17 let history = Cc["@mozilla.org/browser/nav-history-service;1"]. |
|
18 getService(Ci.nsINavHistoryService); |
|
19 history.addObserver(this, false); |
|
20 |
|
21 Services.obs.addObserver(this, "Metro:RefreshTopsiteThumbnail", false); |
|
22 |
|
23 NewTabUtils.allPages.register(this); |
|
24 TopSites.prepareCache().then(function(){ |
|
25 this.populateGrid(); |
|
26 }.bind(this)); |
|
27 } |
|
28 |
|
29 TopSitesView.prototype = Util.extend(Object.create(View.prototype), { |
|
30 _set:null, |
|
31 // _lastSelectedSites used to temporarily store blocked/removed sites for undo/restore-ing |
|
32 _lastSelectedSites: null, |
|
33 // isUpdating used only for testing currently |
|
34 isUpdating: false, |
|
35 |
|
36 // For View's showing property |
|
37 get vbox() { |
|
38 return document.getElementById("start-topsites"); |
|
39 }, |
|
40 |
|
41 destruct: function destruct() { |
|
42 Services.obs.removeObserver(this, "Metro:RefreshTopsiteThumbnail"); |
|
43 NewTabUtils.allPages.unregister(this); |
|
44 if (StartUI.chromeWin) { |
|
45 StartUI.chromeWin.removeEventListener('MozAppbarDismissing', this, false); |
|
46 } |
|
47 View.prototype.destruct.call(this); |
|
48 }, |
|
49 |
|
50 handleItemClick: function tabview_handleItemClick(aItem) { |
|
51 let url = aItem.getAttribute("value"); |
|
52 StartUI.goToURI(url); |
|
53 }, |
|
54 |
|
55 doActionOnSelectedTiles: function(aActionName, aEvent) { |
|
56 let tileGroup = this._set; |
|
57 let selectedTiles = tileGroup.selectedItems; |
|
58 let sites = Array.map(selectedTiles, TopSites._linkFromNode); |
|
59 let nextContextActions = new Set(); |
|
60 |
|
61 switch (aActionName){ |
|
62 case "delete": |
|
63 for (let aNode of selectedTiles) { |
|
64 // add some class to transition element before deletion? |
|
65 aNode.contextActions.delete('delete'); |
|
66 // we need new context buttons to show (the tile node will go away though) |
|
67 } |
|
68 this._lastSelectedSites = (this._lastSelectedSites || []).concat(sites); |
|
69 // stop the appbar from dismissing |
|
70 aEvent.preventDefault(); |
|
71 nextContextActions.add('restore'); |
|
72 TopSites.hideSites(sites); |
|
73 break; |
|
74 case "restore": |
|
75 // usually restore is an undo action, so we have to recreate the tiles and grid selection |
|
76 if (this._lastSelectedSites) { |
|
77 let selectedUrls = this._lastSelectedSites.map((site) => site.url); |
|
78 // re-select the tiles once the tileGroup is done populating and arranging |
|
79 tileGroup.addEventListener("arranged", function _onArranged(aEvent){ |
|
80 for (let url of selectedUrls) { |
|
81 let tileNode = tileGroup.querySelector("richgriditem[value='"+url+"']"); |
|
82 if (tileNode) { |
|
83 tileNode.setAttribute("selected", true); |
|
84 } |
|
85 } |
|
86 tileGroup.removeEventListener("arranged", _onArranged, false); |
|
87 // <sfoster> we can't just call selectItem n times on tileGroup as selecting means trigger the default action |
|
88 // for seltype="single" grids. |
|
89 // so we toggle the attributes and raise the selectionchange "manually" |
|
90 let event = tileGroup.ownerDocument.createEvent("Events"); |
|
91 event.initEvent("selectionchange", true, true); |
|
92 tileGroup.dispatchEvent(event); |
|
93 }, false); |
|
94 |
|
95 TopSites.restoreSites(this._lastSelectedSites); |
|
96 // stop the appbar from dismissing, |
|
97 // the selectionchange event will trigger re-population of the context appbar |
|
98 aEvent.preventDefault(); |
|
99 } |
|
100 break; |
|
101 case "pin": |
|
102 let pinIndices = []; |
|
103 Array.forEach(selectedTiles, function(aNode) { |
|
104 pinIndices.push( Array.indexOf(aNode.control.items, aNode) ); |
|
105 aNode.contextActions.delete('pin'); |
|
106 aNode.contextActions.add('unpin'); |
|
107 }); |
|
108 TopSites.pinSites(sites, pinIndices); |
|
109 break; |
|
110 case "unpin": |
|
111 Array.forEach(selectedTiles, function(aNode) { |
|
112 aNode.contextActions.delete('unpin'); |
|
113 aNode.contextActions.add('pin'); |
|
114 }); |
|
115 TopSites.unpinSites(sites); |
|
116 break; |
|
117 // default: no action |
|
118 } |
|
119 if (nextContextActions.size) { |
|
120 // at next tick, re-populate the context appbar |
|
121 setTimeout(function(){ |
|
122 // fire a MozContextActionsChange event to update the context appbar |
|
123 let event = document.createEvent("Events"); |
|
124 event.actions = [...nextContextActions]; |
|
125 event.initEvent("MozContextActionsChange", true, false); |
|
126 tileGroup.dispatchEvent(event); |
|
127 },0); |
|
128 } |
|
129 }, |
|
130 |
|
131 handleEvent: function(aEvent) { |
|
132 switch (aEvent.type){ |
|
133 case "MozAppbarDismissing": |
|
134 // clean up when the context appbar is dismissed - we don't remember selections |
|
135 this._lastSelectedSites = null; |
|
136 break; |
|
137 } |
|
138 }, |
|
139 |
|
140 update: function() { |
|
141 // called by the NewTabUtils.allPages.update, notifying us of data-change in topsites |
|
142 let grid = this._set, |
|
143 dirtySites = TopSites.dirty(); |
|
144 |
|
145 if (dirtySites.size) { |
|
146 // we can just do a partial update and refresh the node representing each dirty tile |
|
147 for (let site of dirtySites) { |
|
148 let tileNode = grid.querySelector("[value='"+site.url+"']"); |
|
149 if (tileNode) { |
|
150 this.updateTile(tileNode, new Site(site)); |
|
151 } |
|
152 } |
|
153 } else { |
|
154 // flush, recreate all |
|
155 this.isUpdating = true; |
|
156 // destroy and recreate all item nodes, skip calling arrangeItems |
|
157 this.populateGrid(); |
|
158 } |
|
159 }, |
|
160 |
|
161 updateTile: function(aTileNode, aSite, aArrangeGrid) { |
|
162 if (!(aSite && aSite.url)) { |
|
163 throw new Error("Invalid Site object passed to TopSitesView updateTile"); |
|
164 } |
|
165 this._updateFavicon(aTileNode, Util.makeURI(aSite.url)); |
|
166 |
|
167 Task.spawn(function() { |
|
168 let filepath = PageThumbsStorage.getFilePathForURL(aSite.url); |
|
169 if (yield OS.File.exists(filepath)) { |
|
170 aSite.backgroundImage = 'url("'+PageThumbs.getThumbnailURL(aSite.url)+'")'; |
|
171 // use the setter when available to update the backgroundImage value |
|
172 if ('backgroundImage' in aTileNode && |
|
173 aTileNode.backgroundImage != aSite.backgroundImage) { |
|
174 aTileNode.backgroundImage = aSite.backgroundImage; |
|
175 } else { |
|
176 // just update the attribute for when the node gets the binding applied |
|
177 aTileNode.setAttribute("customImage", aSite.backgroundImage); |
|
178 } |
|
179 } |
|
180 }); |
|
181 |
|
182 aSite.applyToTileNode(aTileNode); |
|
183 if (aTileNode.refresh) { |
|
184 aTileNode.refresh(); |
|
185 } |
|
186 if (aArrangeGrid) { |
|
187 this._set.arrangeItems(); |
|
188 } |
|
189 }, |
|
190 |
|
191 populateGrid: function populateGrid() { |
|
192 this.isUpdating = true; |
|
193 |
|
194 let sites = TopSites.getSites(); |
|
195 |
|
196 let tileset = this._set; |
|
197 tileset.clearAll(true); |
|
198 |
|
199 if (!this.maxTiles) { |
|
200 this.isUpdating = false; |
|
201 return; |
|
202 } else { |
|
203 sites = sites.slice(0, this.maxTiles); |
|
204 } |
|
205 |
|
206 for (let site of sites) { |
|
207 let slot = tileset.nextSlot(); |
|
208 this.updateTile(slot, site); |
|
209 } |
|
210 tileset.arrangeItems(); |
|
211 this.isUpdating = false; |
|
212 }, |
|
213 |
|
214 forceReloadOfThumbnail: function forceReloadOfThumbnail(url) { |
|
215 let nodes = this._set.querySelectorAll('richgriditem[value="'+url+'"]'); |
|
216 for (let item of nodes) { |
|
217 if ("isBound" in item && item.isBound) { |
|
218 item.refreshBackgroundImage(); |
|
219 } |
|
220 } |
|
221 }, |
|
222 |
|
223 isFirstRun: function isFirstRun() { |
|
224 return Services.prefs.getBoolPref("browser.firstrun.show.localepicker"); |
|
225 }, |
|
226 |
|
227 _adjustDOMforViewState: function _adjustDOMforViewState(aState) { |
|
228 if (!this._set) |
|
229 return; |
|
230 if (!aState) |
|
231 aState = this._set.getAttribute("viewstate"); |
|
232 |
|
233 View.prototype._adjustDOMforViewState.call(this, aState); |
|
234 |
|
235 // Don't show thumbnails in snapped view. |
|
236 if (aState == "snapped") { |
|
237 document.getElementById("start-topsites-grid").removeAttribute("tiletype"); |
|
238 } else { |
|
239 document.getElementById("start-topsites-grid").setAttribute("tiletype", "thumbnail"); |
|
240 } |
|
241 |
|
242 // propogate tiletype changes down to tile children |
|
243 let tileType = this._set.getAttribute("tiletype"); |
|
244 for (let item of this._set.children) { |
|
245 if (tileType) { |
|
246 item.setAttribute("tiletype", tileType); |
|
247 } else { |
|
248 item.removeAttribute("tiletype"); |
|
249 } |
|
250 } |
|
251 }, |
|
252 |
|
253 refreshView: function () { |
|
254 this.populateGrid(); |
|
255 }, |
|
256 |
|
257 // nsIObservers |
|
258 observe: function (aSubject, aTopic, aState) { |
|
259 switch (aTopic) { |
|
260 case "Metro:RefreshTopsiteThumbnail": |
|
261 this.forceReloadOfThumbnail(aState); |
|
262 break; |
|
263 } |
|
264 View.prototype.observe.call(this, aSubject, aTopic, aState); |
|
265 this.showing = this.maxTiles > 0 && !this.isFirstRun(); |
|
266 }, |
|
267 |
|
268 // nsINavHistoryObserver |
|
269 onBeginUpdateBatch: function() { |
|
270 }, |
|
271 |
|
272 onEndUpdateBatch: function() { |
|
273 }, |
|
274 |
|
275 onVisit: function(aURI, aVisitID, aTime, aSessionID, |
|
276 aReferringID, aTransitionType) { |
|
277 }, |
|
278 |
|
279 onTitleChanged: function(aURI, aPageTitle) { |
|
280 }, |
|
281 |
|
282 onDeleteURI: function(aURI) { |
|
283 }, |
|
284 |
|
285 onClearHistory: function() { |
|
286 if ('clearAll' in this._set) |
|
287 this._set.clearAll(); |
|
288 }, |
|
289 |
|
290 onPageChanged: function(aURI, aWhat, aValue) { |
|
291 }, |
|
292 |
|
293 onDeleteVisits: function (aURI, aVisitTime, aGUID, aReason, aTransitionType) { |
|
294 }, |
|
295 |
|
296 QueryInterface: function(iid) { |
|
297 if (iid.equals(Components.interfaces.nsINavHistoryObserver) || |
|
298 iid.equals(Components.interfaces.nsISupports)) { |
|
299 return this; |
|
300 } |
|
301 throw Cr.NS_ERROR_NO_INTERFACE; |
|
302 } |
|
303 |
|
304 }); |
|
305 |
|
306 let TopSitesStartView = { |
|
307 _view: null, |
|
308 get _grid() { return document.getElementById("start-topsites-grid"); }, |
|
309 |
|
310 init: function init() { |
|
311 this._view = new TopSitesView(this._grid); |
|
312 this._grid.removeAttribute("fade"); |
|
313 }, |
|
314 |
|
315 uninit: function uninit() { |
|
316 if (this._view) { |
|
317 this._view.destruct(); |
|
318 } |
|
319 }, |
|
320 }; |