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: Cu.import("resource://gre/modules/PageThumbs.jsm"); michael@0: michael@0: function TopSitesView(aGrid) { michael@0: View.call(this, aGrid); michael@0: // View monitors this for maximum tile display counts michael@0: this.tilePrefName = "browser.display.startUI.topsites.maxresults"; michael@0: this.showing = this.maxTiles > 0 && !this.isFirstRun(); michael@0: michael@0: // clean up state when the appbar closes michael@0: StartUI.chromeWin.addEventListener('MozAppbarDismissing', this, false); michael@0: let history = Cc["@mozilla.org/browser/nav-history-service;1"]. michael@0: getService(Ci.nsINavHistoryService); michael@0: history.addObserver(this, false); michael@0: michael@0: Services.obs.addObserver(this, "Metro:RefreshTopsiteThumbnail", false); michael@0: michael@0: NewTabUtils.allPages.register(this); michael@0: TopSites.prepareCache().then(function(){ michael@0: this.populateGrid(); michael@0: }.bind(this)); michael@0: } michael@0: michael@0: TopSitesView.prototype = Util.extend(Object.create(View.prototype), { michael@0: _set:null, michael@0: // _lastSelectedSites used to temporarily store blocked/removed sites for undo/restore-ing michael@0: _lastSelectedSites: null, michael@0: // isUpdating used only for testing currently michael@0: isUpdating: false, michael@0: michael@0: // For View's showing property michael@0: get vbox() { michael@0: return document.getElementById("start-topsites"); michael@0: }, michael@0: michael@0: destruct: function destruct() { michael@0: Services.obs.removeObserver(this, "Metro:RefreshTopsiteThumbnail"); michael@0: NewTabUtils.allPages.unregister(this); michael@0: if (StartUI.chromeWin) { michael@0: StartUI.chromeWin.removeEventListener('MozAppbarDismissing', this, false); michael@0: } michael@0: View.prototype.destruct.call(this); 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: doActionOnSelectedTiles: function(aActionName, aEvent) { michael@0: let tileGroup = this._set; michael@0: let selectedTiles = tileGroup.selectedItems; michael@0: let sites = Array.map(selectedTiles, TopSites._linkFromNode); michael@0: let nextContextActions = new Set(); michael@0: michael@0: switch (aActionName){ michael@0: case "delete": michael@0: for (let aNode of selectedTiles) { michael@0: // add some class to transition element before deletion? michael@0: aNode.contextActions.delete('delete'); michael@0: // we need new context buttons to show (the tile node will go away though) michael@0: } michael@0: this._lastSelectedSites = (this._lastSelectedSites || []).concat(sites); michael@0: // stop the appbar from dismissing michael@0: aEvent.preventDefault(); michael@0: nextContextActions.add('restore'); michael@0: TopSites.hideSites(sites); michael@0: break; michael@0: case "restore": michael@0: // usually restore is an undo action, so we have to recreate the tiles and grid selection michael@0: if (this._lastSelectedSites) { michael@0: let selectedUrls = this._lastSelectedSites.map((site) => site.url); michael@0: // re-select the tiles once the tileGroup is done populating and arranging michael@0: tileGroup.addEventListener("arranged", function _onArranged(aEvent){ michael@0: for (let url of selectedUrls) { michael@0: let tileNode = tileGroup.querySelector("richgriditem[value='"+url+"']"); michael@0: if (tileNode) { michael@0: tileNode.setAttribute("selected", true); michael@0: } michael@0: } michael@0: tileGroup.removeEventListener("arranged", _onArranged, false); michael@0: // we can't just call selectItem n times on tileGroup as selecting means trigger the default action michael@0: // for seltype="single" grids. michael@0: // so we toggle the attributes and raise the selectionchange "manually" michael@0: let event = tileGroup.ownerDocument.createEvent("Events"); michael@0: event.initEvent("selectionchange", true, true); michael@0: tileGroup.dispatchEvent(event); michael@0: }, false); michael@0: michael@0: TopSites.restoreSites(this._lastSelectedSites); michael@0: // stop the appbar from dismissing, michael@0: // the selectionchange event will trigger re-population of the context appbar michael@0: aEvent.preventDefault(); michael@0: } michael@0: break; michael@0: case "pin": michael@0: let pinIndices = []; michael@0: Array.forEach(selectedTiles, function(aNode) { michael@0: pinIndices.push( Array.indexOf(aNode.control.items, aNode) ); michael@0: aNode.contextActions.delete('pin'); michael@0: aNode.contextActions.add('unpin'); michael@0: }); michael@0: TopSites.pinSites(sites, pinIndices); michael@0: break; michael@0: case "unpin": michael@0: Array.forEach(selectedTiles, function(aNode) { michael@0: aNode.contextActions.delete('unpin'); michael@0: aNode.contextActions.add('pin'); michael@0: }); michael@0: TopSites.unpinSites(sites); michael@0: break; michael@0: // default: no action michael@0: } michael@0: if (nextContextActions.size) { 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: event.actions = [...nextContextActions]; michael@0: event.initEvent("MozContextActionsChange", true, false); michael@0: tileGroup.dispatchEvent(event); michael@0: },0); michael@0: } michael@0: }, michael@0: michael@0: handleEvent: function(aEvent) { michael@0: switch (aEvent.type){ michael@0: case "MozAppbarDismissing": michael@0: // clean up when the context appbar is dismissed - we don't remember selections michael@0: this._lastSelectedSites = null; michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: update: function() { michael@0: // called by the NewTabUtils.allPages.update, notifying us of data-change in topsites michael@0: let grid = this._set, michael@0: dirtySites = TopSites.dirty(); michael@0: michael@0: if (dirtySites.size) { michael@0: // we can just do a partial update and refresh the node representing each dirty tile michael@0: for (let site of dirtySites) { michael@0: let tileNode = grid.querySelector("[value='"+site.url+"']"); michael@0: if (tileNode) { michael@0: this.updateTile(tileNode, new Site(site)); michael@0: } michael@0: } michael@0: } else { michael@0: // flush, recreate all michael@0: this.isUpdating = true; michael@0: // destroy and recreate all item nodes, skip calling arrangeItems michael@0: this.populateGrid(); michael@0: } michael@0: }, michael@0: michael@0: updateTile: function(aTileNode, aSite, aArrangeGrid) { michael@0: if (!(aSite && aSite.url)) { michael@0: throw new Error("Invalid Site object passed to TopSitesView updateTile"); michael@0: } michael@0: this._updateFavicon(aTileNode, Util.makeURI(aSite.url)); michael@0: michael@0: Task.spawn(function() { michael@0: let filepath = PageThumbsStorage.getFilePathForURL(aSite.url); michael@0: if (yield OS.File.exists(filepath)) { michael@0: aSite.backgroundImage = 'url("'+PageThumbs.getThumbnailURL(aSite.url)+'")'; michael@0: // use the setter when available to update the backgroundImage value michael@0: if ('backgroundImage' in aTileNode && michael@0: aTileNode.backgroundImage != aSite.backgroundImage) { michael@0: aTileNode.backgroundImage = aSite.backgroundImage; michael@0: } else { michael@0: // just update the attribute for when the node gets the binding applied michael@0: aTileNode.setAttribute("customImage", aSite.backgroundImage); michael@0: } michael@0: } michael@0: }); michael@0: michael@0: aSite.applyToTileNode(aTileNode); michael@0: if (aTileNode.refresh) { michael@0: aTileNode.refresh(); michael@0: } michael@0: if (aArrangeGrid) { michael@0: this._set.arrangeItems(); michael@0: } michael@0: }, michael@0: michael@0: populateGrid: function populateGrid() { michael@0: this.isUpdating = true; michael@0: michael@0: let sites = TopSites.getSites(); michael@0: michael@0: let tileset = this._set; michael@0: tileset.clearAll(true); michael@0: michael@0: if (!this.maxTiles) { michael@0: this.isUpdating = false; michael@0: return; michael@0: } else { michael@0: sites = sites.slice(0, this.maxTiles); michael@0: } michael@0: michael@0: for (let site of sites) { michael@0: let slot = tileset.nextSlot(); michael@0: this.updateTile(slot, site); michael@0: } michael@0: tileset.arrangeItems(); michael@0: this.isUpdating = false; michael@0: }, michael@0: michael@0: forceReloadOfThumbnail: function forceReloadOfThumbnail(url) { michael@0: let nodes = this._set.querySelectorAll('richgriditem[value="'+url+'"]'); michael@0: for (let item of nodes) { michael@0: if ("isBound" in item && item.isBound) { michael@0: item.refreshBackgroundImage(); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: isFirstRun: function isFirstRun() { michael@0: return Services.prefs.getBoolPref("browser.firstrun.show.localepicker"); michael@0: }, michael@0: michael@0: _adjustDOMforViewState: function _adjustDOMforViewState(aState) { michael@0: if (!this._set) michael@0: return; michael@0: if (!aState) michael@0: aState = this._set.getAttribute("viewstate"); michael@0: michael@0: View.prototype._adjustDOMforViewState.call(this, aState); michael@0: michael@0: // Don't show thumbnails in snapped view. michael@0: if (aState == "snapped") { michael@0: document.getElementById("start-topsites-grid").removeAttribute("tiletype"); michael@0: } else { michael@0: document.getElementById("start-topsites-grid").setAttribute("tiletype", "thumbnail"); michael@0: } michael@0: michael@0: // propogate tiletype changes down to tile children michael@0: let tileType = this._set.getAttribute("tiletype"); michael@0: for (let item of this._set.children) { michael@0: if (tileType) { michael@0: item.setAttribute("tiletype", tileType); michael@0: } else { michael@0: item.removeAttribute("tiletype"); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: refreshView: function () { michael@0: this.populateGrid(); michael@0: }, michael@0: michael@0: // nsIObservers michael@0: observe: function (aSubject, aTopic, aState) { michael@0: switch (aTopic) { michael@0: case "Metro:RefreshTopsiteThumbnail": michael@0: this.forceReloadOfThumbnail(aState); michael@0: break; michael@0: } michael@0: View.prototype.observe.call(this, aSubject, aTopic, aState); michael@0: this.showing = this.maxTiles > 0 && !this.isFirstRun(); michael@0: }, michael@0: michael@0: // nsINavHistoryObserver michael@0: onBeginUpdateBatch: function() { michael@0: }, michael@0: michael@0: onEndUpdateBatch: function() { michael@0: }, michael@0: michael@0: onVisit: function(aURI, aVisitID, aTime, aSessionID, michael@0: aReferringID, aTransitionType) { michael@0: }, michael@0: michael@0: onTitleChanged: function(aURI, aPageTitle) { michael@0: }, michael@0: michael@0: onDeleteURI: function(aURI) { 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: }, michael@0: michael@0: onDeleteVisits: function (aURI, aVisitTime, aGUID, aReason, aTransitionType) { 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: michael@0: let TopSitesStartView = { michael@0: _view: null, michael@0: get _grid() { return document.getElementById("start-topsites-grid"); }, michael@0: michael@0: init: function init() { michael@0: this._view = new TopSitesView(this._grid); 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: };