diff options
Diffstat (limited to 'browser/base/content/newtab/sites.js')
-rw-r--r-- | browser/base/content/newtab/sites.js | 440 |
1 files changed, 440 insertions, 0 deletions
diff --git a/browser/base/content/newtab/sites.js b/browser/base/content/newtab/sites.js new file mode 100644 index 000000000..9d103ce9b --- /dev/null +++ b/browser/base/content/newtab/sites.js @@ -0,0 +1,440 @@ +#ifdef 0 +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +#endif + +const THUMBNAIL_PLACEHOLDER_ENABLED = + Services.prefs.getBoolPref("browser.newtabpage.thumbnailPlaceholder"); + +/** + * This class represents a site that is contained in a cell and can be pinned, + * moved around or deleted. + */ +function Site(aNode, aLink) { + this._node = aNode; + this._node._newtabSite = this; + + this._link = aLink; + + this._render(); + this._addEventHandlers(); +} + +Site.prototype = { + /** + * The site's DOM node. + */ + get node() { return this._node; }, + + /** + * The site's link. + */ + get link() { return this._link; }, + + /** + * The url of the site's link. + */ + get url() { return this.link.url; }, + + /** + * The title of the site's link. + */ + get title() { return this.link.title || this.link.url; }, + + /** + * The site's parent cell. + */ + get cell() { + let parentNode = this.node.parentNode; + return parentNode && parentNode._newtabCell; + }, + + /** + * Pins the site on its current or a given index. + * @param aIndex The pinned index (optional). + * @return true if link changed type after pin + */ + pin: function Site_pin(aIndex) { + if (typeof aIndex == "undefined") + aIndex = this.cell.index; + + this._updateAttributes(true); + let changed = gPinnedLinks.pin(this._link, aIndex); + if (changed) { + // render site again to remove suggested/sponsored tags + this._render(); + } + return changed; + }, + + /** + * Unpins the site and calls the given callback when done. + */ + unpin: function Site_unpin() { + if (this.isPinned()) { + this._updateAttributes(false); + gPinnedLinks.unpin(this._link); + gUpdater.updateGrid(); + } + }, + + /** + * Checks whether this site is pinned. + * @return Whether this site is pinned. + */ + isPinned: function Site_isPinned() { + return gPinnedLinks.isPinned(this._link); + }, + + /** + * Blocks the site (removes it from the grid) and calls the given callback + * when done. + */ + block: function Site_block() { + if (!gBlockedLinks.isBlocked(this._link)) { + gUndoDialog.show(this); + gBlockedLinks.block(this._link); + gUpdater.updateGrid(); + } + }, + + /** + * Gets the DOM node specified by the given query selector. + * @param aSelector The query selector. + * @return The DOM node we found. + */ + _querySelector: function Site_querySelector(aSelector) { + return this.node.querySelector(aSelector); + }, + + /** + * Updates attributes for all nodes which status depends on this site being + * pinned or unpinned. + * @param aPinned Whether this site is now pinned or unpinned. + */ + _updateAttributes: function (aPinned) { + let control = this._querySelector(".newtab-control-pin"); + + if (aPinned) { + this.node.setAttribute("pinned", true); + control.setAttribute("title", newTabString("unpin")); + } else { + this.node.removeAttribute("pinned"); + control.setAttribute("title", newTabString("pin")); + } + }, + + _newTabString: function(str, substrArr) { + let regExp = /%[0-9]\$S/g; + let matches; + while ((matches = regExp.exec(str))) { + let match = matches[0]; + let index = match.charAt(1); // Get the digit in the regExp. + str = str.replace(match, substrArr[index - 1]); + } + return str; + }, + + _getSuggestedTileExplanation: function() { + let targetedName = `<strong> ${this.link.targetedName} </strong>`; + let targetedSite = `<strong> ${this.link.targetedSite} </strong>`; + if (this.link.explanation) { + return this._newTabString(this.link.explanation, [targetedName, targetedSite]); + } + return newTabString("suggested.button", [targetedName]); + }, + + /** + * Checks for and modifies link at campaign end time + */ + _checkLinkEndTime: function Site_checkLinkEndTime() { + if (this.link.endTime && this.link.endTime < Date.now()) { + let oldUrl = this.url; + // chop off the path part from url + this.link.url = Services.io.newURI(this.url, null, null).resolve("/"); + // clear supplied images - this triggers thumbnail download for new url + delete this.link.imageURI; + delete this.link.enhancedImageURI; + // remove endTime to avoid further time checks + delete this.link.endTime; + // clear enhanced-content image that may still exist in preloaded page + this._querySelector(".enhanced-content").style.backgroundImage = ""; + gPinnedLinks.replace(oldUrl, this.link); + } + }, + + /** + * Renders the site's data (fills the HTML fragment). + */ + _render: function Site_render() { + // first check for end time, as it may modify the link + this._checkLinkEndTime(); + // setup display variables + let enhanced = gAllPages.enhanced && DirectoryLinksProvider.getEnhancedLink(this.link); + let url = this.url; + let title = enhanced && enhanced.title ? enhanced.title : + this.link.type == "history" ? this.link.baseDomain : + this.title; + let tooltip = (this.title == url ? this.title : this.title + "\n" + url); + + let link = this._querySelector(".newtab-link"); + link.setAttribute("title", tooltip); + link.setAttribute("href", url); + this.node.setAttribute("type", this.link.type); + + let titleNode = this._querySelector(".newtab-title"); + titleNode.textContent = title; + if (this.link.titleBgColor) { + titleNode.style.backgroundColor = this.link.titleBgColor; + } + + // remove "suggested" attribute to avoid showing "suggested" tag + // after site was pinned or dropped + this.node.removeAttribute("suggested"); + + if (this.link.targetedSite) { + if (this.node.getAttribute("type") != "sponsored") { + this._querySelector(".newtab-sponsored").textContent = + newTabString("suggested.tag"); + } + + this.node.setAttribute("suggested", true); + let explanation = this._getSuggestedTileExplanation(); + this._querySelector(".newtab-suggested").innerHTML = + `<div class='newtab-suggested-bounds'> ${explanation} </div>`; + } + + if (this.isPinned()) + this._updateAttributes(true); + // Capture the page if the thumbnail is missing, which will cause page.js + // to be notified and call our refreshThumbnail() method. + this.captureIfMissing(); + // but still display whatever thumbnail might be available now. + this.refreshThumbnail(); + }, + + /** + * Called when the site's tab becomes visible for the first time. + * Since the newtab may be preloaded long before it's displayed, + * check for changed conditions and re-render if needed + */ + onFirstVisible: function Site_onFirstVisible() { + if (this.link.endTime && this.link.endTime < Date.now()) { + // site needs to change landing url and background image + this._render(); + } + else { + this.captureIfMissing(); + } + }, + + /** + * Captures the site's thumbnail in the background, but only if there's no + * existing thumbnail and the page allows background captures. + */ + captureIfMissing: function Site_captureIfMissing() { + if (!document.hidden && !this.link.imageURI) { + BackgroundPageThumbs.captureIfMissing(this.url); + } + }, + + /** + * Refreshes the thumbnail for the site. + */ + refreshThumbnail: function Site_refreshThumbnail() { + // Only enhance tiles if that feature is turned on + let link = gAllPages.enhanced && DirectoryLinksProvider.getEnhancedLink(this.link) || + this.link; + + let thumbnail = this._querySelector(".newtab-thumbnail.thumbnail"); + if (link.bgColor) { + thumbnail.style.backgroundColor = link.bgColor; + } + let uri = link.imageURI || PageThumbs.getThumbnailURL(this.url); + thumbnail.style.backgroundImage = 'url("' + uri + '")'; + + if (THUMBNAIL_PLACEHOLDER_ENABLED && + link.type == "history" && + link.baseDomain) { + let placeholder = this._querySelector(".newtab-thumbnail.placeholder"); + let charCodeSum = 0; + for (let c of link.baseDomain) { + charCodeSum += c.charCodeAt(0); + } + const COLORS = 16; + let hue = Math.round((charCodeSum % COLORS) / COLORS * 360); + placeholder.style.backgroundColor = "hsl(" + hue + ",80%,40%)"; + placeholder.textContent = link.baseDomain.substr(0,1).toUpperCase(); + } + + if (link.enhancedImageURI) { + let enhanced = this._querySelector(".enhanced-content"); + enhanced.style.backgroundImage = 'url("' + link.enhancedImageURI + '")'; + + if (this.link.type != link.type) { + this.node.setAttribute("type", "enhanced"); + this.enhancedId = link.directoryId; + } + } + }, + + _ignoreHoverEvents: function(element) { + element.addEventListener("mouseover", () => { + this.cell.node.setAttribute("ignorehover", "true"); + }); + element.addEventListener("mouseout", () => { + this.cell.node.removeAttribute("ignorehover"); + }); + }, + + /** + * Adds event handlers for the site and its buttons. + */ + _addEventHandlers: function Site_addEventHandlers() { + // Register drag-and-drop event handlers. + this._node.addEventListener("dragstart", this, false); + this._node.addEventListener("dragend", this, false); + this._node.addEventListener("mouseover", this, false); + + // Specially treat the sponsored icon & suggested explanation + // text to prevent regular hover effects + let sponsored = this._querySelector(".newtab-sponsored"); + let suggested = this._querySelector(".newtab-suggested"); + this._ignoreHoverEvents(sponsored); + this._ignoreHoverEvents(suggested); + }, + + /** + * Speculatively opens a connection to the current site. + */ + _speculativeConnect: function Site_speculativeConnect() { + let sc = Services.io.QueryInterface(Ci.nsISpeculativeConnect); + let uri = Services.io.newURI(this.url, null, null); + try { + // This can throw for certain internal URLs, when they wind up in + // about:newtab. Be sure not to propagate the error. + sc.speculativeConnect(uri, null); + } catch (e) {} + }, + + /** + * Record interaction with site using telemetry. + */ + _recordSiteClicked: function Site_recordSiteClicked(aIndex) { + if (Services.prefs.prefHasUserValue("browser.newtabpage.rows") || + Services.prefs.prefHasUserValue("browser.newtabpage.columns") || + aIndex > 8) { + // We only want to get indices for the default configuration, everything + // else goes in the same bucket. + aIndex = 9; + } + Services.telemetry.getHistogramById("NEWTAB_PAGE_SITE_CLICKED") + .add(aIndex); + }, + + _toggleLegalText: function(buttonClass, explanationTextClass) { + let button = this._querySelector(buttonClass); + if (button.hasAttribute("active")) { + let explain = this._querySelector(explanationTextClass); + explain.parentNode.removeChild(explain); + + button.removeAttribute("active"); + } + else { + let explain = document.createElementNS(HTML_NAMESPACE, "div"); + explain.className = explanationTextClass.slice(1); // Slice off the first character, '.' + this.node.appendChild(explain); + + let link = '<a href="' + TILES_EXPLAIN_LINK + '">' + + newTabString("learn.link") + "</a>"; + let type = (this.node.getAttribute("suggested") && this.node.getAttribute("type") == "affiliate") ? + "suggested" : this.node.getAttribute("type"); + let icon = '<input type="button" class="newtab-control newtab-' + + (type == "enhanced" ? "customize" : "control-block") + '"/>'; + explain.innerHTML = newTabString(type + (type == "sponsored" ? ".explain2" : ".explain"), [icon, link]); + + button.setAttribute("active", "true"); + } + }, + + /** + * Handles site click events. + */ + onClick: function Site_onClick(aEvent) { + let action; + let pinned = this.isPinned(); + let tileIndex = this.cell.index; + let {button, target} = aEvent; + + // Handle tile/thumbnail link click + if (target.classList.contains("newtab-link") || + target.parentElement.classList.contains("newtab-link")) { + // Record for primary and middle clicks + if (button == 0 || button == 1) { + this._recordSiteClicked(tileIndex); + action = "click"; + } + } + // Handle sponsored explanation link click + else if (target.parentElement.classList.contains("sponsored-explain")) { + action = "sponsored_link"; + } + else if (target.parentElement.classList.contains("suggested-explain")) { + action = "suggested_link"; + } + // Only handle primary clicks for the remaining targets + else if (button == 0) { + aEvent.preventDefault(); + if (target.classList.contains("newtab-control-block")) { + // Notify DirectoryLinksProvider of suggested tile block, this may + // affect if and how suggested tiles are recommended and needs to + // be reported before pages are updated inside block() call + if (this.link.targetedSite) { + DirectoryLinksProvider.handleSuggestedTileBlock(); + } + this.block(); + action = "block"; + } + else if (target.classList.contains("sponsored-explain") || + target.classList.contains("newtab-sponsored")) { + this._toggleLegalText(".newtab-sponsored", ".sponsored-explain"); + action = "sponsored"; + } + else if (pinned && target.classList.contains("newtab-control-pin")) { + this.unpin(); + action = "unpin"; + } + else if (!pinned && target.classList.contains("newtab-control-pin")) { + if (this.pin()) { + // suggested link has changed - update rest of the pages + gAllPages.update(gPage); + } + action = "pin"; + } + } + + // Report all link click actions + if (action) { + DirectoryLinksProvider.reportSitesAction(gGrid.sites, action, tileIndex); + } + }, + + /** + * Handles all site events. + */ + handleEvent: function Site_handleEvent(aEvent) { + switch (aEvent.type) { + case "mouseover": + this._node.removeEventListener("mouseover", this, false); + this._speculativeConnect(); + break; + case "dragstart": + gDrag.start(this, aEvent); + break; + case "dragend": + gDrag.end(this, aEvent); + break; + } + } +}; |