#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 = ` ${this.link.targetedName} `; let targetedSite = ` ${this.link.targetedSite} `; 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 = `
${explanation}
`; } 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 = '' + newTabString("learn.link") + ""; let type = (this.node.getAttribute("suggested") && this.node.getAttribute("type") == "affiliate") ? "suggested" : this.node.getAttribute("type"); let icon = ''; 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; } } };