|
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 /** |
|
8 * Wraps a list/grid control implementing nsIDOMXULSelectControlElement and |
|
9 * fills it with the user's bookmarks. |
|
10 * |
|
11 * @param aSet Control implementing nsIDOMXULSelectControlElement. |
|
12 * @param aRoot Bookmark root to show in the view. |
|
13 */ |
|
14 function BookmarksView(aSet, aRoot, aFilterUnpinned) { |
|
15 View.call(this, aSet); |
|
16 |
|
17 this._inBatch = false; // batch up grid updates to avoid redundant arrangeItems calls |
|
18 |
|
19 // View monitors this for maximum tile display counts |
|
20 this.tilePrefName = "browser.display.startUI.bookmarks.maxresults"; |
|
21 this.showing = this.maxTiles > 0; |
|
22 |
|
23 this._filterUnpinned = aFilterUnpinned; |
|
24 this._bookmarkService = PlacesUtils.bookmarks; |
|
25 this._navHistoryService = gHistSvc; |
|
26 |
|
27 this._changes = new BookmarkChangeListener(this); |
|
28 this._pinHelper = new ItemPinHelper("metro.bookmarks.unpinned"); |
|
29 this._bookmarkService.addObserver(this._changes, false); |
|
30 StartUI.chromeWin.addEventListener('MozAppbarDismissing', this, false); |
|
31 StartUI.chromeWin.addEventListener('BookmarksNeedsRefresh', this, false); |
|
32 window.addEventListener("TabClose", this, true); |
|
33 |
|
34 this.root = aRoot; |
|
35 } |
|
36 |
|
37 BookmarksView.prototype = Util.extend(Object.create(View.prototype), { |
|
38 _set: null, |
|
39 _changes: null, |
|
40 _root: null, |
|
41 _sort: 0, // Natural bookmark order. |
|
42 _toRemove: null, |
|
43 |
|
44 // For View's showing property |
|
45 get vbox() { |
|
46 return document.getElementById("start-bookmarks"); |
|
47 }, |
|
48 |
|
49 get sort() { |
|
50 return this._sort; |
|
51 }, |
|
52 |
|
53 set sort(aSort) { |
|
54 this._sort = aSort; |
|
55 this.clearBookmarks(); |
|
56 this.getBookmarks(); |
|
57 }, |
|
58 |
|
59 get root() { |
|
60 return this._root; |
|
61 }, |
|
62 |
|
63 set root(aRoot) { |
|
64 this._root = aRoot; |
|
65 }, |
|
66 |
|
67 destruct: function bv_destruct() { |
|
68 this._bookmarkService.removeObserver(this._changes); |
|
69 if (StartUI.chromeWin) { |
|
70 StartUI.chromeWin.removeEventListener('MozAppbarDismissing', this, false); |
|
71 StartUI.chromeWin.removeEventListener('BookmarksNeedsRefresh', this, false); |
|
72 } |
|
73 View.prototype.destruct.call(this); |
|
74 }, |
|
75 |
|
76 refreshView: function () { |
|
77 this.clearBookmarks(); |
|
78 this.getBookmarks(); |
|
79 }, |
|
80 |
|
81 handleItemClick: function bv_handleItemClick(aItem) { |
|
82 let url = aItem.getAttribute("value"); |
|
83 StartUI.goToURI(url); |
|
84 }, |
|
85 |
|
86 _getItemForBookmarkId: function bv__getItemForBookmark(aBookmarkId) { |
|
87 return this._set.querySelector("richgriditem[anonid='" + aBookmarkId + "']"); |
|
88 }, |
|
89 |
|
90 _getBookmarkIdForItem: function bv__getBookmarkForItem(aItem) { |
|
91 return +aItem.getAttribute("anonid"); |
|
92 }, |
|
93 |
|
94 _updateItemWithAttrs: function dv__updateItemWithAttrs(anItem, aAttrs) { |
|
95 for (let name in aAttrs) |
|
96 anItem.setAttribute(name, aAttrs[name]); |
|
97 }, |
|
98 |
|
99 getBookmarks: function bv_getBookmarks(aRefresh) { |
|
100 let options = this._navHistoryService.getNewQueryOptions(); |
|
101 options.queryType = options.QUERY_TYPE_BOOKMARKS; |
|
102 options.excludeQueries = true; // Don't include "smart folders" |
|
103 options.sortingMode = this._sort; |
|
104 |
|
105 let limit = this.maxTiles; |
|
106 |
|
107 let query = this._navHistoryService.getNewQuery(); |
|
108 query.setFolders([Bookmarks.metroRoot], 1); |
|
109 |
|
110 let result = this._navHistoryService.executeQuery(query, options); |
|
111 let rootNode = result.root; |
|
112 rootNode.containerOpen = true; |
|
113 let childCount = rootNode.childCount; |
|
114 |
|
115 this._inBatch = true; // batch up grid updates to avoid redundant arrangeItems calls |
|
116 |
|
117 for (let i = 0, addedCount = 0; i < childCount && addedCount < limit; i++) { |
|
118 let node = rootNode.getChild(i); |
|
119 |
|
120 // Ignore folders, separators, undefined item types, etc. |
|
121 if (node.type != node.RESULT_TYPE_URI) |
|
122 continue; |
|
123 |
|
124 // If item is marked for deletion, skip it. |
|
125 if (this._toRemove && this._toRemove.indexOf(node.itemId) !== -1) |
|
126 continue; |
|
127 |
|
128 let item = this._getItemForBookmarkId(node.itemId); |
|
129 |
|
130 // Item has been unpinned. |
|
131 if (this._filterUnpinned && !this._pinHelper.isPinned(node.itemId)) { |
|
132 if (item) |
|
133 this.removeBookmark(node.itemId); |
|
134 |
|
135 continue; |
|
136 } |
|
137 |
|
138 if (!aRefresh || !item) { |
|
139 // If we're not refreshing or the item is not in the grid, add it. |
|
140 this.addBookmark(node.itemId, addedCount); |
|
141 } else if (aRefresh && item) { |
|
142 // Update context action in case it changed in another view. |
|
143 this._setContextActions(item); |
|
144 } |
|
145 |
|
146 addedCount++; |
|
147 } |
|
148 |
|
149 // Remove extra items in case a refresh added more than the limit. |
|
150 // This can happen when undoing a delete. |
|
151 if (aRefresh) { |
|
152 while (this._set.itemCount > limit) |
|
153 this._set.removeItemAt(this._set.itemCount - 1, true); |
|
154 } |
|
155 this._set.arrangeItems(); |
|
156 this._inBatch = false; |
|
157 rootNode.containerOpen = false; |
|
158 }, |
|
159 |
|
160 inCurrentView: function bv_inCurrentView(aParentId, aItemId) { |
|
161 if (this._root && aParentId != this._root) |
|
162 return false; |
|
163 |
|
164 return !!this._getItemForBookmarkId(aItemId); |
|
165 }, |
|
166 |
|
167 clearBookmarks: function bv_clearBookmarks() { |
|
168 if ('clearAll' in this._set) |
|
169 this._set.clearAll(); |
|
170 }, |
|
171 |
|
172 addBookmark: function bv_addBookmark(aBookmarkId, aPos) { |
|
173 let index = this._bookmarkService.getItemIndex(aBookmarkId); |
|
174 let uri = this._bookmarkService.getBookmarkURI(aBookmarkId); |
|
175 let title = this._bookmarkService.getItemTitle(aBookmarkId) || uri.spec; |
|
176 let item = this._set.insertItemAt(aPos || index, title, uri.spec, this._inBatch); |
|
177 item.setAttribute("anonid", aBookmarkId); |
|
178 this._setContextActions(item); |
|
179 this._updateFavicon(item, uri); |
|
180 }, |
|
181 |
|
182 _setContextActions: function bv__setContextActions(aItem) { |
|
183 let itemId = this._getBookmarkIdForItem(aItem); |
|
184 aItem.setAttribute("data-contextactions", "delete," + (this._pinHelper.isPinned(itemId) ? "hide" : "pin")); |
|
185 if (aItem.refresh) aItem.refresh(); |
|
186 }, |
|
187 |
|
188 _sendNeedsRefresh: function bv__sendNeedsRefresh(){ |
|
189 // Event sent when all view instances need to refresh. |
|
190 let event = document.createEvent("Events"); |
|
191 event.initEvent("BookmarksNeedsRefresh", true, false); |
|
192 window.dispatchEvent(event); |
|
193 }, |
|
194 |
|
195 updateBookmark: function bv_updateBookmark(aBookmarkId) { |
|
196 let item = this._getItemForBookmarkId(aBookmarkId); |
|
197 |
|
198 if (!item) |
|
199 return; |
|
200 |
|
201 let oldIndex = this._set.getIndexOfItem(item); |
|
202 let index = this._bookmarkService.getItemIndex(aBookmarkId); |
|
203 |
|
204 if (oldIndex != index) { |
|
205 this.removeBookmark(aBookmarkId); |
|
206 this.addBookmark(aBookmarkId); |
|
207 return; |
|
208 } |
|
209 |
|
210 let uri = this._bookmarkService.getBookmarkURI(aBookmarkId); |
|
211 let title = this._bookmarkService.getItemTitle(aBookmarkId) || uri.spec; |
|
212 |
|
213 item.setAttribute("anonid", aBookmarkId); |
|
214 item.setAttribute("value", uri.spec); |
|
215 item.setAttribute("label", title); |
|
216 |
|
217 this._updateFavicon(item, uri); |
|
218 }, |
|
219 |
|
220 removeBookmark: function bv_removeBookmark(aBookmarkId) { |
|
221 let item = this._getItemForBookmarkId(aBookmarkId); |
|
222 let index = this._set.getIndexOfItem(item); |
|
223 this._set.removeItemAt(index, this._inBatch); |
|
224 }, |
|
225 |
|
226 doActionOnSelectedTiles: function bv_doActionOnSelectedTiles(aActionName, aEvent) { |
|
227 let tileGroup = this._set; |
|
228 let selectedTiles = tileGroup.selectedItems; |
|
229 |
|
230 switch (aActionName){ |
|
231 case "delete": |
|
232 Array.forEach(selectedTiles, function(aNode) { |
|
233 if (!this._toRemove) { |
|
234 this._toRemove = []; |
|
235 } |
|
236 |
|
237 let itemId = this._getBookmarkIdForItem(aNode); |
|
238 |
|
239 this._toRemove.push(itemId); |
|
240 this.removeBookmark(itemId); |
|
241 }, this); |
|
242 |
|
243 // stop the appbar from dismissing |
|
244 aEvent.preventDefault(); |
|
245 |
|
246 // at next tick, re-populate the context appbar. |
|
247 setTimeout(function(){ |
|
248 // fire a MozContextActionsChange event to update the context appbar |
|
249 let event = document.createEvent("Events"); |
|
250 // we need the restore button to show (the tile node will go away though) |
|
251 event.actions = ["restore"]; |
|
252 event.initEvent("MozContextActionsChange", true, false); |
|
253 tileGroup.dispatchEvent(event); |
|
254 }, 0); |
|
255 break; |
|
256 |
|
257 case "restore": |
|
258 // clear toRemove and let _sendNeedsRefresh update the items. |
|
259 this._toRemove = null; |
|
260 break; |
|
261 |
|
262 case "unpin": |
|
263 Array.forEach(selectedTiles, function(aNode) { |
|
264 let itemId = this._getBookmarkIdForItem(aNode); |
|
265 |
|
266 if (this._filterUnpinned) |
|
267 this.removeBookmark(itemId); |
|
268 |
|
269 this._pinHelper.setUnpinned(itemId); |
|
270 }, this); |
|
271 break; |
|
272 |
|
273 case "pin": |
|
274 Array.forEach(selectedTiles, function(aNode) { |
|
275 let itemId = this._getBookmarkIdForItem(aNode); |
|
276 |
|
277 this._pinHelper.setPinned(itemId); |
|
278 }, this); |
|
279 break; |
|
280 |
|
281 default: |
|
282 return; |
|
283 } |
|
284 |
|
285 // Send refresh event so all view are in sync. |
|
286 this._sendNeedsRefresh(); |
|
287 }, |
|
288 |
|
289 handleEvent: function bv_handleEvent(aEvent) { |
|
290 switch (aEvent.type){ |
|
291 case "MozAppbarDismissing": |
|
292 // If undo wasn't pressed, time to do definitive actions. |
|
293 if (this._toRemove) { |
|
294 for (let bookmarkId of this._toRemove) { |
|
295 this._bookmarkService.removeItem(bookmarkId); |
|
296 } |
|
297 this._toRemove = null; |
|
298 } |
|
299 break; |
|
300 |
|
301 case "BookmarksNeedsRefresh": |
|
302 this.getBookmarks(true); |
|
303 break; |
|
304 |
|
305 case "TabClose": |
|
306 // Flush any pending actions - appbar will call us back |
|
307 // before this returns with 'MozAppbarDismissing' above. |
|
308 StartUI.chromeWin.ContextUI.dismissContextAppbar(); |
|
309 break; |
|
310 } |
|
311 } |
|
312 }); |
|
313 |
|
314 let BookmarksStartView = { |
|
315 _view: null, |
|
316 get _grid() { return document.getElementById("start-bookmarks-grid"); }, |
|
317 |
|
318 init: function init() { |
|
319 this._view = new BookmarksView(this._grid, Bookmarks.metroRoot, true); |
|
320 this._view.getBookmarks(); |
|
321 this._grid.removeAttribute("fade"); |
|
322 }, |
|
323 |
|
324 uninit: function uninit() { |
|
325 if (this._view) { |
|
326 this._view.destruct(); |
|
327 } |
|
328 }, |
|
329 }; |
|
330 |
|
331 /** |
|
332 * Observes bookmark changes and keeps a linked BookmarksView updated. |
|
333 * |
|
334 * @param aView An instance of BookmarksView. |
|
335 */ |
|
336 function BookmarkChangeListener(aView) { |
|
337 this._view = aView; |
|
338 } |
|
339 |
|
340 BookmarkChangeListener.prototype = { |
|
341 ////////////////////////////////////////////////////////////////////////////// |
|
342 //// nsINavBookmarkObserver |
|
343 onBeginUpdateBatch: function () { }, |
|
344 onEndUpdateBatch: function () { }, |
|
345 |
|
346 onItemAdded: function bCL_onItemAdded(aItemId, aParentId, aIndex, aItemType, aURI, aTitle, aDateAdded, aGUID, aParentGUID) { |
|
347 this._view.getBookmarks(true); |
|
348 }, |
|
349 |
|
350 onItemChanged: function bCL_onItemChanged(aItemId, aProperty, aIsAnnotationProperty, aNewValue, aLastModified, aItemType, aParentId, aGUID, aParentGUID) { |
|
351 let itemIndex = PlacesUtils.bookmarks.getItemIndex(aItemId); |
|
352 if (!this._view.inCurrentView(aParentId, aItemId)) |
|
353 return; |
|
354 |
|
355 this._view.updateBookmark(aItemId); |
|
356 }, |
|
357 |
|
358 onItemMoved: function bCL_onItemMoved(aItemId, aOldParentId, aOldIndex, aNewParentId, aNewIndex, aItemType, aGUID, aOldParentGUID, aNewParentGUID) { |
|
359 let wasInView = this._view.inCurrentView(aOldParentId, aItemId); |
|
360 let nowInView = this._view.inCurrentView(aNewParentId, aItemId); |
|
361 |
|
362 if (!wasInView && nowInView) |
|
363 this._view.addBookmark(aItemId); |
|
364 |
|
365 if (wasInView && !nowInView) |
|
366 this._view.removeBookmark(aItemId); |
|
367 |
|
368 this._view.getBookmarks(true); |
|
369 }, |
|
370 |
|
371 onBeforeItemRemoved: function (aItemId, aItemType, aParentId, aGUID, aParentGUID) { }, |
|
372 onItemRemoved: function bCL_onItemRemoved(aItemId, aParentId, aIndex, aItemType, aURI, aGUID, aParentGUID) { |
|
373 if (!this._view.inCurrentView(aParentId, aItemId)) |
|
374 return; |
|
375 |
|
376 this._view.removeBookmark(aItemId); |
|
377 this._view.getBookmarks(true); |
|
378 }, |
|
379 |
|
380 onItemVisited: function(aItemId, aVisitId, aTime, aTransitionType, aURI, aParentId, aGUID, aParentGUID) { }, |
|
381 |
|
382 ////////////////////////////////////////////////////////////////////////////// |
|
383 //// nsISupports |
|
384 QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarkObserver]) |
|
385 }; |