| 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 |