OLD | NEW |
1 /* Copyright 2015 The Chromium Authors. All rights reserved. | 1 /* Copyright 2015 The Chromium Authors. All rights reserved. |
2 * Use of this source code is governed by a BSD-style license that can be | 2 * Use of this source code is governed by a BSD-style license that can be |
3 * found in the LICENSE file. */ | 3 * found in the LICENSE file. */ |
4 | 4 |
5 // Single iframe for NTP tiles. | 5 // Single iframe for NTP tiles. |
6 (function() { | 6 (function() { |
7 'use strict'; | 7 'use strict'; |
8 | 8 |
9 | 9 |
10 /** | 10 /** |
11 * The different types of events that are logged from the NTP. This enum is | 11 * The different types of events that are logged from the NTP. This enum is |
12 * used to transfer information from the NTP JavaScript to the renderer and is | 12 * used to transfer information from the NTP JavaScript to the renderer and is |
13 * not used as a UMA enum histogram's logged value. | 13 * not used as a UMA enum histogram's logged value. |
14 * Note: Keep in sync with common/ntp_logging_events.h | 14 * Note: Keep in sync with common/ntp_logging_events.h |
15 * @enum {number} | 15 * @enum {number} |
16 * @const | 16 * @const |
17 */ | 17 */ |
18 var LOG_TYPE = { | 18 var LOG_TYPE = { |
19 // All NTP Tiles have finished loading (successfully or failing). | 19 // All NTP Tiles have finished loading (successfully or failing). |
20 NTP_ALL_TILES_LOADED: 11, | 20 NTP_ALL_TILES_LOADED: 11, |
21 }; | 21 }; |
22 | 22 |
23 | 23 |
24 /** | 24 /** |
25 * The different sources that an NTP tile can have. | 25 * The different sources that an NTP tile can have. |
26 * Note: Keep in sync with components/ntp_tiles/ntp_tile_source.h | 26 * Note: Keep in sync with components/ntp_tiles/ntp_tile_source.h |
27 * @enum {number} | 27 * @enum {number} |
28 * @const | 28 * @const |
29 */ | 29 */ |
30 var NTPTileSource = { | 30 var NTPTileSource = { |
31 TOP_SITES: 0, | 31 TOP_SITES: 0, |
32 SUGGESTIONS_SERVICE: 1, | 32 SUGGESTIONS_SERVICE: 1, |
33 POPULAR: 3, | 33 POPULAR: 3, |
34 WHITELIST: 4, | 34 WHITELIST: 4, |
35 }; | 35 }; |
36 | 36 |
37 | 37 |
38 /** | 38 /** |
39 * Total number of tiles to show at any time. If the host page doesn't send | 39 * Total number of tiles to show at any time. If the host page doesn't send |
40 * enough tiles, we fill them blank. | 40 * enough tiles, we fill them blank. |
41 * @const {number} | 41 * @const {number} |
42 */ | 42 */ |
43 var NUMBER_OF_TILES = 8; | 43 var NUMBER_OF_TILES = 8; |
44 | 44 |
45 | 45 |
46 /** | 46 /** |
47 * Whether to use icons instead of thumbnails. | 47 * Whether to use icons instead of thumbnails. |
48 * @type {boolean} | 48 * @type {boolean} |
49 */ | 49 */ |
50 var USE_ICONS = false; | 50 var USE_ICONS = false; |
51 | 51 |
52 | 52 |
53 /** | 53 /** |
54 * Number of lines to display in titles. | 54 * Number of lines to display in titles. |
55 * @type {number} | 55 * @type {number} |
56 */ | 56 */ |
57 var NUM_TITLE_LINES = 1; | 57 var NUM_TITLE_LINES = 1; |
58 | 58 |
59 | 59 |
60 /** | 60 /** |
61 * The origin of this request. | 61 * The origin of this request. |
62 * @const {string} | 62 * @const {string} |
63 */ | 63 */ |
64 var DOMAIN_ORIGIN = '{{ORIGIN}}'; | 64 var DOMAIN_ORIGIN = '{{ORIGIN}}'; |
65 | 65 |
66 | 66 |
67 /** | 67 /** |
68 * Counter for DOM elements that we are waiting to finish loading. | 68 * Counter for DOM elements that we are waiting to finish loading. |
69 * @type {number} | 69 * @type {number} |
70 */ | 70 */ |
71 var loadedCounter = 1; | 71 var loadedCounter = 1; |
72 | 72 |
73 | 73 |
74 /** | 74 /** |
75 * DOM element containing the tiles we are going to present next. | 75 * DOM element containing the tiles we are going to present next. |
76 * Works as a double-buffer that is shown when we receive a "show" postMessage. | 76 * Works as a double-buffer that is shown when we receive a "show" |
77 * @type {Element} | 77 * postMessage. |
78 */ | 78 * @type {Element} |
79 var tiles = null; | 79 */ |
80 | 80 var tiles = null; |
81 | 81 |
82 /** | 82 |
83 * List of parameters passed by query args. | 83 /** |
84 * @type {Object} | 84 * List of parameters passed by query args. |
85 */ | 85 * @type {Object} |
86 var queryArgs = {}; | 86 */ |
87 | 87 var queryArgs = {}; |
88 | 88 |
89 /** | 89 |
90 * Log an event on the NTP. | 90 /** |
91 * @param {number} eventType Event from LOG_TYPE. | 91 * Log an event on the NTP. |
92 */ | 92 * @param {number} eventType Event from LOG_TYPE. |
93 var logEvent = function(eventType) { | 93 */ |
94 chrome.embeddedSearch.newTabPage.logEvent(eventType); | 94 var logEvent = function(eventType) { |
95 }; | 95 chrome.embeddedSearch.newTabPage.logEvent(eventType); |
96 | 96 }; |
97 /** | 97 |
98 * Log impression of an NTP tile. | 98 /** |
99 * @param {number} tileIndex Position of the tile, >= 0 and < NUMBER_OF_TILES. | 99 * Log impression of an NTP tile. |
100 * @param {number} tileSource The source from NTPTileSource. | 100 * @param {number} tileIndex Position of the tile, >= 0 and < NUMBER_OF_TILES. |
101 */ | 101 * @param {number} tileSource The source from NTPTileSource. |
102 function logMostVisitedImpression(tileIndex, tileSource) { | 102 */ |
103 chrome.embeddedSearch.newTabPage.logMostVisitedImpression(tileIndex, | 103 function logMostVisitedImpression(tileIndex, tileSource) { |
104 tileSource); | 104 chrome.embeddedSearch.newTabPage.logMostVisitedImpression( |
105 } | 105 tileIndex, tileSource); |
106 | |
107 /** | |
108 * Log click on an NTP tile. | |
109 * @param {number} tileIndex Position of the tile, >= 0 and < NUMBER_OF_TILES. | |
110 * @param {number} tileSource The source from NTPTileSource. | |
111 */ | |
112 function logMostVisitedNavigation(tileIndex, tileSource) { | |
113 chrome.embeddedSearch.newTabPage.logMostVisitedNavigation(tileIndex, | |
114 tileSource); | |
115 } | |
116 | |
117 /** | |
118 * Down counts the DOM elements that we are waiting for the page to load. | |
119 * When we get to 0, we send a message to the parent window. | |
120 * This is usually used as an EventListener of onload/onerror. | |
121 */ | |
122 var countLoad = function() { | |
123 loadedCounter -= 1; | |
124 if (loadedCounter <= 0) { | |
125 showTiles(); | |
126 logEvent(LOG_TYPE.NTP_ALL_TILES_LOADED); | |
127 window.parent.postMessage({cmd: 'loaded'}, DOMAIN_ORIGIN); | |
128 loadedCounter = 1; | |
129 } | 106 } |
130 }; | 107 |
131 | 108 /** |
132 | 109 * Log click on an NTP tile. |
133 /** | 110 * @param {number} tileIndex Position of the tile, >= 0 and < NUMBER_OF_TILES. |
134 * Handles postMessages coming from the host page to the iframe. | 111 * @param {number} tileSource The source from NTPTileSource. |
135 * Mostly, it dispatches every command to handleCommand. | 112 */ |
136 */ | 113 function logMostVisitedNavigation(tileIndex, tileSource) { |
137 var handlePostMessage = function(event) { | 114 chrome.embeddedSearch.newTabPage.logMostVisitedNavigation( |
138 if (event.data instanceof Array) { | 115 tileIndex, tileSource); |
139 for (var i = 0; i < event.data.length; ++i) { | |
140 handleCommand(event.data[i]); | |
141 } | |
142 } else { | |
143 handleCommand(event.data); | |
144 } | 116 } |
145 }; | 117 |
146 | 118 /** |
147 | 119 * Down counts the DOM elements that we are waiting for the page to load. |
148 /** | 120 * When we get to 0, we send a message to the parent window. |
149 * Handles a single command coming from the host page to the iframe. | 121 * This is usually used as an EventListener of onload/onerror. |
150 * We try to keep the logic here to a minimum and just dispatch to the relevant | 122 */ |
151 * functions. | 123 var countLoad = function() { |
152 */ | 124 loadedCounter -= 1; |
153 var handleCommand = function(data) { | 125 if (loadedCounter <= 0) { |
154 var cmd = data.cmd; | 126 showTiles(); |
155 | 127 logEvent(LOG_TYPE.NTP_ALL_TILES_LOADED); |
156 if (cmd == 'tile') { | 128 window.parent.postMessage({cmd: 'loaded'}, DOMAIN_ORIGIN); |
157 addTile(data); | 129 loadedCounter = 1; |
158 } else if (cmd == 'show') { | 130 } |
159 countLoad(); | 131 }; |
160 hideOverflowTiles(data); | 132 |
161 } else if (cmd == 'updateTheme') { | 133 |
162 updateTheme(data); | 134 /** |
163 } else if (cmd == 'tilesVisible') { | 135 * Handles postMessages coming from the host page to the iframe. |
164 hideOverflowTiles(data); | 136 * Mostly, it dispatches every command to handleCommand. |
165 } else { | 137 */ |
166 console.error('Unknown command: ' + JSON.stringify(data)); | 138 var handlePostMessage = function(event) { |
167 } | 139 if (event.data instanceof Array) { |
168 }; | 140 for (var i = 0; i < event.data.length; ++i) { |
169 | 141 handleCommand(event.data[i]); |
170 | 142 } |
171 var updateTheme = function(info) { | 143 } else { |
172 var themeStyle = []; | 144 handleCommand(event.data); |
173 | 145 } |
174 if (info.tileBorderColor) { | 146 }; |
175 themeStyle.push('.thumb-ntp .mv-tile {' + | 147 |
176 'border: 1px solid ' + info.tileBorderColor + '; }'); | 148 |
177 } | 149 /** |
178 if (info.tileHoverBorderColor) { | 150 * Handles a single command coming from the host page to the iframe. |
179 themeStyle.push('.thumb-ntp .mv-tile:hover {' + | 151 * We try to keep the logic here to a minimum and just dispatch to the |
180 'border-color: ' + info.tileHoverBorderColor + '; }'); | 152 * relevant |
181 } | 153 * functions. |
182 if (info.isThemeDark) { | 154 */ |
183 themeStyle.push('.thumb-ntp .mv-tile, .thumb-ntp .mv-empty-tile { ' + | 155 var handleCommand = function(data) { |
184 'background: rgb(51,51,51); }'); | 156 var cmd = data.cmd; |
185 themeStyle.push('.thumb-ntp .mv-thumb.failed-img { ' + | 157 |
186 'background-color: #555; }'); | 158 if (cmd == 'tile') { |
187 themeStyle.push('.thumb-ntp .mv-thumb.failed-img::after { ' + | 159 addTile(data); |
188 'border-color: #333; }'); | 160 } else if (cmd == 'show') { |
189 themeStyle.push('.thumb-ntp .mv-x { ' + | 161 countLoad(); |
190 'background: linear-gradient(to left, ' + | 162 hideOverflowTiles(data); |
191 'rgb(51,51,51) 60%, transparent); }'); | 163 } else if (cmd == 'updateTheme') { |
192 themeStyle.push('html[dir=rtl] .thumb-ntp .mv-x { ' + | 164 updateTheme(data); |
193 'background: linear-gradient(to right, ' + | 165 } else if (cmd == 'tilesVisible') { |
194 'rgb(51,51,51) 60%, transparent); }'); | 166 hideOverflowTiles(data); |
195 themeStyle.push('.thumb-ntp .mv-x::after { ' + | 167 } else { |
196 'background-color: rgba(255,255,255,0.7); }'); | 168 console.error('Unknown command: ' + JSON.stringify(data)); |
197 themeStyle.push('.thumb-ntp .mv-x:hover::after { ' + | 169 } |
198 'background-color: #fff; }'); | 170 }; |
199 themeStyle.push('.thumb-ntp .mv-x:active::after { ' + | 171 |
200 'background-color: rgba(255,255,255,0.5); }'); | 172 |
201 themeStyle.push('.icon-ntp .mv-tile:focus { ' + | 173 var updateTheme = function(info) { |
202 'background: rgba(255,255,255,0.2); }'); | 174 var themeStyle = []; |
203 } | 175 |
204 if (info.tileTitleColor) { | 176 if (info.tileBorderColor) { |
205 themeStyle.push('body { color: ' + info.tileTitleColor + '; }'); | 177 themeStyle.push( |
206 } | 178 '.thumb-ntp .mv-tile {' + |
207 | 179 'border: 1px solid ' + info.tileBorderColor + '; }'); |
208 document.querySelector('#custom-theme').textContent = themeStyle.join('\n'); | 180 } |
209 }; | 181 if (info.tileHoverBorderColor) { |
210 | 182 themeStyle.push( |
211 | 183 '.thumb-ntp .mv-tile:hover {' + |
212 /** | 184 'border-color: ' + info.tileHoverBorderColor + '; }'); |
213 * Hides extra tiles that don't fit on screen. | 185 } |
214 */ | 186 if (info.isThemeDark) { |
215 var hideOverflowTiles = function(data) { | 187 themeStyle.push( |
216 var tileAndEmptyTileList = document.querySelectorAll( | 188 '.thumb-ntp .mv-tile, .thumb-ntp .mv-empty-tile { ' + |
217 '#mv-tiles .mv-tile,#mv-tiles .mv-empty-tile'); | 189 'background: rgb(51,51,51); }'); |
218 for (var i = 0; i < tileAndEmptyTileList.length; ++i) { | 190 themeStyle.push( |
219 tileAndEmptyTileList[i].classList.toggle('hidden', i >= data.maxVisible); | 191 '.thumb-ntp .mv-thumb.failed-img { ' + |
220 } | 192 'background-color: #555; }'); |
221 }; | 193 themeStyle.push( |
222 | 194 '.thumb-ntp .mv-thumb.failed-img::after { ' + |
223 | 195 'border-color: #333; }'); |
224 /** | 196 themeStyle.push( |
225 * Removes all old instances of #mv-tiles that are pending for deletion. | 197 '.thumb-ntp .mv-x { ' + |
226 */ | 198 'background: linear-gradient(to left, ' + |
227 var removeAllOldTiles = function() { | 199 'rgb(51,51,51) 60%, transparent); }'); |
228 var parent = document.querySelector('#most-visited'); | 200 themeStyle.push( |
229 var oldList = parent.querySelectorAll('.mv-tiles-old'); | 201 'html[dir=rtl] .thumb-ntp .mv-x { ' + |
230 for (var i = 0; i < oldList.length; ++i) { | 202 'background: linear-gradient(to right, ' + |
231 parent.removeChild(oldList[i]); | 203 'rgb(51,51,51) 60%, transparent); }'); |
232 } | 204 themeStyle.push( |
233 }; | 205 '.thumb-ntp .mv-x::after { ' + |
234 | 206 'background-color: rgba(255,255,255,0.7); }'); |
235 | 207 themeStyle.push( |
236 /** | 208 '.thumb-ntp .mv-x:hover::after { ' + |
237 * Called when the host page has finished sending us tile information and | 209 'background-color: #fff; }'); |
238 * we are ready to show the new tiles and drop the old ones. | 210 themeStyle.push( |
239 */ | 211 '.thumb-ntp .mv-x:active::after { ' + |
240 var showTiles = function() { | 212 'background-color: rgba(255,255,255,0.5); }'); |
241 // Store the tiles on the current closure. | 213 themeStyle.push( |
242 var cur = tiles; | 214 '.icon-ntp .mv-tile:focus { ' + |
243 | 215 'background: rgba(255,255,255,0.2); }'); |
244 // Create empty tiles until we have NUMBER_OF_TILES. | 216 } |
245 while (cur.childNodes.length < NUMBER_OF_TILES) { | 217 if (info.tileTitleColor) { |
246 addTile({}); | 218 themeStyle.push('body { color: ' + info.tileTitleColor + '; }'); |
247 } | 219 } |
248 | 220 |
249 var parent = document.querySelector('#most-visited'); | 221 document.querySelector('#custom-theme').textContent = themeStyle.join('\n'); |
250 | 222 }; |
251 // Only fade in the new tiles if there were tiles before. | 223 |
252 var fadeIn = false; | 224 |
253 var old = parent.querySelector('#mv-tiles'); | 225 /** |
254 if (old) { | 226 * Hides extra tiles that don't fit on screen. |
255 fadeIn = true; | 227 */ |
256 // Mark old tile DIV for removal after the transition animation is done. | 228 var hideOverflowTiles = function(data) { |
257 old.removeAttribute('id'); | 229 var tileAndEmptyTileList = document.querySelectorAll( |
258 old.classList.add('mv-tiles-old'); | 230 '#mv-tiles .mv-tile,#mv-tiles .mv-empty-tile'); |
259 old.style.opacity = 0.0; | 231 for (var i = 0; i < tileAndEmptyTileList.length; ++i) { |
260 cur.addEventListener('webkitTransitionEnd', function(ev) { | 232 tileAndEmptyTileList[i].classList.toggle('hidden', i >= data.maxVisible); |
261 if (ev.target === cur) { | 233 } |
262 removeAllOldTiles(); | 234 }; |
| 235 |
| 236 |
| 237 /** |
| 238 * Removes all old instances of #mv-tiles that are pending for deletion. |
| 239 */ |
| 240 var removeAllOldTiles = function() { |
| 241 var parent = document.querySelector('#most-visited'); |
| 242 var oldList = parent.querySelectorAll('.mv-tiles-old'); |
| 243 for (var i = 0; i < oldList.length; ++i) { |
| 244 parent.removeChild(oldList[i]); |
| 245 } |
| 246 }; |
| 247 |
| 248 |
| 249 /** |
| 250 * Called when the host page has finished sending us tile information and |
| 251 * we are ready to show the new tiles and drop the old ones. |
| 252 */ |
| 253 var showTiles = function() { |
| 254 // Store the tiles on the current closure. |
| 255 var cur = tiles; |
| 256 |
| 257 // Create empty tiles until we have NUMBER_OF_TILES. |
| 258 while (cur.childNodes.length < NUMBER_OF_TILES) { |
| 259 addTile({}); |
| 260 } |
| 261 |
| 262 var parent = document.querySelector('#most-visited'); |
| 263 |
| 264 // Only fade in the new tiles if there were tiles before. |
| 265 var fadeIn = false; |
| 266 var old = parent.querySelector('#mv-tiles'); |
| 267 if (old) { |
| 268 fadeIn = true; |
| 269 // Mark old tile DIV for removal after the transition animation is done. |
| 270 old.removeAttribute('id'); |
| 271 old.classList.add('mv-tiles-old'); |
| 272 old.style.opacity = 0.0; |
| 273 cur.addEventListener('webkitTransitionEnd', function(ev) { |
| 274 if (ev.target === cur) { |
| 275 removeAllOldTiles(); |
| 276 } |
| 277 }); |
| 278 } |
| 279 |
| 280 // Add new tileset. |
| 281 cur.id = 'mv-tiles'; |
| 282 parent.appendChild(cur); |
| 283 // getComputedStyle causes the initial style (opacity 0) to be applied, so |
| 284 // that when we then set it to 1, that triggers the CSS transition. |
| 285 if (fadeIn) { |
| 286 window.getComputedStyle(cur).opacity; |
| 287 } |
| 288 cur.style.opacity = 1.0; |
| 289 |
| 290 // Make sure the tiles variable contain the next tileset we may use. |
| 291 tiles = document.createElement('div'); |
| 292 }; |
| 293 |
| 294 |
| 295 /** |
| 296 * Called when the host page wants to add a suggestion tile. |
| 297 * For Most Visited, it grabs the data from Chrome and pass on. |
| 298 * For host page generated it just passes the data. |
| 299 * @param {object} args Data for the tile to be rendered. |
| 300 */ |
| 301 var addTile = function(args) { |
| 302 if (isFinite(args.rid)) { |
| 303 // If a valid number passed in |args.rid|: a local chrome suggestion. |
| 304 var data = |
| 305 chrome.embeddedSearch.newTabPage.getMostVisitedItemData(args.rid); |
| 306 if (!data) |
| 307 return; |
| 308 |
| 309 data.tid = data.rid; |
| 310 if (!data.faviconUrl) { |
| 311 data.faviconUrl = 'chrome-search://favicon/size/16@' + |
| 312 window.devicePixelRatio + 'x/' + data.renderViewId + '/' + data.tid; |
| 313 } |
| 314 tiles.appendChild(renderTile(data)); |
| 315 } else if (args.url) { |
| 316 // If a URL is passed: a server-side suggestion. |
| 317 args.tileSource = NTPTileSource.SUGGESTIONS_SERVICE; |
| 318 // check sanity of the arguments |
| 319 if (/^javascript:/i.test(args.url) || |
| 320 /^javascript:/i.test(args.thumbnailUrl)) |
| 321 return; |
| 322 tiles.appendChild(renderTile(args)); |
| 323 } else { // an empty tile |
| 324 tiles.appendChild(renderTile(null)); |
| 325 } |
| 326 }; |
| 327 |
| 328 /** |
| 329 * Called when the user decided to add a tile to the blacklist. |
| 330 * It sets of the animation for the blacklist and sends the blacklisted id |
| 331 * to the host page. |
| 332 * @param {Element} tile DOM node of the tile we want to remove. |
| 333 */ |
| 334 var blacklistTile = function(tile) { |
| 335 tile.classList.add('blacklisted'); |
| 336 tile.addEventListener('webkitTransitionEnd', function(ev) { |
| 337 if (ev.propertyName != 'width') |
| 338 return; |
| 339 |
| 340 window.parent.postMessage( |
| 341 {cmd: 'tileBlacklisted', tid: Number(tile.getAttribute('data-tid'))}, |
| 342 DOMAIN_ORIGIN); |
| 343 }); |
| 344 }; |
| 345 |
| 346 |
| 347 /** |
| 348 * Returns whether the given URL has a known, safe scheme. |
| 349 * @param {string} url URL to check. |
| 350 */ |
| 351 var isSchemeAllowed = function(url) { |
| 352 return url.startsWith('http://') || url.startsWith('https://') || |
| 353 url.startsWith('ftp://') || url.startsWith('file://') || |
| 354 url.startsWith('chrome-extension://'); |
| 355 }; |
| 356 |
| 357 |
| 358 /** |
| 359 * Renders a MostVisited tile to the DOM. |
| 360 * @param {object} data Object containing rid, url, title, favicon, thumbnail. |
| 361 * data is null if you want to construct an empty tile. |
| 362 */ |
| 363 var renderTile = function(data) { |
| 364 var tile = document.createElement('a'); |
| 365 |
| 366 if (data == null) { |
| 367 tile.className = 'mv-empty-tile'; |
| 368 return tile; |
| 369 } |
| 370 |
| 371 // The tile will be appended to tiles. |
| 372 var position = tiles.children.length; |
| 373 logMostVisitedImpression(position, data.tileSource); |
| 374 |
| 375 tile.className = 'mv-tile'; |
| 376 tile.setAttribute('data-tid', data.tid); |
| 377 var html = []; |
| 378 if (!USE_ICONS) { |
| 379 html.push('<div class="mv-favicon"></div>'); |
| 380 } |
| 381 html.push('<div class="mv-title"></div><div class="mv-thumb"></div>'); |
| 382 html.push('<div class="mv-x" role="button"></div>'); |
| 383 tile.innerHTML = html.join(''); |
| 384 tile.lastElementChild.title = queryArgs['removeTooltip'] || ''; |
| 385 |
| 386 if (isSchemeAllowed(data.url)) { |
| 387 tile.href = data.url; |
| 388 } |
| 389 tile.setAttribute('aria-label', data.title); |
| 390 tile.title = data.title; |
| 391 |
| 392 tile.addEventListener('click', function(ev) { |
| 393 logMostVisitedNavigation(position, data.tileSource); |
| 394 }); |
| 395 |
| 396 tile.addEventListener('keydown', function(event) { |
| 397 if (event.keyCode == 46 /* DELETE */ || |
| 398 event.keyCode == 8 /* BACKSPACE */) { |
| 399 event.preventDefault(); |
| 400 event.stopPropagation(); |
| 401 blacklistTile(this); |
| 402 } else if ( |
| 403 event.keyCode == 13 /* ENTER */ || event.keyCode == 32 /* SPACE */) { |
| 404 event.preventDefault(); |
| 405 this.click(); |
| 406 } else if (event.keyCode >= 37 && event.keyCode <= 40 /* ARROWS */) { |
| 407 // specify the direction of movement |
| 408 var inArrowDirection = function(origin, target) { |
| 409 return (event.keyCode == 37 /* LEFT */ && |
| 410 origin.offsetTop == target.offsetTop && |
| 411 origin.offsetLeft > target.offsetLeft) || |
| 412 (event.keyCode == 38 /* UP */ && |
| 413 origin.offsetTop > target.offsetTop && |
| 414 origin.offsetLeft == target.offsetLeft) || |
| 415 (event.keyCode == 39 /* RIGHT */ && |
| 416 origin.offsetTop == target.offsetTop && |
| 417 origin.offsetLeft < target.offsetLeft) || |
| 418 (event.keyCode == 40 /* DOWN */ && |
| 419 origin.offsetTop < target.offsetTop && |
| 420 origin.offsetLeft == target.offsetLeft); |
| 421 }; |
| 422 |
| 423 var nonEmptyTiles = document.querySelectorAll('#mv-tiles .mv-tile'); |
| 424 var nextTile = null; |
| 425 // Find the closest tile in the appropriate direction. |
| 426 for (var i = 0; i < nonEmptyTiles.length; i++) { |
| 427 if (inArrowDirection(this, nonEmptyTiles[i]) && |
| 428 (!nextTile || inArrowDirection(nonEmptyTiles[i], nextTile))) { |
| 429 nextTile = nonEmptyTiles[i]; |
| 430 } |
| 431 } |
| 432 if (nextTile) { |
| 433 nextTile.focus(); |
| 434 } |
263 } | 435 } |
264 }); | 436 }); |
265 } | 437 |
266 | 438 var title = tile.querySelector('.mv-title'); |
267 // Add new tileset. | 439 title.innerText = data.title; |
268 cur.id = 'mv-tiles'; | 440 title.style.direction = data.direction || 'ltr'; |
269 parent.appendChild(cur); | 441 if (NUM_TITLE_LINES > 1) { |
270 // getComputedStyle causes the initial style (opacity 0) to be applied, so | 442 title.classList.add('multiline'); |
271 // that when we then set it to 1, that triggers the CSS transition. | 443 } |
272 if (fadeIn) { | 444 |
273 window.getComputedStyle(cur).opacity; | 445 if (USE_ICONS) { |
274 } | 446 var thumb = tile.querySelector('.mv-thumb'); |
275 cur.style.opacity = 1.0; | 447 if (data.largeIconUrl) { |
276 | 448 var img = document.createElement('img'); |
277 // Make sure the tiles variable contain the next tileset we may use. | 449 img.title = data.title; |
278 tiles = document.createElement('div'); | 450 img.src = data.largeIconUrl; |
279 }; | 451 img.classList.add('large-icon'); |
280 | 452 loadedCounter += 1; |
281 | 453 img.addEventListener('load', countLoad); |
282 /** | 454 img.addEventListener('load', function(ev) { |
283 * Called when the host page wants to add a suggestion tile. | 455 thumb.classList.add('large-icon-outer'); |
284 * For Most Visited, it grabs the data from Chrome and pass on. | 456 }); |
285 * For host page generated it just passes the data. | 457 img.addEventListener('error', countLoad); |
286 * @param {object} args Data for the tile to be rendered. | 458 img.addEventListener('error', function(ev) { |
287 */ | 459 thumb.classList.add('failed-img'); |
288 var addTile = function(args) { | 460 thumb.removeChild(img); |
289 if (isFinite(args.rid)) { | 461 }); |
290 // If a valid number passed in |args.rid|: a local chrome suggestion. | 462 thumb.appendChild(img); |
291 var data = | 463 } else { |
292 chrome.embeddedSearch.newTabPage.getMostVisitedItemData(args.rid); | 464 thumb.classList.add('failed-img'); |
293 if (!data) | 465 } |
294 return; | 466 } else { // THUMBNAILS |
295 | 467 // We keep track of the outcome of loading possible thumbnails for this |
296 data.tid = data.rid; | 468 // tile. Possible values: |
297 if (!data.faviconUrl) { | 469 // - null: waiting for load/error |
298 data.faviconUrl = 'chrome-search://favicon/size/16@' + | 470 // - false: error |
299 window.devicePixelRatio + 'x/' + data.renderViewId + '/' + data.tid; | 471 // - a string: URL that loaded correctly. |
300 } | 472 // This is populated by acceptImage/rejectImage and loadBestImage |
301 tiles.appendChild(renderTile(data)); | 473 // decides the best one to load. |
302 } else if (args.url) { | 474 var results = []; |
303 // If a URL is passed: a server-side suggestion. | 475 var thumb = tile.querySelector('.mv-thumb'); |
304 args.tileSource = NTPTileSource.SUGGESTIONS_SERVICE; | 476 var img = document.createElement('img'); |
305 // check sanity of the arguments | 477 var loaded = false; |
306 if (/^javascript:/i.test(args.url) || | 478 |
307 /^javascript:/i.test(args.thumbnailUrl)) | 479 var loadBestImage = function() { |
308 return; | 480 if (loaded) { |
309 tiles.appendChild(renderTile(args)); | 481 return; |
310 } else { // an empty tile | 482 } |
311 tiles.appendChild(renderTile(null)); | 483 for (var i = 0; i < results.length; ++i) { |
312 } | 484 if (results[i] === null) { |
313 }; | 485 return; |
314 | 486 } |
315 /** | 487 if (results[i] != false) { |
316 * Called when the user decided to add a tile to the blacklist. | 488 img.src = results[i]; |
317 * It sets of the animation for the blacklist and sends the blacklisted id | 489 loaded = true; |
318 * to the host page. | 490 return; |
319 * @param {Element} tile DOM node of the tile we want to remove. | 491 } |
320 */ | 492 } |
321 var blacklistTile = function(tile) { | 493 thumb.classList.add('failed-img'); |
322 tile.classList.add('blacklisted'); | 494 thumb.removeChild(img); |
323 tile.addEventListener('webkitTransitionEnd', function(ev) { | 495 countLoad(); |
324 if (ev.propertyName != 'width') return; | |
325 | |
326 window.parent.postMessage({cmd: 'tileBlacklisted', | |
327 tid: Number(tile.getAttribute('data-tid'))}, | |
328 DOMAIN_ORIGIN); | |
329 }); | |
330 }; | |
331 | |
332 | |
333 /** | |
334 * Returns whether the given URL has a known, safe scheme. | |
335 * @param {string} url URL to check. | |
336 */ | |
337 var isSchemeAllowed = function(url) { | |
338 return url.startsWith('http://') || url.startsWith('https://') || | |
339 url.startsWith('ftp://') || url.startsWith('file://') || | |
340 url.startsWith('chrome-extension://'); | |
341 }; | |
342 | |
343 | |
344 /** | |
345 * Renders a MostVisited tile to the DOM. | |
346 * @param {object} data Object containing rid, url, title, favicon, thumbnail. | |
347 * data is null if you want to construct an empty tile. | |
348 */ | |
349 var renderTile = function(data) { | |
350 var tile = document.createElement('a'); | |
351 | |
352 if (data == null) { | |
353 tile.className = 'mv-empty-tile'; | |
354 return tile; | |
355 } | |
356 | |
357 // The tile will be appended to tiles. | |
358 var position = tiles.children.length; | |
359 logMostVisitedImpression(position, data.tileSource); | |
360 | |
361 tile.className = 'mv-tile'; | |
362 tile.setAttribute('data-tid', data.tid); | |
363 var html = []; | |
364 if (!USE_ICONS) { | |
365 html.push('<div class="mv-favicon"></div>'); | |
366 } | |
367 html.push('<div class="mv-title"></div><div class="mv-thumb"></div>'); | |
368 html.push('<div class="mv-x" role="button"></div>'); | |
369 tile.innerHTML = html.join(''); | |
370 tile.lastElementChild.title = queryArgs['removeTooltip'] || ''; | |
371 | |
372 if (isSchemeAllowed(data.url)) { | |
373 tile.href = data.url; | |
374 } | |
375 tile.setAttribute('aria-label', data.title); | |
376 tile.title = data.title; | |
377 | |
378 tile.addEventListener('click', function(ev) { | |
379 logMostVisitedNavigation(position, data.tileSource); | |
380 }); | |
381 | |
382 tile.addEventListener('keydown', function(event) { | |
383 if (event.keyCode == 46 /* DELETE */ || | |
384 event.keyCode == 8 /* BACKSPACE */) { | |
385 event.preventDefault(); | |
386 event.stopPropagation(); | |
387 blacklistTile(this); | |
388 } else if (event.keyCode == 13 /* ENTER */ || | |
389 event.keyCode == 32 /* SPACE */) { | |
390 event.preventDefault(); | |
391 this.click(); | |
392 } else if (event.keyCode >= 37 && event.keyCode <= 40 /* ARROWS */) { | |
393 // specify the direction of movement | |
394 var inArrowDirection = function(origin, target) { | |
395 return (event.keyCode == 37 /* LEFT */ && | |
396 origin.offsetTop == target.offsetTop && | |
397 origin.offsetLeft > target.offsetLeft) || | |
398 (event.keyCode == 38 /* UP */ && | |
399 origin.offsetTop > target.offsetTop && | |
400 origin.offsetLeft == target.offsetLeft) || | |
401 (event.keyCode == 39 /* RIGHT */ && | |
402 origin.offsetTop == target.offsetTop && | |
403 origin.offsetLeft < target.offsetLeft) || | |
404 (event.keyCode == 40 /* DOWN */ && | |
405 origin.offsetTop < target.offsetTop && | |
406 origin.offsetLeft == target.offsetLeft); | |
407 }; | 496 }; |
408 | 497 |
409 var nonEmptyTiles = document.querySelectorAll('#mv-tiles .mv-tile'); | 498 var acceptImage = function(idx, url) { |
410 var nextTile = null; | 499 return function(ev) { |
411 // Find the closest tile in the appropriate direction. | 500 results[idx] = url; |
412 for (var i = 0; i < nonEmptyTiles.length; i++) { | 501 loadBestImage(); |
413 if (inArrowDirection(this, nonEmptyTiles[i]) && | 502 }; |
414 (!nextTile || inArrowDirection(nonEmptyTiles[i], nextTile))) { | 503 }; |
415 nextTile = nonEmptyTiles[i]; | 504 |
416 } | 505 var rejectImage = function(idx) { |
417 } | 506 return function(ev) { |
418 if (nextTile) { | 507 results[idx] = false; |
419 nextTile.focus(); | 508 loadBestImage(); |
420 } | 509 }; |
421 } | 510 }; |
422 }); | 511 |
423 | |
424 var title = tile.querySelector('.mv-title'); | |
425 title.innerText = data.title; | |
426 title.style.direction = data.direction || 'ltr'; | |
427 if (NUM_TITLE_LINES > 1) { | |
428 title.classList.add('multiline'); | |
429 } | |
430 | |
431 if (USE_ICONS) { | |
432 var thumb = tile.querySelector('.mv-thumb'); | |
433 if (data.largeIconUrl) { | |
434 var img = document.createElement('img'); | |
435 img.title = data.title; | 512 img.title = data.title; |
436 img.src = data.largeIconUrl; | 513 img.classList.add('thumbnail'); |
437 img.classList.add('large-icon'); | |
438 loadedCounter += 1; | 514 loadedCounter += 1; |
439 img.addEventListener('load', countLoad); | 515 img.addEventListener('load', countLoad); |
440 img.addEventListener('load', function(ev) { | |
441 thumb.classList.add('large-icon-outer'); | |
442 }); | |
443 img.addEventListener('error', countLoad); | 516 img.addEventListener('error', countLoad); |
444 img.addEventListener('error', function(ev) { | 517 img.addEventListener('error', function(ev) { |
445 thumb.classList.add('failed-img'); | 518 thumb.classList.add('failed-img'); |
446 thumb.removeChild(img); | 519 thumb.removeChild(img); |
447 }); | 520 }); |
448 thumb.appendChild(img); | 521 thumb.appendChild(img); |
449 } else { | 522 |
450 thumb.classList.add('failed-img'); | 523 if (data.thumbnailUrl) { |
451 } | 524 img.src = data.thumbnailUrl; |
452 } else { // THUMBNAILS | 525 } else { |
453 // We keep track of the outcome of loading possible thumbnails for this | 526 // Get all thumbnailUrls for the tile. |
454 // tile. Possible values: | 527 // They are ordered from best one to be used to worst. |
455 // - null: waiting for load/error | 528 for (var i = 0; i < data.thumbnailUrls.length; ++i) { |
456 // - false: error | 529 results.push(null); |
457 // - a string: URL that loaded correctly. | |
458 // This is populated by acceptImage/rejectImage and loadBestImage | |
459 // decides the best one to load. | |
460 var results = []; | |
461 var thumb = tile.querySelector('.mv-thumb'); | |
462 var img = document.createElement('img'); | |
463 var loaded = false; | |
464 | |
465 var loadBestImage = function() { | |
466 if (loaded) { | |
467 return; | |
468 } | |
469 for (var i = 0; i < results.length; ++i) { | |
470 if (results[i] === null) { | |
471 return; | |
472 } | 530 } |
473 if (results[i] != false) { | 531 for (var i = 0; i < data.thumbnailUrls.length; ++i) { |
474 img.src = results[i]; | 532 if (data.thumbnailUrls[i]) { |
475 loaded = true; | 533 var image = new Image(); |
476 return; | 534 image.src = data.thumbnailUrls[i]; |
| 535 image.onload = acceptImage(i, data.thumbnailUrls[i]); |
| 536 image.onerror = rejectImage(i); |
| 537 } else { |
| 538 rejectImage(i)(null); |
| 539 } |
477 } | 540 } |
478 } | 541 } |
479 thumb.classList.add('failed-img'); | 542 |
480 thumb.removeChild(img); | 543 var favicon = tile.querySelector('.mv-favicon'); |
481 countLoad(); | 544 if (data.faviconUrl) { |
482 }; | 545 var fi = document.createElement('img'); |
483 | 546 fi.src = data.faviconUrl; |
484 var acceptImage = function(idx, url) { | 547 // Set the title to empty so screen readers won't say the image name. |
485 return function(ev) { | 548 fi.title = ''; |
486 results[idx] = url; | 549 loadedCounter += 1; |
487 loadBestImage(); | 550 fi.addEventListener('load', countLoad); |
488 }; | 551 fi.addEventListener('error', countLoad); |
489 }; | 552 fi.addEventListener('error', function(ev) { |
490 | 553 favicon.classList.add('failed-favicon'); |
491 var rejectImage = function(idx) { | 554 }); |
492 return function(ev) { | 555 favicon.appendChild(fi); |
493 results[idx] = false; | 556 } else { |
494 loadBestImage(); | 557 favicon.classList.add('failed-favicon'); |
495 }; | 558 } |
496 }; | 559 } |
497 | 560 |
498 img.title = data.title; | 561 var mvx = tile.querySelector('.mv-x'); |
499 img.classList.add('thumbnail'); | 562 mvx.addEventListener('click', function(ev) { |
500 loadedCounter += 1; | 563 removeAllOldTiles(); |
501 img.addEventListener('load', countLoad); | 564 blacklistTile(tile); |
502 img.addEventListener('error', countLoad); | 565 ev.preventDefault(); |
503 img.addEventListener('error', function(ev) { | 566 ev.stopPropagation(); |
504 thumb.classList.add('failed-img'); | |
505 thumb.removeChild(img); | |
506 }); | 567 }); |
507 thumb.appendChild(img); | 568 |
508 | 569 return tile; |
509 if (data.thumbnailUrl) { | 570 }; |
510 img.src = data.thumbnailUrl; | 571 |
511 } else { | 572 |
512 // Get all thumbnailUrls for the tile. | 573 /** |
513 // They are ordered from best one to be used to worst. | 574 * Do some initialization and parses the query arguments passed to the iframe. |
514 for (var i = 0; i < data.thumbnailUrls.length; ++i) { | 575 */ |
515 results.push(null); | 576 var init = function() { |
516 } | 577 // Creates a new DOM element to hold the tiles. |
517 for (var i = 0; i < data.thumbnailUrls.length; ++i) { | 578 tiles = document.createElement('div'); |
518 if (data.thumbnailUrls[i]) { | 579 |
519 var image = new Image(); | 580 // Parse query arguments. |
520 image.src = data.thumbnailUrls[i]; | 581 var query = window.location.search.substring(1).split('&'); |
521 image.onload = acceptImage(i, data.thumbnailUrls[i]); | 582 queryArgs = {}; |
522 image.onerror = rejectImage(i); | 583 for (var i = 0; i < query.length; ++i) { |
523 } else { | 584 var val = query[i].split('='); |
524 rejectImage(i)(null); | 585 if (val[0] == '') |
525 } | 586 continue; |
526 } | 587 queryArgs[decodeURIComponent(val[0])] = decodeURIComponent(val[1]); |
527 } | 588 } |
528 | 589 |
529 var favicon = tile.querySelector('.mv-favicon'); | 590 // Apply class for icon NTP, if specified. |
530 if (data.faviconUrl) { | 591 USE_ICONS = queryArgs['icons'] == '1'; |
531 var fi = document.createElement('img'); | 592 if ('ntl' in queryArgs) { |
532 fi.src = data.faviconUrl; | 593 var ntl = parseInt(queryArgs['ntl'], 10); |
533 // Set the title to empty so screen readers won't say the image name. | 594 if (isFinite(ntl)) |
534 fi.title = ''; | 595 NUM_TITLE_LINES = ntl; |
535 loadedCounter += 1; | 596 } |
536 fi.addEventListener('load', countLoad); | 597 |
537 fi.addEventListener('error', countLoad); | 598 // Duplicating NTP_DESIGN.mainClass. |
538 fi.addEventListener('error', function(ev) { | 599 document.querySelector('#most-visited') |
539 favicon.classList.add('failed-favicon'); | 600 .classList.add(USE_ICONS ? 'icon-ntp' : 'thumb-ntp'); |
540 }); | 601 |
541 favicon.appendChild(fi); | 602 // Enable RTL. |
542 } else { | 603 if (queryArgs['rtl'] == '1') { |
543 favicon.classList.add('failed-favicon'); | 604 var html = document.querySelector('html'); |
544 } | 605 html.dir = 'rtl'; |
545 } | 606 } |
546 | 607 |
547 var mvx = tile.querySelector('.mv-x'); | 608 window.addEventListener('message', handlePostMessage); |
548 mvx.addEventListener('click', function(ev) { | 609 }; |
549 removeAllOldTiles(); | 610 |
550 blacklistTile(tile); | 611 |
551 ev.preventDefault(); | 612 window.addEventListener('DOMContentLoaded', init); |
552 ev.stopPropagation(); | |
553 }); | |
554 | |
555 return tile; | |
556 }; | |
557 | |
558 | |
559 /** | |
560 * Do some initialization and parses the query arguments passed to the iframe. | |
561 */ | |
562 var init = function() { | |
563 // Creates a new DOM element to hold the tiles. | |
564 tiles = document.createElement('div'); | |
565 | |
566 // Parse query arguments. | |
567 var query = window.location.search.substring(1).split('&'); | |
568 queryArgs = {}; | |
569 for (var i = 0; i < query.length; ++i) { | |
570 var val = query[i].split('='); | |
571 if (val[0] == '') continue; | |
572 queryArgs[decodeURIComponent(val[0])] = decodeURIComponent(val[1]); | |
573 } | |
574 | |
575 // Apply class for icon NTP, if specified. | |
576 USE_ICONS = queryArgs['icons'] == '1'; | |
577 if ('ntl' in queryArgs) { | |
578 var ntl = parseInt(queryArgs['ntl'], 10); | |
579 if (isFinite(ntl)) | |
580 NUM_TITLE_LINES = ntl; | |
581 } | |
582 | |
583 // Duplicating NTP_DESIGN.mainClass. | |
584 document.querySelector('#most-visited').classList.add( | |
585 USE_ICONS ? 'icon-ntp' : 'thumb-ntp'); | |
586 | |
587 // Enable RTL. | |
588 if (queryArgs['rtl'] == '1') { | |
589 var html = document.querySelector('html'); | |
590 html.dir = 'rtl'; | |
591 } | |
592 | |
593 window.addEventListener('message', handlePostMessage); | |
594 }; | |
595 | |
596 | |
597 window.addEventListener('DOMContentLoaded', init); | |
598 })(); | 613 })(); |
OLD | NEW |