sites.js 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. #ifdef 0
  2. /* This Source Code Form is subject to the terms of the Mozilla Public
  3. * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  4. * You can obtain one at http://mozilla.org/MPL/2.0/. */
  5. #endif
  6. const THUMBNAIL_PLACEHOLDER_ENABLED =
  7. Services.prefs.getBoolPref("browser.newtabpage.thumbnailPlaceholder");
  8. /**
  9. * This class represents a site that is contained in a cell and can be pinned,
  10. * moved around or deleted.
  11. */
  12. function Site(aNode, aLink) {
  13. this._node = aNode;
  14. this._node._newtabSite = this;
  15. this._link = aLink;
  16. this._render();
  17. this._addEventHandlers();
  18. }
  19. Site.prototype = {
  20. /**
  21. * The site's DOM node.
  22. */
  23. get node() { return this._node; },
  24. /**
  25. * The site's link.
  26. */
  27. get link() { return this._link; },
  28. /**
  29. * The url of the site's link.
  30. */
  31. get url() { return this.link.url; },
  32. /**
  33. * The title of the site's link.
  34. */
  35. get title() { return this.link.title || this.link.url; },
  36. /**
  37. * The site's parent cell.
  38. */
  39. get cell() {
  40. let parentNode = this.node.parentNode;
  41. return parentNode && parentNode._newtabCell;
  42. },
  43. /**
  44. * Pins the site on its current or a given index.
  45. * @param aIndex The pinned index (optional).
  46. * @return true if link changed type after pin
  47. */
  48. pin: function(aIndex) {
  49. if (typeof aIndex == "undefined")
  50. aIndex = this.cell.index;
  51. this._updateAttributes(true);
  52. let changed = gPinnedLinks.pin(this._link, aIndex);
  53. if (changed) {
  54. // render site again
  55. this._render();
  56. }
  57. return changed;
  58. },
  59. /**
  60. * Unpins the site and calls the given callback when done.
  61. */
  62. unpin: function() {
  63. if (this.isPinned()) {
  64. this._updateAttributes(false);
  65. gPinnedLinks.unpin(this._link);
  66. gUpdater.updateGrid();
  67. }
  68. },
  69. /**
  70. * Checks whether this site is pinned.
  71. * @return Whether this site is pinned.
  72. */
  73. isPinned: function() {
  74. return gPinnedLinks.isPinned(this._link);
  75. },
  76. /**
  77. * Blocks the site (removes it from the grid) and calls the given callback
  78. * when done.
  79. */
  80. block: function() {
  81. if (!gBlockedLinks.isBlocked(this._link)) {
  82. gUndoDialog.show(this);
  83. gBlockedLinks.block(this._link);
  84. gUpdater.updateGrid();
  85. }
  86. },
  87. /**
  88. * Gets the DOM node specified by the given query selector.
  89. * @param aSelector The query selector.
  90. * @return The DOM node we found.
  91. */
  92. _querySelector: function(aSelector) {
  93. return this.node.querySelector(aSelector);
  94. },
  95. /**
  96. * Updates attributes for all nodes which status depends on this site being
  97. * pinned or unpinned.
  98. * @param aPinned Whether this site is now pinned or unpinned.
  99. */
  100. _updateAttributes: function(aPinned) {
  101. let control = this._querySelector(".newtab-control-pin");
  102. if (aPinned) {
  103. this.node.setAttribute("pinned", true);
  104. control.setAttribute("title", newTabString("unpin"));
  105. } else {
  106. this.node.removeAttribute("pinned");
  107. control.setAttribute("title", newTabString("pin"));
  108. }
  109. },
  110. _newTabString: function(str, substrArr) {
  111. let regExp = /%[0-9]\$S/g;
  112. let matches;
  113. while ((matches = regExp.exec(str))) {
  114. let match = matches[0];
  115. let index = match.charAt(1); // Get the digit in the regExp.
  116. str = str.replace(match, substrArr[index - 1]);
  117. }
  118. return str;
  119. },
  120. /**
  121. * Checks for and modifies link at campaign end time
  122. */
  123. _checkLinkEndTime: function() {
  124. if (this.link.endTime && this.link.endTime < Date.now()) {
  125. let oldUrl = this.url;
  126. // chop off the path part from url
  127. this.link.url = Services.io.newURI(this.url, null, null).resolve("/");
  128. // clear supplied images - this triggers thumbnail download for new url
  129. delete this.link.imageURI;
  130. // remove endTime to avoid further time checks
  131. delete this.link.endTime;
  132. gPinnedLinks.replace(oldUrl, this.link);
  133. }
  134. },
  135. /**
  136. * Renders the site's data (fills the HTML fragment).
  137. */
  138. _render: function() {
  139. // first check for end time, as it may modify the link
  140. this._checkLinkEndTime();
  141. // setup display variables
  142. let url = this.url;
  143. let title = this.link.type == "history" ? this.link.baseDomain :
  144. this.title;
  145. let tooltip = (this.title == url ? this.title : this.title + "\n" + url);
  146. let link = this._querySelector(".newtab-link");
  147. link.setAttribute("title", tooltip);
  148. link.setAttribute("href", url);
  149. this.node.setAttribute("type", this.link.type);
  150. let titleNode = this._querySelector(".newtab-title");
  151. titleNode.textContent = title;
  152. if (this.link.titleBgColor) {
  153. titleNode.style.backgroundColor = this.link.titleBgColor;
  154. }
  155. if (this.isPinned())
  156. this._updateAttributes(true);
  157. // Capture the page if the thumbnail is missing, which will cause page.js
  158. // to be notified and call our refreshThumbnail() method.
  159. this.captureIfMissing();
  160. // but still display whatever thumbnail might be available now.
  161. this.refreshThumbnail();
  162. },
  163. /**
  164. * Called when the site's tab becomes visible for the first time.
  165. * Since the newtab may be preloaded long before it's displayed,
  166. * check for changed conditions and re-render if needed
  167. */
  168. onFirstVisible: function() {
  169. if (this.link.endTime && this.link.endTime < Date.now()) {
  170. // site needs to change landing url and background image
  171. this._render();
  172. }
  173. else {
  174. this.captureIfMissing();
  175. }
  176. },
  177. /**
  178. * Captures the site's thumbnail in the background, but only if there's no
  179. * existing thumbnail and the page allows background captures.
  180. */
  181. captureIfMissing: function() {
  182. if (!document.hidden && !this.link.imageURI) {
  183. BackgroundPageThumbs.captureIfMissing(this.url);
  184. }
  185. },
  186. /**
  187. * Refreshes the thumbnail for the site.
  188. */
  189. refreshThumbnail: function() {
  190. let link = this.link;
  191. let thumbnail = this._querySelector(".newtab-thumbnail.thumbnail");
  192. if (link.bgColor) {
  193. thumbnail.style.backgroundColor = link.bgColor;
  194. }
  195. let uri = link.imageURI || PageThumbs.getThumbnailURL(this.url);
  196. thumbnail.style.backgroundImage = 'url("' + uri + '")';
  197. if (THUMBNAIL_PLACEHOLDER_ENABLED &&
  198. link.type == "history" &&
  199. link.baseDomain) {
  200. let placeholder = this._querySelector(".newtab-thumbnail.placeholder");
  201. let charCodeSum = 0;
  202. for (let c of link.baseDomain) {
  203. charCodeSum += c.charCodeAt(0);
  204. }
  205. const COLORS = 16;
  206. let hue = Math.round((charCodeSum % COLORS) / COLORS * 360);
  207. placeholder.style.backgroundColor = "hsl(" + hue + ",80%,40%)";
  208. placeholder.textContent = link.baseDomain.substr(0,1).toUpperCase();
  209. }
  210. },
  211. _ignoreHoverEvents: function(element) {
  212. element.addEventListener("mouseover", () => {
  213. this.cell.node.setAttribute("ignorehover", "true");
  214. });
  215. element.addEventListener("mouseout", () => {
  216. this.cell.node.removeAttribute("ignorehover");
  217. });
  218. },
  219. /**
  220. * Adds event handlers for the site and its buttons.
  221. */
  222. _addEventHandlers: function() {
  223. // Register drag-and-drop event handlers.
  224. this._node.addEventListener("dragstart", this, false);
  225. this._node.addEventListener("dragend", this, false);
  226. this._node.addEventListener("mouseover", this, false);
  227. },
  228. /**
  229. * Speculatively opens a connection to the current site.
  230. */
  231. _speculativeConnect: function() {
  232. let sc = Services.io.QueryInterface(Ci.nsISpeculativeConnect);
  233. let uri = Services.io.newURI(this.url, null, null);
  234. try {
  235. // This can throw for certain internal URLs, when they wind up in
  236. // about:newtab. Be sure not to propagate the error.
  237. sc.speculativeConnect(uri, null);
  238. } catch (e) {}
  239. },
  240. /**
  241. * Record interaction with site using telemetry.
  242. */
  243. _recordSiteClicked: function(aIndex) {
  244. if (Services.prefs.prefHasUserValue("browser.newtabpage.rows") ||
  245. Services.prefs.prefHasUserValue("browser.newtabpage.columns") ||
  246. aIndex > 8) {
  247. // We only want to get indices for the default configuration, everything
  248. // else goes in the same bucket.
  249. aIndex = 9;
  250. }
  251. Services.telemetry.getHistogramById("NEWTAB_PAGE_SITE_CLICKED")
  252. .add(aIndex);
  253. },
  254. _toggleLegalText: function(buttonClass, explanationTextClass) {
  255. let button = this._querySelector(buttonClass);
  256. if (button.hasAttribute("active")) {
  257. let explain = this._querySelector(explanationTextClass);
  258. explain.parentNode.removeChild(explain);
  259. button.removeAttribute("active");
  260. }
  261. },
  262. /**
  263. * Handles site click events.
  264. */
  265. onClick: function(aEvent) {
  266. let action;
  267. let pinned = this.isPinned();
  268. let tileIndex = this.cell.index;
  269. let {button, target} = aEvent;
  270. // Handle tile/thumbnail link click
  271. if (target.classList.contains("newtab-link") ||
  272. target.parentElement.classList.contains("newtab-link")) {
  273. // Record for primary and middle clicks
  274. if (button == 0 || button == 1) {
  275. this._recordSiteClicked(tileIndex);
  276. action = "click";
  277. }
  278. }
  279. // Only handle primary clicks for the remaining targets
  280. else if (button == 0) {
  281. aEvent.preventDefault();
  282. if (target.classList.contains("newtab-control-block")) {
  283. this.block();
  284. action = "block";
  285. }
  286. else if (pinned && target.classList.contains("newtab-control-pin")) {
  287. this.unpin();
  288. action = "unpin";
  289. }
  290. else if (!pinned && target.classList.contains("newtab-control-pin")) {
  291. if (this.pin()) {
  292. // link has changed - update rest of the pages
  293. gAllPages.update(gPage);
  294. }
  295. action = "pin";
  296. }
  297. }
  298. },
  299. /**
  300. * Handles all site events.
  301. */
  302. handleEvent: function(aEvent) {
  303. switch (aEvent.type) {
  304. case "mouseover":
  305. this._node.removeEventListener("mouseover", this, false);
  306. this._speculativeConnect();
  307. break;
  308. case "dragstart":
  309. gDrag.start(this, aEvent);
  310. break;
  311. case "dragend":
  312. gDrag.end(this, aEvent);
  313. break;
  314. }
  315. }
  316. };