1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/metro/base/content/startui/BookmarksView.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,385 @@ 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 +/** 1.11 + * Wraps a list/grid control implementing nsIDOMXULSelectControlElement and 1.12 + * fills it with the user's bookmarks. 1.13 + * 1.14 + * @param aSet Control implementing nsIDOMXULSelectControlElement. 1.15 + * @param aRoot Bookmark root to show in the view. 1.16 + */ 1.17 +function BookmarksView(aSet, aRoot, aFilterUnpinned) { 1.18 + View.call(this, aSet); 1.19 + 1.20 + this._inBatch = false; // batch up grid updates to avoid redundant arrangeItems calls 1.21 + 1.22 + // View monitors this for maximum tile display counts 1.23 + this.tilePrefName = "browser.display.startUI.bookmarks.maxresults"; 1.24 + this.showing = this.maxTiles > 0; 1.25 + 1.26 + this._filterUnpinned = aFilterUnpinned; 1.27 + this._bookmarkService = PlacesUtils.bookmarks; 1.28 + this._navHistoryService = gHistSvc; 1.29 + 1.30 + this._changes = new BookmarkChangeListener(this); 1.31 + this._pinHelper = new ItemPinHelper("metro.bookmarks.unpinned"); 1.32 + this._bookmarkService.addObserver(this._changes, false); 1.33 + StartUI.chromeWin.addEventListener('MozAppbarDismissing', this, false); 1.34 + StartUI.chromeWin.addEventListener('BookmarksNeedsRefresh', this, false); 1.35 + window.addEventListener("TabClose", this, true); 1.36 + 1.37 + this.root = aRoot; 1.38 +} 1.39 + 1.40 +BookmarksView.prototype = Util.extend(Object.create(View.prototype), { 1.41 + _set: null, 1.42 + _changes: null, 1.43 + _root: null, 1.44 + _sort: 0, // Natural bookmark order. 1.45 + _toRemove: null, 1.46 + 1.47 + // For View's showing property 1.48 + get vbox() { 1.49 + return document.getElementById("start-bookmarks"); 1.50 + }, 1.51 + 1.52 + get sort() { 1.53 + return this._sort; 1.54 + }, 1.55 + 1.56 + set sort(aSort) { 1.57 + this._sort = aSort; 1.58 + this.clearBookmarks(); 1.59 + this.getBookmarks(); 1.60 + }, 1.61 + 1.62 + get root() { 1.63 + return this._root; 1.64 + }, 1.65 + 1.66 + set root(aRoot) { 1.67 + this._root = aRoot; 1.68 + }, 1.69 + 1.70 + destruct: function bv_destruct() { 1.71 + this._bookmarkService.removeObserver(this._changes); 1.72 + if (StartUI.chromeWin) { 1.73 + StartUI.chromeWin.removeEventListener('MozAppbarDismissing', this, false); 1.74 + StartUI.chromeWin.removeEventListener('BookmarksNeedsRefresh', this, false); 1.75 + } 1.76 + View.prototype.destruct.call(this); 1.77 + }, 1.78 + 1.79 + refreshView: function () { 1.80 + this.clearBookmarks(); 1.81 + this.getBookmarks(); 1.82 + }, 1.83 + 1.84 + handleItemClick: function bv_handleItemClick(aItem) { 1.85 + let url = aItem.getAttribute("value"); 1.86 + StartUI.goToURI(url); 1.87 + }, 1.88 + 1.89 + _getItemForBookmarkId: function bv__getItemForBookmark(aBookmarkId) { 1.90 + return this._set.querySelector("richgriditem[anonid='" + aBookmarkId + "']"); 1.91 + }, 1.92 + 1.93 + _getBookmarkIdForItem: function bv__getBookmarkForItem(aItem) { 1.94 + return +aItem.getAttribute("anonid"); 1.95 + }, 1.96 + 1.97 + _updateItemWithAttrs: function dv__updateItemWithAttrs(anItem, aAttrs) { 1.98 + for (let name in aAttrs) 1.99 + anItem.setAttribute(name, aAttrs[name]); 1.100 + }, 1.101 + 1.102 + getBookmarks: function bv_getBookmarks(aRefresh) { 1.103 + let options = this._navHistoryService.getNewQueryOptions(); 1.104 + options.queryType = options.QUERY_TYPE_BOOKMARKS; 1.105 + options.excludeQueries = true; // Don't include "smart folders" 1.106 + options.sortingMode = this._sort; 1.107 + 1.108 + let limit = this.maxTiles; 1.109 + 1.110 + let query = this._navHistoryService.getNewQuery(); 1.111 + query.setFolders([Bookmarks.metroRoot], 1); 1.112 + 1.113 + let result = this._navHistoryService.executeQuery(query, options); 1.114 + let rootNode = result.root; 1.115 + rootNode.containerOpen = true; 1.116 + let childCount = rootNode.childCount; 1.117 + 1.118 + this._inBatch = true; // batch up grid updates to avoid redundant arrangeItems calls 1.119 + 1.120 + for (let i = 0, addedCount = 0; i < childCount && addedCount < limit; i++) { 1.121 + let node = rootNode.getChild(i); 1.122 + 1.123 + // Ignore folders, separators, undefined item types, etc. 1.124 + if (node.type != node.RESULT_TYPE_URI) 1.125 + continue; 1.126 + 1.127 + // If item is marked for deletion, skip it. 1.128 + if (this._toRemove && this._toRemove.indexOf(node.itemId) !== -1) 1.129 + continue; 1.130 + 1.131 + let item = this._getItemForBookmarkId(node.itemId); 1.132 + 1.133 + // Item has been unpinned. 1.134 + if (this._filterUnpinned && !this._pinHelper.isPinned(node.itemId)) { 1.135 + if (item) 1.136 + this.removeBookmark(node.itemId); 1.137 + 1.138 + continue; 1.139 + } 1.140 + 1.141 + if (!aRefresh || !item) { 1.142 + // If we're not refreshing or the item is not in the grid, add it. 1.143 + this.addBookmark(node.itemId, addedCount); 1.144 + } else if (aRefresh && item) { 1.145 + // Update context action in case it changed in another view. 1.146 + this._setContextActions(item); 1.147 + } 1.148 + 1.149 + addedCount++; 1.150 + } 1.151 + 1.152 + // Remove extra items in case a refresh added more than the limit. 1.153 + // This can happen when undoing a delete. 1.154 + if (aRefresh) { 1.155 + while (this._set.itemCount > limit) 1.156 + this._set.removeItemAt(this._set.itemCount - 1, true); 1.157 + } 1.158 + this._set.arrangeItems(); 1.159 + this._inBatch = false; 1.160 + rootNode.containerOpen = false; 1.161 + }, 1.162 + 1.163 + inCurrentView: function bv_inCurrentView(aParentId, aItemId) { 1.164 + if (this._root && aParentId != this._root) 1.165 + return false; 1.166 + 1.167 + return !!this._getItemForBookmarkId(aItemId); 1.168 + }, 1.169 + 1.170 + clearBookmarks: function bv_clearBookmarks() { 1.171 + if ('clearAll' in this._set) 1.172 + this._set.clearAll(); 1.173 + }, 1.174 + 1.175 + addBookmark: function bv_addBookmark(aBookmarkId, aPos) { 1.176 + let index = this._bookmarkService.getItemIndex(aBookmarkId); 1.177 + let uri = this._bookmarkService.getBookmarkURI(aBookmarkId); 1.178 + let title = this._bookmarkService.getItemTitle(aBookmarkId) || uri.spec; 1.179 + let item = this._set.insertItemAt(aPos || index, title, uri.spec, this._inBatch); 1.180 + item.setAttribute("anonid", aBookmarkId); 1.181 + this._setContextActions(item); 1.182 + this._updateFavicon(item, uri); 1.183 + }, 1.184 + 1.185 + _setContextActions: function bv__setContextActions(aItem) { 1.186 + let itemId = this._getBookmarkIdForItem(aItem); 1.187 + aItem.setAttribute("data-contextactions", "delete," + (this._pinHelper.isPinned(itemId) ? "hide" : "pin")); 1.188 + if (aItem.refresh) aItem.refresh(); 1.189 + }, 1.190 + 1.191 + _sendNeedsRefresh: function bv__sendNeedsRefresh(){ 1.192 + // Event sent when all view instances need to refresh. 1.193 + let event = document.createEvent("Events"); 1.194 + event.initEvent("BookmarksNeedsRefresh", true, false); 1.195 + window.dispatchEvent(event); 1.196 + }, 1.197 + 1.198 + updateBookmark: function bv_updateBookmark(aBookmarkId) { 1.199 + let item = this._getItemForBookmarkId(aBookmarkId); 1.200 + 1.201 + if (!item) 1.202 + return; 1.203 + 1.204 + let oldIndex = this._set.getIndexOfItem(item); 1.205 + let index = this._bookmarkService.getItemIndex(aBookmarkId); 1.206 + 1.207 + if (oldIndex != index) { 1.208 + this.removeBookmark(aBookmarkId); 1.209 + this.addBookmark(aBookmarkId); 1.210 + return; 1.211 + } 1.212 + 1.213 + let uri = this._bookmarkService.getBookmarkURI(aBookmarkId); 1.214 + let title = this._bookmarkService.getItemTitle(aBookmarkId) || uri.spec; 1.215 + 1.216 + item.setAttribute("anonid", aBookmarkId); 1.217 + item.setAttribute("value", uri.spec); 1.218 + item.setAttribute("label", title); 1.219 + 1.220 + this._updateFavicon(item, uri); 1.221 + }, 1.222 + 1.223 + removeBookmark: function bv_removeBookmark(aBookmarkId) { 1.224 + let item = this._getItemForBookmarkId(aBookmarkId); 1.225 + let index = this._set.getIndexOfItem(item); 1.226 + this._set.removeItemAt(index, this._inBatch); 1.227 + }, 1.228 + 1.229 + doActionOnSelectedTiles: function bv_doActionOnSelectedTiles(aActionName, aEvent) { 1.230 + let tileGroup = this._set; 1.231 + let selectedTiles = tileGroup.selectedItems; 1.232 + 1.233 + switch (aActionName){ 1.234 + case "delete": 1.235 + Array.forEach(selectedTiles, function(aNode) { 1.236 + if (!this._toRemove) { 1.237 + this._toRemove = []; 1.238 + } 1.239 + 1.240 + let itemId = this._getBookmarkIdForItem(aNode); 1.241 + 1.242 + this._toRemove.push(itemId); 1.243 + this.removeBookmark(itemId); 1.244 + }, this); 1.245 + 1.246 + // stop the appbar from dismissing 1.247 + aEvent.preventDefault(); 1.248 + 1.249 + // at next tick, re-populate the context appbar. 1.250 + setTimeout(function(){ 1.251 + // fire a MozContextActionsChange event to update the context appbar 1.252 + let event = document.createEvent("Events"); 1.253 + // we need the restore button to show (the tile node will go away though) 1.254 + event.actions = ["restore"]; 1.255 + event.initEvent("MozContextActionsChange", true, false); 1.256 + tileGroup.dispatchEvent(event); 1.257 + }, 0); 1.258 + break; 1.259 + 1.260 + case "restore": 1.261 + // clear toRemove and let _sendNeedsRefresh update the items. 1.262 + this._toRemove = null; 1.263 + break; 1.264 + 1.265 + case "unpin": 1.266 + Array.forEach(selectedTiles, function(aNode) { 1.267 + let itemId = this._getBookmarkIdForItem(aNode); 1.268 + 1.269 + if (this._filterUnpinned) 1.270 + this.removeBookmark(itemId); 1.271 + 1.272 + this._pinHelper.setUnpinned(itemId); 1.273 + }, this); 1.274 + break; 1.275 + 1.276 + case "pin": 1.277 + Array.forEach(selectedTiles, function(aNode) { 1.278 + let itemId = this._getBookmarkIdForItem(aNode); 1.279 + 1.280 + this._pinHelper.setPinned(itemId); 1.281 + }, this); 1.282 + break; 1.283 + 1.284 + default: 1.285 + return; 1.286 + } 1.287 + 1.288 + // Send refresh event so all view are in sync. 1.289 + this._sendNeedsRefresh(); 1.290 + }, 1.291 + 1.292 + handleEvent: function bv_handleEvent(aEvent) { 1.293 + switch (aEvent.type){ 1.294 + case "MozAppbarDismissing": 1.295 + // If undo wasn't pressed, time to do definitive actions. 1.296 + if (this._toRemove) { 1.297 + for (let bookmarkId of this._toRemove) { 1.298 + this._bookmarkService.removeItem(bookmarkId); 1.299 + } 1.300 + this._toRemove = null; 1.301 + } 1.302 + break; 1.303 + 1.304 + case "BookmarksNeedsRefresh": 1.305 + this.getBookmarks(true); 1.306 + break; 1.307 + 1.308 + case "TabClose": 1.309 + // Flush any pending actions - appbar will call us back 1.310 + // before this returns with 'MozAppbarDismissing' above. 1.311 + StartUI.chromeWin.ContextUI.dismissContextAppbar(); 1.312 + break; 1.313 + } 1.314 + } 1.315 +}); 1.316 + 1.317 +let BookmarksStartView = { 1.318 + _view: null, 1.319 + get _grid() { return document.getElementById("start-bookmarks-grid"); }, 1.320 + 1.321 + init: function init() { 1.322 + this._view = new BookmarksView(this._grid, Bookmarks.metroRoot, true); 1.323 + this._view.getBookmarks(); 1.324 + this._grid.removeAttribute("fade"); 1.325 + }, 1.326 + 1.327 + uninit: function uninit() { 1.328 + if (this._view) { 1.329 + this._view.destruct(); 1.330 + } 1.331 + }, 1.332 +}; 1.333 + 1.334 +/** 1.335 + * Observes bookmark changes and keeps a linked BookmarksView updated. 1.336 + * 1.337 + * @param aView An instance of BookmarksView. 1.338 + */ 1.339 +function BookmarkChangeListener(aView) { 1.340 + this._view = aView; 1.341 +} 1.342 + 1.343 +BookmarkChangeListener.prototype = { 1.344 + ////////////////////////////////////////////////////////////////////////////// 1.345 + //// nsINavBookmarkObserver 1.346 + onBeginUpdateBatch: function () { }, 1.347 + onEndUpdateBatch: function () { }, 1.348 + 1.349 + onItemAdded: function bCL_onItemAdded(aItemId, aParentId, aIndex, aItemType, aURI, aTitle, aDateAdded, aGUID, aParentGUID) { 1.350 + this._view.getBookmarks(true); 1.351 + }, 1.352 + 1.353 + onItemChanged: function bCL_onItemChanged(aItemId, aProperty, aIsAnnotationProperty, aNewValue, aLastModified, aItemType, aParentId, aGUID, aParentGUID) { 1.354 + let itemIndex = PlacesUtils.bookmarks.getItemIndex(aItemId); 1.355 + if (!this._view.inCurrentView(aParentId, aItemId)) 1.356 + return; 1.357 + 1.358 + this._view.updateBookmark(aItemId); 1.359 + }, 1.360 + 1.361 + onItemMoved: function bCL_onItemMoved(aItemId, aOldParentId, aOldIndex, aNewParentId, aNewIndex, aItemType, aGUID, aOldParentGUID, aNewParentGUID) { 1.362 + let wasInView = this._view.inCurrentView(aOldParentId, aItemId); 1.363 + let nowInView = this._view.inCurrentView(aNewParentId, aItemId); 1.364 + 1.365 + if (!wasInView && nowInView) 1.366 + this._view.addBookmark(aItemId); 1.367 + 1.368 + if (wasInView && !nowInView) 1.369 + this._view.removeBookmark(aItemId); 1.370 + 1.371 + this._view.getBookmarks(true); 1.372 + }, 1.373 + 1.374 + onBeforeItemRemoved: function (aItemId, aItemType, aParentId, aGUID, aParentGUID) { }, 1.375 + onItemRemoved: function bCL_onItemRemoved(aItemId, aParentId, aIndex, aItemType, aURI, aGUID, aParentGUID) { 1.376 + if (!this._view.inCurrentView(aParentId, aItemId)) 1.377 + return; 1.378 + 1.379 + this._view.removeBookmark(aItemId); 1.380 + this._view.getBookmarks(true); 1.381 + }, 1.382 + 1.383 + onItemVisited: function(aItemId, aVisitId, aTime, aTransitionType, aURI, aParentId, aGUID, aParentGUID) { }, 1.384 + 1.385 + ////////////////////////////////////////////////////////////////////////////// 1.386 + //// nsISupports 1.387 + QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarkObserver]) 1.388 +};