|
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 function HistoryView(aSet, aFilterUnpinned) { |
|
8 View.call(this, aSet); |
|
9 |
|
10 this._inBatch = 0; |
|
11 |
|
12 // View monitors this for maximum tile display counts |
|
13 this.tilePrefName = "browser.display.startUI.history.maxresults"; |
|
14 this.showing = this.maxTiles > 0; |
|
15 |
|
16 this._filterUnpinned = aFilterUnpinned; |
|
17 this._historyService = PlacesUtils.history; |
|
18 this._navHistoryService = gHistSvc; |
|
19 |
|
20 this._pinHelper = new ItemPinHelper("metro.history.unpinned"); |
|
21 this._historyService.addObserver(this, false); |
|
22 StartUI.chromeWin.addEventListener('MozAppbarDismissing', this, false); |
|
23 StartUI.chromeWin.addEventListener('HistoryNeedsRefresh', this, false); |
|
24 window.addEventListener("TabClose", this, true); |
|
25 } |
|
26 |
|
27 HistoryView.prototype = Util.extend(Object.create(View.prototype), { |
|
28 _set: null, |
|
29 _toRemove: null, |
|
30 |
|
31 // For View's showing property |
|
32 get vbox() { |
|
33 return document.getElementById("start-history"); |
|
34 }, |
|
35 |
|
36 destruct: function destruct() { |
|
37 this._historyService.removeObserver(this); |
|
38 if (StartUI.chromeWin) { |
|
39 StartUI.chromeWin.removeEventListener('MozAppbarDismissing', this, false); |
|
40 StartUI.chromeWin.removeEventListener('HistoryNeedsRefresh', this, false); |
|
41 } |
|
42 View.prototype.destruct.call(this); |
|
43 }, |
|
44 |
|
45 refreshView: function () { |
|
46 this.onClearHistory(); |
|
47 this.populateGrid(); |
|
48 }, |
|
49 |
|
50 handleItemClick: function tabview_handleItemClick(aItem) { |
|
51 let url = aItem.getAttribute("value"); |
|
52 StartUI.goToURI(url); |
|
53 }, |
|
54 |
|
55 populateGrid: function populateGrid(aRefresh) { |
|
56 this._inBatch++; // always batch up grid updates to avoid redundant arrangeItems calls |
|
57 let query = this._navHistoryService.getNewQuery(); |
|
58 let options = this._navHistoryService.getNewQueryOptions(); |
|
59 options.excludeQueries = true; |
|
60 options.queryType = options.QUERY_TYPE_HISTORY; |
|
61 options.resultType = options.RESULTS_AS_URI; |
|
62 options.sortingMode = options.SORT_BY_DATE_DESCENDING; |
|
63 |
|
64 let limit = this.maxTiles; |
|
65 let result = this._navHistoryService.executeQuery(query, options); |
|
66 let rootNode = result.root; |
|
67 rootNode.containerOpen = true; |
|
68 let childCount = rootNode.childCount; |
|
69 |
|
70 for (let i = 0, addedCount = 0; i < childCount && addedCount < limit; i++) { |
|
71 let node = rootNode.getChild(i); |
|
72 let uri = node.uri; |
|
73 let title = (node.title && node.title.length) ? node.title : uri; |
|
74 |
|
75 // If item is marked for deletion, skip it. |
|
76 if (this._toRemove && this._toRemove.indexOf(uri) !== -1) |
|
77 continue; |
|
78 |
|
79 let items = this._set.getItemsByUrl(uri); |
|
80 |
|
81 // Item has been unpinned, skip if filterUnpinned set. |
|
82 if (this._filterUnpinned && !this._pinHelper.isPinned(uri)) { |
|
83 if (items.length > 0) |
|
84 this.removeHistory(uri); |
|
85 |
|
86 continue; |
|
87 } |
|
88 |
|
89 if (!aRefresh || items.length === 0) { |
|
90 // If we're not refreshing or the item is not in the grid, add it. |
|
91 this.addItemToSet(uri, title, node.icon, addedCount); |
|
92 } else if (aRefresh && items.length > 0) { |
|
93 // Update context action in case it changed in another view. |
|
94 for (let item of items) { |
|
95 this._setContextActions(item); |
|
96 } |
|
97 } |
|
98 |
|
99 addedCount++; |
|
100 } |
|
101 |
|
102 // Remove extra items in case a refresh added more than the limit. |
|
103 // This can happen when undoing a delete. |
|
104 if (aRefresh) { |
|
105 while (this._set.itemCount > limit) |
|
106 this._set.removeItemAt(this._set.itemCount - 1); |
|
107 } |
|
108 |
|
109 rootNode.containerOpen = false; |
|
110 this._set.arrangeItems(); |
|
111 if (this._inBatch > 0) |
|
112 this._inBatch--; |
|
113 }, |
|
114 |
|
115 addItemToSet: function addItemToSet(aURI, aTitle, aIcon, aPos) { |
|
116 let item = this._set.insertItemAt(aPos || 0, aTitle, aURI, this._inBatch); |
|
117 this._setContextActions(item); |
|
118 this._updateFavicon(item, aURI); |
|
119 }, |
|
120 |
|
121 _setContextActions: function bv__setContextActions(aItem) { |
|
122 let uri = aItem.getAttribute("value"); |
|
123 aItem.setAttribute("data-contextactions", "delete," + (this._pinHelper.isPinned(uri) ? "hide" : "pin")); |
|
124 if ("refresh" in aItem) aItem.refresh(); |
|
125 }, |
|
126 |
|
127 _sendNeedsRefresh: function bv__sendNeedsRefresh(){ |
|
128 // Event sent when all views need to refresh. |
|
129 let event = document.createEvent("Events"); |
|
130 event.initEvent("HistoryNeedsRefresh", true, false); |
|
131 window.dispatchEvent(event); |
|
132 }, |
|
133 |
|
134 removeHistory: function (aUri) { |
|
135 let items = this._set.getItemsByUrl(aUri); |
|
136 for (let item of items) |
|
137 this._set.removeItem(item, true); |
|
138 if (!this._inBatch) |
|
139 this._set.arrangeItems(); |
|
140 }, |
|
141 |
|
142 doActionOnSelectedTiles: function bv_doActionOnSelectedTiles(aActionName, aEvent) { |
|
143 let tileGroup = this._set; |
|
144 let selectedTiles = tileGroup.selectedItems; |
|
145 |
|
146 // just arrange the grid once at the end of any action handling |
|
147 this._inBatch = true; |
|
148 |
|
149 switch (aActionName){ |
|
150 case "delete": |
|
151 Array.forEach(selectedTiles, function(aNode) { |
|
152 if (!this._toRemove) { |
|
153 this._toRemove = []; |
|
154 } |
|
155 |
|
156 let uri = aNode.getAttribute("value"); |
|
157 |
|
158 this._toRemove.push(uri); |
|
159 this.removeHistory(uri); |
|
160 }, this); |
|
161 |
|
162 // stop the appbar from dismissing |
|
163 aEvent.preventDefault(); |
|
164 |
|
165 // at next tick, re-populate the context appbar. |
|
166 setTimeout(function(){ |
|
167 // fire a MozContextActionsChange event to update the context appbar |
|
168 let event = document.createEvent("Events"); |
|
169 // we need the restore button to show (the tile node will go away though) |
|
170 event.actions = ["restore"]; |
|
171 event.initEvent("MozContextActionsChange", true, false); |
|
172 tileGroup.dispatchEvent(event); |
|
173 }, 0); |
|
174 break; |
|
175 |
|
176 case "restore": |
|
177 // clear toRemove and let _sendNeedsRefresh update the items. |
|
178 this._toRemove = null; |
|
179 break; |
|
180 |
|
181 case "unpin": |
|
182 Array.forEach(selectedTiles, function(aNode) { |
|
183 let uri = aNode.getAttribute("value"); |
|
184 |
|
185 if (this._filterUnpinned) |
|
186 this.removeHistory(uri); |
|
187 |
|
188 this._pinHelper.setUnpinned(uri); |
|
189 }, this); |
|
190 break; |
|
191 |
|
192 case "pin": |
|
193 Array.forEach(selectedTiles, function(aNode) { |
|
194 let uri = aNode.getAttribute("value"); |
|
195 |
|
196 this._pinHelper.setPinned(uri); |
|
197 }, this); |
|
198 break; |
|
199 |
|
200 default: |
|
201 this._inBatch = false; |
|
202 return; |
|
203 } |
|
204 |
|
205 this._inBatch = false; |
|
206 // Send refresh event so all view are in sync. |
|
207 this._sendNeedsRefresh(); |
|
208 }, |
|
209 |
|
210 handleEvent: function bv_handleEvent(aEvent) { |
|
211 switch (aEvent.type){ |
|
212 case "MozAppbarDismissing": |
|
213 // If undo wasn't pressed, time to do definitive actions. |
|
214 if (this._toRemove) { |
|
215 for (let uri of this._toRemove) { |
|
216 this._historyService.removePage(NetUtil.newURI(uri)); |
|
217 } |
|
218 |
|
219 // Clear context app bar |
|
220 let event = document.createEvent("Events"); |
|
221 event.actions = []; |
|
222 event.initEvent("MozContextActionsChange", true, false); |
|
223 this._set.dispatchEvent(event); |
|
224 |
|
225 this._toRemove = null; |
|
226 } |
|
227 break; |
|
228 |
|
229 case "HistoryNeedsRefresh": |
|
230 this.populateGrid(true); |
|
231 break; |
|
232 |
|
233 case "TabClose": |
|
234 // Flush any pending actions - appbar will call us back |
|
235 // before this returns with 'MozAppbarDismissing' above. |
|
236 StartUI.chromeWin.ContextUI.dismissContextAppbar(); |
|
237 break; |
|
238 } |
|
239 }, |
|
240 |
|
241 // nsINavHistoryObserver & helpers |
|
242 |
|
243 onBeginUpdateBatch: function() { |
|
244 // Avoid heavy grid redraws while a batch is in process |
|
245 this._inBatch++; |
|
246 }, |
|
247 |
|
248 onEndUpdateBatch: function() { |
|
249 this.populateGrid(true); |
|
250 if (this._inBatch > 0) { |
|
251 this._inBatch--; |
|
252 this._set.arrangeItems(); |
|
253 } |
|
254 }, |
|
255 |
|
256 onVisit: function(aURI, aVisitID, aTime, aSessionID, |
|
257 aReferringID, aTransitionType) { |
|
258 if (!this._inBatch) { |
|
259 this.populateGrid(true); |
|
260 } |
|
261 }, |
|
262 |
|
263 onTitleChanged: function(aURI, aPageTitle) { |
|
264 let changedItems = this._set.getItemsByUrl(aURI.spec); |
|
265 for (let item of changedItems) { |
|
266 item.setAttribute("label", aPageTitle); |
|
267 } |
|
268 }, |
|
269 |
|
270 onDeleteURI: function(aURI) { |
|
271 this.removeHistory(aURI.spec); |
|
272 }, |
|
273 |
|
274 onClearHistory: function() { |
|
275 if ('clearAll' in this._set) |
|
276 this._set.clearAll(); |
|
277 }, |
|
278 |
|
279 onPageChanged: function(aURI, aWhat, aValue) { |
|
280 if (aWhat == Ci.nsINavHistoryObserver.ATTRIBUTE_FAVICON) { |
|
281 let changedItems = this._set.getItemsByUrl(aURI.spec); |
|
282 for (let item of changedItems) { |
|
283 let currIcon = item.getAttribute("iconURI"); |
|
284 if (currIcon != aValue) { |
|
285 item.setAttribute("iconURI", aValue); |
|
286 if ("refresh" in item) |
|
287 item.refresh(); |
|
288 } |
|
289 } |
|
290 } |
|
291 }, |
|
292 |
|
293 onDeleteVisits: function (aURI, aVisitTime, aGUID, aReason, aTransitionType) { |
|
294 if ((aReason == Ci.nsINavHistoryObserver.REASON_DELETED) && !this._inBatch) { |
|
295 this.populateGrid(true); |
|
296 } |
|
297 }, |
|
298 |
|
299 QueryInterface: function(iid) { |
|
300 if (iid.equals(Components.interfaces.nsINavHistoryObserver) || |
|
301 iid.equals(Components.interfaces.nsISupports)) { |
|
302 return this; |
|
303 } |
|
304 throw Cr.NS_ERROR_NO_INTERFACE; |
|
305 } |
|
306 }); |
|
307 |
|
308 let HistoryStartView = { |
|
309 _view: null, |
|
310 get _grid() { return document.getElementById("start-history-grid"); }, |
|
311 |
|
312 init: function init() { |
|
313 this._view = new HistoryView(this._grid, true); |
|
314 this._view.populateGrid(); |
|
315 this._grid.removeAttribute("fade"); |
|
316 }, |
|
317 |
|
318 uninit: function uninit() { |
|
319 if (this._view) { |
|
320 this._view.destruct(); |
|
321 } |
|
322 } |
|
323 }; |