Wed, 31 Dec 2014 06:55:50 +0100
Added tag UPSTREAM_283F7C6 for changeset ca08bd8f51b2
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/. */
5 "use strict";
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);
17 this._inBatch = false; // batch up grid updates to avoid redundant arrangeItems calls
19 // View monitors this for maximum tile display counts
20 this.tilePrefName = "browser.display.startUI.bookmarks.maxresults";
21 this.showing = this.maxTiles > 0;
23 this._filterUnpinned = aFilterUnpinned;
24 this._bookmarkService = PlacesUtils.bookmarks;
25 this._navHistoryService = gHistSvc;
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);
34 this.root = aRoot;
35 }
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,
44 // For View's showing property
45 get vbox() {
46 return document.getElementById("start-bookmarks");
47 },
49 get sort() {
50 return this._sort;
51 },
53 set sort(aSort) {
54 this._sort = aSort;
55 this.clearBookmarks();
56 this.getBookmarks();
57 },
59 get root() {
60 return this._root;
61 },
63 set root(aRoot) {
64 this._root = aRoot;
65 },
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 },
76 refreshView: function () {
77 this.clearBookmarks();
78 this.getBookmarks();
79 },
81 handleItemClick: function bv_handleItemClick(aItem) {
82 let url = aItem.getAttribute("value");
83 StartUI.goToURI(url);
84 },
86 _getItemForBookmarkId: function bv__getItemForBookmark(aBookmarkId) {
87 return this._set.querySelector("richgriditem[anonid='" + aBookmarkId + "']");
88 },
90 _getBookmarkIdForItem: function bv__getBookmarkForItem(aItem) {
91 return +aItem.getAttribute("anonid");
92 },
94 _updateItemWithAttrs: function dv__updateItemWithAttrs(anItem, aAttrs) {
95 for (let name in aAttrs)
96 anItem.setAttribute(name, aAttrs[name]);
97 },
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;
105 let limit = this.maxTiles;
107 let query = this._navHistoryService.getNewQuery();
108 query.setFolders([Bookmarks.metroRoot], 1);
110 let result = this._navHistoryService.executeQuery(query, options);
111 let rootNode = result.root;
112 rootNode.containerOpen = true;
113 let childCount = rootNode.childCount;
115 this._inBatch = true; // batch up grid updates to avoid redundant arrangeItems calls
117 for (let i = 0, addedCount = 0; i < childCount && addedCount < limit; i++) {
118 let node = rootNode.getChild(i);
120 // Ignore folders, separators, undefined item types, etc.
121 if (node.type != node.RESULT_TYPE_URI)
122 continue;
124 // If item is marked for deletion, skip it.
125 if (this._toRemove && this._toRemove.indexOf(node.itemId) !== -1)
126 continue;
128 let item = this._getItemForBookmarkId(node.itemId);
130 // Item has been unpinned.
131 if (this._filterUnpinned && !this._pinHelper.isPinned(node.itemId)) {
132 if (item)
133 this.removeBookmark(node.itemId);
135 continue;
136 }
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 }
146 addedCount++;
147 }
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 },
160 inCurrentView: function bv_inCurrentView(aParentId, aItemId) {
161 if (this._root && aParentId != this._root)
162 return false;
164 return !!this._getItemForBookmarkId(aItemId);
165 },
167 clearBookmarks: function bv_clearBookmarks() {
168 if ('clearAll' in this._set)
169 this._set.clearAll();
170 },
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 },
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 },
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 },
195 updateBookmark: function bv_updateBookmark(aBookmarkId) {
196 let item = this._getItemForBookmarkId(aBookmarkId);
198 if (!item)
199 return;
201 let oldIndex = this._set.getIndexOfItem(item);
202 let index = this._bookmarkService.getItemIndex(aBookmarkId);
204 if (oldIndex != index) {
205 this.removeBookmark(aBookmarkId);
206 this.addBookmark(aBookmarkId);
207 return;
208 }
210 let uri = this._bookmarkService.getBookmarkURI(aBookmarkId);
211 let title = this._bookmarkService.getItemTitle(aBookmarkId) || uri.spec;
213 item.setAttribute("anonid", aBookmarkId);
214 item.setAttribute("value", uri.spec);
215 item.setAttribute("label", title);
217 this._updateFavicon(item, uri);
218 },
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 },
226 doActionOnSelectedTiles: function bv_doActionOnSelectedTiles(aActionName, aEvent) {
227 let tileGroup = this._set;
228 let selectedTiles = tileGroup.selectedItems;
230 switch (aActionName){
231 case "delete":
232 Array.forEach(selectedTiles, function(aNode) {
233 if (!this._toRemove) {
234 this._toRemove = [];
235 }
237 let itemId = this._getBookmarkIdForItem(aNode);
239 this._toRemove.push(itemId);
240 this.removeBookmark(itemId);
241 }, this);
243 // stop the appbar from dismissing
244 aEvent.preventDefault();
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;
257 case "restore":
258 // clear toRemove and let _sendNeedsRefresh update the items.
259 this._toRemove = null;
260 break;
262 case "unpin":
263 Array.forEach(selectedTiles, function(aNode) {
264 let itemId = this._getBookmarkIdForItem(aNode);
266 if (this._filterUnpinned)
267 this.removeBookmark(itemId);
269 this._pinHelper.setUnpinned(itemId);
270 }, this);
271 break;
273 case "pin":
274 Array.forEach(selectedTiles, function(aNode) {
275 let itemId = this._getBookmarkIdForItem(aNode);
277 this._pinHelper.setPinned(itemId);
278 }, this);
279 break;
281 default:
282 return;
283 }
285 // Send refresh event so all view are in sync.
286 this._sendNeedsRefresh();
287 },
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;
301 case "BookmarksNeedsRefresh":
302 this.getBookmarks(true);
303 break;
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 });
314 let BookmarksStartView = {
315 _view: null,
316 get _grid() { return document.getElementById("start-bookmarks-grid"); },
318 init: function init() {
319 this._view = new BookmarksView(this._grid, Bookmarks.metroRoot, true);
320 this._view.getBookmarks();
321 this._grid.removeAttribute("fade");
322 },
324 uninit: function uninit() {
325 if (this._view) {
326 this._view.destruct();
327 }
328 },
329 };
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 }
340 BookmarkChangeListener.prototype = {
341 //////////////////////////////////////////////////////////////////////////////
342 //// nsINavBookmarkObserver
343 onBeginUpdateBatch: function () { },
344 onEndUpdateBatch: function () { },
346 onItemAdded: function bCL_onItemAdded(aItemId, aParentId, aIndex, aItemType, aURI, aTitle, aDateAdded, aGUID, aParentGUID) {
347 this._view.getBookmarks(true);
348 },
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;
355 this._view.updateBookmark(aItemId);
356 },
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);
362 if (!wasInView && nowInView)
363 this._view.addBookmark(aItemId);
365 if (wasInView && !nowInView)
366 this._view.removeBookmark(aItemId);
368 this._view.getBookmarks(true);
369 },
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;
376 this._view.removeBookmark(aItemId);
377 this._view.getBookmarks(true);
378 },
380 onItemVisited: function(aItemId, aVisitId, aTime, aTransitionType, aURI, aParentId, aGUID, aParentGUID) { },
382 //////////////////////////////////////////////////////////////////////////////
383 //// nsISupports
384 QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarkObserver])
385 };