michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: 'use strict'; michael@0: michael@0: function HistoryView(aSet, aFilterUnpinned) { michael@0: View.call(this, aSet); michael@0: michael@0: this._inBatch = 0; michael@0: michael@0: // View monitors this for maximum tile display counts michael@0: this.tilePrefName = "browser.display.startUI.history.maxresults"; michael@0: this.showing = this.maxTiles > 0; michael@0: michael@0: this._filterUnpinned = aFilterUnpinned; michael@0: this._historyService = PlacesUtils.history; michael@0: this._navHistoryService = gHistSvc; michael@0: michael@0: this._pinHelper = new ItemPinHelper("metro.history.unpinned"); michael@0: this._historyService.addObserver(this, false); michael@0: StartUI.chromeWin.addEventListener('MozAppbarDismissing', this, false); michael@0: StartUI.chromeWin.addEventListener('HistoryNeedsRefresh', this, false); michael@0: window.addEventListener("TabClose", this, true); michael@0: } michael@0: michael@0: HistoryView.prototype = Util.extend(Object.create(View.prototype), { michael@0: _set: null, michael@0: _toRemove: null, michael@0: michael@0: // For View's showing property michael@0: get vbox() { michael@0: return document.getElementById("start-history"); michael@0: }, michael@0: michael@0: destruct: function destruct() { michael@0: this._historyService.removeObserver(this); michael@0: if (StartUI.chromeWin) { michael@0: StartUI.chromeWin.removeEventListener('MozAppbarDismissing', this, false); michael@0: StartUI.chromeWin.removeEventListener('HistoryNeedsRefresh', this, false); michael@0: } michael@0: View.prototype.destruct.call(this); michael@0: }, michael@0: michael@0: refreshView: function () { michael@0: this.onClearHistory(); michael@0: this.populateGrid(); michael@0: }, michael@0: michael@0: handleItemClick: function tabview_handleItemClick(aItem) { michael@0: let url = aItem.getAttribute("value"); michael@0: StartUI.goToURI(url); michael@0: }, michael@0: michael@0: populateGrid: function populateGrid(aRefresh) { michael@0: this._inBatch++; // always batch up grid updates to avoid redundant arrangeItems calls michael@0: let query = this._navHistoryService.getNewQuery(); michael@0: let options = this._navHistoryService.getNewQueryOptions(); michael@0: options.excludeQueries = true; michael@0: options.queryType = options.QUERY_TYPE_HISTORY; michael@0: options.resultType = options.RESULTS_AS_URI; michael@0: options.sortingMode = options.SORT_BY_DATE_DESCENDING; michael@0: michael@0: let limit = this.maxTiles; michael@0: let result = this._navHistoryService.executeQuery(query, options); michael@0: let rootNode = result.root; michael@0: rootNode.containerOpen = true; michael@0: let childCount = rootNode.childCount; michael@0: michael@0: for (let i = 0, addedCount = 0; i < childCount && addedCount < limit; i++) { michael@0: let node = rootNode.getChild(i); michael@0: let uri = node.uri; michael@0: let title = (node.title && node.title.length) ? node.title : uri; michael@0: michael@0: // If item is marked for deletion, skip it. michael@0: if (this._toRemove && this._toRemove.indexOf(uri) !== -1) michael@0: continue; michael@0: michael@0: let items = this._set.getItemsByUrl(uri); michael@0: michael@0: // Item has been unpinned, skip if filterUnpinned set. michael@0: if (this._filterUnpinned && !this._pinHelper.isPinned(uri)) { michael@0: if (items.length > 0) michael@0: this.removeHistory(uri); michael@0: michael@0: continue; michael@0: } michael@0: michael@0: if (!aRefresh || items.length === 0) { michael@0: // If we're not refreshing or the item is not in the grid, add it. michael@0: this.addItemToSet(uri, title, node.icon, addedCount); michael@0: } else if (aRefresh && items.length > 0) { michael@0: // Update context action in case it changed in another view. michael@0: for (let item of items) { michael@0: this._setContextActions(item); michael@0: } michael@0: } michael@0: michael@0: addedCount++; michael@0: } michael@0: michael@0: // Remove extra items in case a refresh added more than the limit. michael@0: // This can happen when undoing a delete. michael@0: if (aRefresh) { michael@0: while (this._set.itemCount > limit) michael@0: this._set.removeItemAt(this._set.itemCount - 1); michael@0: } michael@0: michael@0: rootNode.containerOpen = false; michael@0: this._set.arrangeItems(); michael@0: if (this._inBatch > 0) michael@0: this._inBatch--; michael@0: }, michael@0: michael@0: addItemToSet: function addItemToSet(aURI, aTitle, aIcon, aPos) { michael@0: let item = this._set.insertItemAt(aPos || 0, aTitle, aURI, this._inBatch); michael@0: this._setContextActions(item); michael@0: this._updateFavicon(item, aURI); michael@0: }, michael@0: michael@0: _setContextActions: function bv__setContextActions(aItem) { michael@0: let uri = aItem.getAttribute("value"); michael@0: aItem.setAttribute("data-contextactions", "delete," + (this._pinHelper.isPinned(uri) ? "hide" : "pin")); michael@0: if ("refresh" in aItem) aItem.refresh(); michael@0: }, michael@0: michael@0: _sendNeedsRefresh: function bv__sendNeedsRefresh(){ michael@0: // Event sent when all views need to refresh. michael@0: let event = document.createEvent("Events"); michael@0: event.initEvent("HistoryNeedsRefresh", true, false); michael@0: window.dispatchEvent(event); michael@0: }, michael@0: michael@0: removeHistory: function (aUri) { michael@0: let items = this._set.getItemsByUrl(aUri); michael@0: for (let item of items) michael@0: this._set.removeItem(item, true); michael@0: if (!this._inBatch) michael@0: this._set.arrangeItems(); michael@0: }, michael@0: michael@0: doActionOnSelectedTiles: function bv_doActionOnSelectedTiles(aActionName, aEvent) { michael@0: let tileGroup = this._set; michael@0: let selectedTiles = tileGroup.selectedItems; michael@0: michael@0: // just arrange the grid once at the end of any action handling michael@0: this._inBatch = true; michael@0: michael@0: switch (aActionName){ michael@0: case "delete": michael@0: Array.forEach(selectedTiles, function(aNode) { michael@0: if (!this._toRemove) { michael@0: this._toRemove = []; michael@0: } michael@0: michael@0: let uri = aNode.getAttribute("value"); michael@0: michael@0: this._toRemove.push(uri); michael@0: this.removeHistory(uri); michael@0: }, this); michael@0: michael@0: // stop the appbar from dismissing michael@0: aEvent.preventDefault(); michael@0: michael@0: // at next tick, re-populate the context appbar. michael@0: setTimeout(function(){ michael@0: // fire a MozContextActionsChange event to update the context appbar michael@0: let event = document.createEvent("Events"); michael@0: // we need the restore button to show (the tile node will go away though) michael@0: event.actions = ["restore"]; michael@0: event.initEvent("MozContextActionsChange", true, false); michael@0: tileGroup.dispatchEvent(event); michael@0: }, 0); michael@0: break; michael@0: michael@0: case "restore": michael@0: // clear toRemove and let _sendNeedsRefresh update the items. michael@0: this._toRemove = null; michael@0: break; michael@0: michael@0: case "unpin": michael@0: Array.forEach(selectedTiles, function(aNode) { michael@0: let uri = aNode.getAttribute("value"); michael@0: michael@0: if (this._filterUnpinned) michael@0: this.removeHistory(uri); michael@0: michael@0: this._pinHelper.setUnpinned(uri); michael@0: }, this); michael@0: break; michael@0: michael@0: case "pin": michael@0: Array.forEach(selectedTiles, function(aNode) { michael@0: let uri = aNode.getAttribute("value"); michael@0: michael@0: this._pinHelper.setPinned(uri); michael@0: }, this); michael@0: break; michael@0: michael@0: default: michael@0: this._inBatch = false; michael@0: return; michael@0: } michael@0: michael@0: this._inBatch = false; michael@0: // Send refresh event so all view are in sync. michael@0: this._sendNeedsRefresh(); michael@0: }, michael@0: michael@0: handleEvent: function bv_handleEvent(aEvent) { michael@0: switch (aEvent.type){ michael@0: case "MozAppbarDismissing": michael@0: // If undo wasn't pressed, time to do definitive actions. michael@0: if (this._toRemove) { michael@0: for (let uri of this._toRemove) { michael@0: this._historyService.removePage(NetUtil.newURI(uri)); michael@0: } michael@0: michael@0: // Clear context app bar michael@0: let event = document.createEvent("Events"); michael@0: event.actions = []; michael@0: event.initEvent("MozContextActionsChange", true, false); michael@0: this._set.dispatchEvent(event); michael@0: michael@0: this._toRemove = null; michael@0: } michael@0: break; michael@0: michael@0: case "HistoryNeedsRefresh": michael@0: this.populateGrid(true); michael@0: break; michael@0: michael@0: case "TabClose": michael@0: // Flush any pending actions - appbar will call us back michael@0: // before this returns with 'MozAppbarDismissing' above. michael@0: StartUI.chromeWin.ContextUI.dismissContextAppbar(); michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: // nsINavHistoryObserver & helpers michael@0: michael@0: onBeginUpdateBatch: function() { michael@0: // Avoid heavy grid redraws while a batch is in process michael@0: this._inBatch++; michael@0: }, michael@0: michael@0: onEndUpdateBatch: function() { michael@0: this.populateGrid(true); michael@0: if (this._inBatch > 0) { michael@0: this._inBatch--; michael@0: this._set.arrangeItems(); michael@0: } michael@0: }, michael@0: michael@0: onVisit: function(aURI, aVisitID, aTime, aSessionID, michael@0: aReferringID, aTransitionType) { michael@0: if (!this._inBatch) { michael@0: this.populateGrid(true); michael@0: } michael@0: }, michael@0: michael@0: onTitleChanged: function(aURI, aPageTitle) { michael@0: let changedItems = this._set.getItemsByUrl(aURI.spec); michael@0: for (let item of changedItems) { michael@0: item.setAttribute("label", aPageTitle); michael@0: } michael@0: }, michael@0: michael@0: onDeleteURI: function(aURI) { michael@0: this.removeHistory(aURI.spec); michael@0: }, michael@0: michael@0: onClearHistory: function() { michael@0: if ('clearAll' in this._set) michael@0: this._set.clearAll(); michael@0: }, michael@0: michael@0: onPageChanged: function(aURI, aWhat, aValue) { michael@0: if (aWhat == Ci.nsINavHistoryObserver.ATTRIBUTE_FAVICON) { michael@0: let changedItems = this._set.getItemsByUrl(aURI.spec); michael@0: for (let item of changedItems) { michael@0: let currIcon = item.getAttribute("iconURI"); michael@0: if (currIcon != aValue) { michael@0: item.setAttribute("iconURI", aValue); michael@0: if ("refresh" in item) michael@0: item.refresh(); michael@0: } michael@0: } michael@0: } michael@0: }, michael@0: michael@0: onDeleteVisits: function (aURI, aVisitTime, aGUID, aReason, aTransitionType) { michael@0: if ((aReason == Ci.nsINavHistoryObserver.REASON_DELETED) && !this._inBatch) { michael@0: this.populateGrid(true); michael@0: } michael@0: }, michael@0: michael@0: QueryInterface: function(iid) { michael@0: if (iid.equals(Components.interfaces.nsINavHistoryObserver) || michael@0: iid.equals(Components.interfaces.nsISupports)) { michael@0: return this; michael@0: } michael@0: throw Cr.NS_ERROR_NO_INTERFACE; michael@0: } michael@0: }); michael@0: michael@0: let HistoryStartView = { michael@0: _view: null, michael@0: get _grid() { return document.getElementById("start-history-grid"); }, michael@0: michael@0: init: function init() { michael@0: this._view = new HistoryView(this._grid, true); michael@0: this._view.populateGrid(); michael@0: this._grid.removeAttribute("fade"); michael@0: }, michael@0: michael@0: uninit: function uninit() { michael@0: if (this._view) { michael@0: this._view.destruct(); michael@0: } michael@0: } michael@0: };