Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. | |
| 2 // Use of this source code is governed by a BSD-style license that can be | |
| 3 // found in the LICENSE file. | |
| 4 | |
| 5 /** | |
| 6 * @fileoverview NTP Standalone hack | |
| 7 * This file contains the code necessary to make the Touch NTP work | |
| 8 * as a stand-alone application (either in Chrome or mobile Safari). | |
| 9 * This is useful for rapid development and testing. | |
| 10 */ | |
| 11 | |
| 12 /** | |
| 13 * For Safari, create a dummy chrome object | |
| 14 */ | |
| 15 if (!window.chrome) { | |
| 16 var chrome = {}; | |
| 17 } | |
| 18 | |
| 19 /** | |
| 20 * A replacement chrome.send method that supplies static data for the | |
| 21 * key APIs used by the NTP. | |
| 22 * | |
| 23 * Note that the real chrome object also supplies data for most-viewed and | |
| 24 * recently-closed pages, but the tangent NTP doesn't use that data so we | |
| 25 * don't bother simulating it here. | |
| 26 * | |
| 27 * We create this object by applying an anonymous function so that we can have | |
| 28 * local variables (avoid polluting the global object) | |
| 29 */ | |
| 30 chrome.send = (function() { | |
| 31 | |
| 32 var apps = [{ | |
| 33 app_launch_index: 2, | |
| 34 description: 'The prickly puzzle game where popping balloons has ' + | |
| 35 'never been so much fun!', | |
| 36 icon_big: 'standalone/poppit-icon.png', | |
| 37 icon_small: 'standalone/poppit-favicon.png', | |
| 38 id: 'mcbkbpnkkkipelfledbfocopglifcfmi', | |
| 39 launch_container: 2, | |
| 40 launch_type: 1, | |
| 41 launch_url: 'http://poppit.pogo.com/hd/PoppitHD.html', | |
| 42 name: 'Poppit', | |
| 43 options_url: '' | |
| 44 }, | |
| 45 { | |
| 46 app_launch_index: 1, | |
| 47 description: 'Fast, searchable email with less spam.', | |
| 48 icon_big: 'standalone/gmail-icon.png', | |
| 49 icon_small: 'standalone/gmail-favicon.png', | |
| 50 id: 'pjkljhegncpnkpknbcohdijeoejaedia', | |
| 51 launch_container: 2, | |
| 52 launch_type: 1, | |
| 53 launch_url: 'https://mail.google.com/', | |
| 54 name: 'Gmail', | |
| 55 options_url: 'https://mail.google.com/mail/#settings' | |
| 56 }, | |
| 57 { | |
| 58 app_launch_index: 3, | |
| 59 description: 'Read over 3 million Google eBooks on the web.', | |
| 60 icon_big: 'standalone/googlebooks-icon.png', | |
| 61 icon_small: 'standalone/googlebooks-favicon.png', | |
| 62 id: 'mmimngoggfoobjdlefbcabngfnmieonb', | |
| 63 launch_container: 2, | |
| 64 launch_type: 1, | |
| 65 launch_url: 'http://books.google.com/ebooks?source=chrome-app', | |
| 66 name: 'Google Books', | |
| 67 options_url: '' | |
| 68 }, | |
| 69 { | |
| 70 app_launch_index: 4, | |
| 71 description: 'Find local business information, directions, and ' + | |
| 72 'street-level imagery around the world with Google Maps.', | |
| 73 icon_big: 'standalone/googlemaps-icon.png', | |
| 74 icon_small: 'standalone/googlemaps-favicon.png', | |
| 75 id: 'lneaknkopdijkpnocmklfnjbeapigfbh', | |
| 76 launch_container: 2, | |
| 77 launch_type: 1, | |
| 78 launch_url: 'http://maps.google.com/', | |
| 79 name: 'Google Maps', | |
| 80 options_url: '' | |
| 81 }, | |
| 82 { | |
| 83 app_launch_index: 5, | |
| 84 description: 'Create the longest path possible and challenge your ' + | |
| 85 'friends in the game of Entanglement.', | |
| 86 icon_big: 'standalone/entaglement-icon.png', | |
| 87 id: 'aciahcmjmecflokailenpkdchphgkefd', | |
| 88 launch_container: 2, | |
| 89 launch_type: 1, | |
| 90 launch_url: 'http://entanglement.gopherwoodstudios.com/', | |
| 91 name: 'Entanglement', | |
| 92 options_url: '' | |
| 93 }, | |
| 94 { | |
| 95 name: 'NYTimes', | |
| 96 app_launch_index: 6, | |
| 97 description: 'The New York Times App for the Chrome Web Store.', | |
| 98 icon_big: 'standalone/nytimes-icon.png', | |
| 99 id: 'ecmphppfkcfflgglcokcbdkofpfegoel', | |
| 100 launch_container: 2, | |
| 101 launch_type: 1, | |
| 102 launch_url: 'http://www.nytimes.com/chrome/', | |
| 103 options_url: '', | |
| 104 page_index: 2 | |
| 105 }, | |
| 106 { | |
| 107 app_launch_index: 7, | |
| 108 description: 'The world\'s most popular online video community.', | |
| 109 id: 'blpcfgokakmgnkcojhhkbfbldkacnbeo', | |
| 110 icon_big: 'standalone/youtube-icon.png', | |
| 111 launch_container: 2, | |
| 112 launch_type: 1, | |
| 113 launch_url: 'http://www.youtube.com/', | |
| 114 name: 'YouTube', | |
| 115 options_url: '', | |
| 116 name: 'YouTube', | |
| 117 page_index: 3 | |
| 118 }]; | |
| 119 | |
| 120 // For testing | |
| 121 apps = spamApps(apps); | |
| 122 | |
| 123 /** | |
| 124 * Invoke the getAppsCallback function with a snapshot of the current app | |
| 125 * database. | |
| 126 */ | |
| 127 function sendGetAppsCallback() | |
| 128 { | |
| 129 // We don't want to hand out our array directly because the NTP will | |
| 130 // assume it owns the array and is free to modify it. For now we make a | |
| 131 // one-level deep copy of the array (since cloning the whole thing is | |
| 132 // more work and unnecessary at the moment). | |
| 133 var appsData = { | |
| 134 showPromo: false, | |
| 135 showLauncher: true, | |
| 136 apps: apps.slice(0) | |
| 137 }; | |
| 138 getAppsCallback(appsData); | |
| 139 } | |
| 140 | |
| 141 /** | |
| 142 * To make testing real-world scenarios easier, this expands our list of | |
| 143 * apps by duplicating them a number of times | |
| 144 */ | |
| 145 function spamApps(apps) | |
| 146 { | |
| 147 // Create an object that extends another object | |
| 148 // This is an easy/efficient way to make slightly modified copies of our | |
| 149 // app objects without having to do a deep copy | |
| 150 function createObject(proto) { | |
| 151 /** @constructor */ | |
| 152 var F = function() {}; | |
| 153 F.prototype = proto; | |
| 154 return new F(); | |
| 155 } | |
| 156 | |
| 157 var newApps = []; | |
| 158 var pages = Math.floor(Math.random() * 8) + 1; | |
| 159 var idx = 1; | |
| 160 for (var p = 0; p < pages; p++) { | |
| 161 var count = Math.floor(Math.random() * 18) + 1; | |
| 162 for (var a = 0; a < count; a++) { | |
| 163 var i = Math.floor(Math.random() * apps.length); | |
| 164 var newApp = createObject(apps[i]); | |
| 165 newApp.page_index = p; | |
| 166 newApp.app_launch_index = idx; | |
| 167 // Uniqify the ID | |
| 168 newApp.id = apps[i].id + '-' + idx; | |
| 169 idx++; | |
| 170 newApps.push(newApp); | |
| 171 } | |
| 172 } | |
| 173 return newApps; | |
| 174 } | |
| 175 | |
| 176 /** | |
| 177 * Like Array.prototype.indexOf but calls a predicate to test for match | |
| 178 * | |
| 179 * @param {Array} array The array to search. | |
| 180 * @param {function(Object): boolean} predicate The function to invoke on | |
| 181 * each element. | |
| 182 * @return {number} First index at which predicate returned true, or -1. | |
| 183 */ | |
| 184 function indexOfPred(array, predicate) | |
| 185 { | |
| 186 for (var i = 0; i < array.length; i++) { | |
| 187 if (predicate(array[i])) { | |
| 188 return i; | |
| 189 } | |
| 190 } | |
| 191 return -1; | |
| 192 } | |
| 193 | |
| 194 /** | |
| 195 * Get index into apps of an application object | |
| 196 * Requires the specified app to be present | |
| 197 * | |
| 198 * @param {string} id The ID of the application to locate. | |
| 199 * @return {number} The index in apps for an object with the specified ID. | |
| 200 */ | |
| 201 function getAppIndex(id) | |
| 202 { | |
| 203 var i = indexOfPred(apps, function(e) { return e.id === id;}); | |
| 204 if (i == -1) { | |
| 205 alert('Error: got unexpected App ID'); | |
| 206 } | |
| 207 return i; | |
| 208 } | |
| 209 | |
| 210 /** | |
| 211 * Get an application object given the application ID | |
| 212 * Requires | |
| 213 * @param {string} id The application ID to search for. | |
| 214 * @return {Object} The corresponding application object. | |
| 215 */ | |
| 216 function getApp(id) | |
| 217 { | |
| 218 return apps[getAppIndex(id)]; | |
| 219 } | |
| 220 | |
| 221 /** | |
| 222 * Simlulate the launching of an application | |
| 223 * | |
| 224 * @param {string} id The ID of the application to launch. | |
| 225 */ | |
| 226 function launchApp(id) | |
| 227 { | |
| 228 // Note that we don't do anything with the icon location. | |
| 229 // That's used by Chrome only on Windows to animate the icon during | |
| 230 // launch. | |
| 231 var app = getApp(id); | |
| 232 switch (parseInt(app.launch_type, 10)) { | |
| 233 case 0: // pinned | |
| 234 case 1: // regular | |
| 235 | |
| 236 // Replace the current tab with the app. | |
| 237 // Pinned seems to omit the tab title, but I doubt it's | |
| 238 // possible for us to do that here | |
| 239 window.location = (app.launch_url); | |
| 240 break; | |
| 241 | |
| 242 case 2: // fullscreen | |
| 243 case 3: // window | |
| 244 // attempt to launch in a new window | |
| 245 window.close(); | |
| 246 window.open(app.launch_url, app.name, | |
| 247 'resizable=yes,scrollbars=yes,status=yes'); | |
| 248 break; | |
| 249 | |
| 250 default: | |
| 251 alert('Unexpected launch type: ' + app.launch_type); | |
| 252 } | |
| 253 } | |
| 254 | |
| 255 /** | |
| 256 * Simulate uninstall of an app | |
| 257 * @param {string} id The ID of the application to uninstall. | |
| 258 */ | |
| 259 function uninstallApp(id) { | |
| 260 var i = getAppIndex(id); | |
| 261 // This confirmation dialog doesn't look exactly the same as the | |
| 262 // standard NTP one, but it's close enough. | |
| 263 if (window.confirm('Uninstall \"' + apps[i].name + '\"?')) { | |
| 264 apps.splice(i, 1); | |
| 265 sendGetAppsCallback(); | |
| 266 } | |
| 267 } | |
| 268 | |
| 269 /** | |
| 270 * Update the app_launch_index of all apps | |
| 271 * @param {Array.<string>} appIds All app IDs in their desired order. | |
| 272 */ | |
| 273 function reorderApps(movedAppId, appIds) { | |
| 274 assert(apps.length == appIds.length, 'Expected all apps in reorderApps'); | |
| 275 | |
| 276 // Clear the launch indicies so we can easily verify no dups | |
| 277 apps.forEach(function(a) { | |
| 278 a.app_launch_index = -1; | |
| 279 }); | |
| 280 | |
| 281 for (var i = 0; i < appIds.length; i++) { | |
| 282 var a = getApp(appIds[i]); | |
| 283 assert(a.app_launch_index == -1, | |
| 284 'Found duplicate appId in reorderApps'); | |
| 285 a.app_launch_index = i; | |
| 286 } | |
| 287 sendGetAppsCallback(); | |
| 288 } | |
| 289 | |
| 290 /** | |
| 291 * Update the page number of an app | |
| 292 * @param {string} id The ID of the application to move. | |
| 293 * @param {number} page The page index to place the app. | |
| 294 */ | |
| 295 function setPageIndex(id, page) { | |
| 296 var app = getApp(id); | |
| 297 app.page_index = page; | |
| 298 } | |
| 299 | |
| 300 // The 'send' function | |
| 301 /** | |
| 302 * The chrome server communication entrypoint. | |
| 303 * | |
| 304 * @param {string} command Name of the command to send. | |
| 305 * @param {Array} args Array of command-specific arguments. | |
| 306 */ | |
| 307 return function(command, args) { | |
| 308 // Chrome API is async | |
| 309 window.setTimeout(function() { | |
| 310 switch (command) { | |
| 311 // called to populate the list of applications | |
| 312 case 'getApps': | |
| 313 sendGetAppsCallback(); | |
| 314 break; | |
| 315 | |
| 316 // Called when an app is launched | |
| 317 // Ignore additional arguments - they've been changing over time and | |
| 318 // we don't use them in our NTP anyway. | |
| 319 case 'launchApp': | |
| 320 launchApp(args[0]); | |
| 321 break; | |
| 322 | |
| 323 // Called when an app is uninstalled | |
| 324 case 'uninstallApp': | |
| 325 uninstallApp(args[0]); | |
| 326 break; | |
| 327 | |
| 328 // Called when an app is repositioned in the touch NTP | |
| 329 case 'reorderApps': | |
| 330 reorderApps(args[0], args[1]); | |
| 331 break; | |
| 332 | |
| 333 // Called when an app is moved to a different page | |
| 334 case 'setPageIndex': | |
| 335 setPageIndex(args[0], parseInt(args[1], 10)); | |
| 336 break; | |
| 337 | |
| 338 default: | |
| 339 throw new Error('Unexpected chrome command: ' + command); | |
| 340 break; | |
| 341 } | |
| 342 }, 0); | |
| 343 }; | |
| 344 })(); | |
| 345 | |
| 346 /* A static templateData with english resources */ | |
| 347 var templateData = { | |
| 348 title: 'Standalone New Tab', | |
| 349 web_store_title: 'Web Store', | |
| 350 web_store_url: 'https://chrome.google.com/webstore?hl=en-US' | |
| 351 }; | |
| 352 | |
| 353 /* Hook construction of chrome://theme URLs */ | |
| 354 function themeUrlMapper(resourceName) { | |
| 355 if (resourceName == 'IDR_WEBSTORE_ICON') { | |
| 356 return 'standalone/webstore_icon.png'; | |
| 357 } | |
| 358 return undefined; | |
| 359 } | |
| 360 | |
| 361 /* | |
| 362 * On iOS we need a hack to avoid spurious click events | |
| 363 * In particular, if the user delays briefly between first touching and starting | |
| 364 * to drag, when the user releases a click event will be generated. | |
| 365 * Note that this seems to happen regardless of whether we do preventDefault on | |
| 366 * touchmove events. | |
| 367 */ | |
| 368 if (/iPhone|iPod|iPad/.test(navigator.userAgent) && | |
| 369 !(/Chrome/.test(navigator.userAgent))) { | |
| 370 // We have a real iOS device (no a ChromeOS device pretending to be iOS) | |
| 371 (function() { | |
| 372 // True if a gesture is occuring that should cause clicks to be swallowed | |
| 373 var gestureActive = false; | |
| 374 | |
| 375 // The position a touch was last started | |
| 376 var lastTouchStartPosition; | |
| 377 | |
| 378 // Distance which a touch needs to move to be considered a drag | |
| 379 var DRAG_DISTANCE = 3; | |
| 380 | |
| 381 document.addEventListener('touchstart', function(event) { | |
| 382 lastTouchStartPosition = { | |
| 383 x: event.touches[0].clientX, | |
| 384 y: event.touches[0].clientY | |
| 385 }; | |
| 386 // A touchstart ALWAYS preceeds a click (valid or not), so cancel any | |
| 387 // outstanding gesture. Also, any multi-touch is a gesture that should | |
| 388 // prevent clicks. | |
| 389 gestureActive = event.touches.length > 1; | |
| 390 }, true); | |
| 391 | |
| 392 document.addEventListener('touchmove', function(event) { | |
| 393 // When we see a move, measure the distance from the last touchStart | |
| 394 // If this is a multi-touch then the work here is irrelevant | |
| 395 // (gestureActive is already true) | |
| 396 var t = event.touches[0]; | |
| 397 if (Math.abs(t.clientX - lastTouchStartPosition.x) > DRAG_DISTANCE || | |
| 398 Math.abs(t.clientY - lastTouchStartPosition.y) > DRAG_DISTANCE) { | |
| 399 gestureActive = true; | |
| 400 } | |
| 401 }, true); | |
| 402 | |
| 403 document.addEventListener('click', function(event) { | |
| 404 // If we got here without gestureActive being set then it means we had | |
| 405 // a touchStart without any real dragging before touchEnd - we can allow | |
| 406 // the click to proceed. | |
| 407 if (gestureActive) { | |
| 408 event.preventDefault(); | |
| 409 event.stopPropagation(); | |
| 410 } | |
| 411 }, true); | |
| 412 | |
| 413 })(); | |
| 414 } | |
| 415 | |
| 416 /* Hack to add Element.classList to older browsers that don't yet support it. | |
|
arv (Not doing code reviews)
2011/03/10 19:25:59
Why do we care about old browsers?
Rick Byers
2011/03/11 02:44:33
See the comment at the top of this file - which I'
| |
| 417 From https://developer.mozilla.org/en/DOM/element.classList. | |
| 418 */ | |
| 419 if (typeof Element !== 'undefined' && | |
| 420 !Element.prototype.hasOwnProperty('classList')) { | |
| 421 (function() { | |
| 422 var classListProp = 'classList', | |
| 423 protoProp = 'prototype', | |
| 424 elemCtrProto = Element[protoProp], | |
| 425 objCtr = Object, | |
| 426 strTrim = String[protoProp].trim || function() { | |
| 427 return this.replace(/^\s+|\s+$/g, ''); | |
| 428 }, | |
| 429 arrIndexOf = Array[protoProp].indexOf || function(item) { | |
| 430 for (var i = 0, len = this.length; i < len; i++) { | |
| 431 if (i in this && this[i] === item) { | |
| 432 return i; | |
| 433 } | |
| 434 } | |
| 435 return -1; | |
| 436 }, | |
| 437 // Vendors: please allow content code to instantiate DOMExceptions | |
| 438 /** @constructor */ | |
| 439 DOMEx = function(type, message) { | |
| 440 this.name = type; | |
| 441 this.code = DOMException[type]; | |
| 442 this.message = message; | |
| 443 }, | |
| 444 checkTokenAndGetIndex = function(classList, token) { | |
| 445 if (token === '') { | |
| 446 throw new DOMEx( | |
| 447 'SYNTAX_ERR', | |
| 448 'An invalid or illegal string was specified' | |
| 449 ); | |
| 450 } | |
| 451 if (/\s/.test(token)) { | |
| 452 throw new DOMEx( | |
| 453 'INVALID_CHARACTER_ERR', | |
| 454 'String contains an invalid character' | |
| 455 ); | |
| 456 } | |
| 457 return arrIndexOf.call(classList, token); | |
| 458 }, | |
| 459 /** @constructor | |
| 460 * @extends Array */ | |
| 461 ClassList = function(elem) { | |
| 462 var trimmedClasses = strTrim.call(elem.className), | |
| 463 classes = trimmedClasses ? trimmedClasses.split(/\s+/) : []; | |
| 464 | |
| 465 for (var i = 0, len = classes.length; i < len; i++) { | |
| 466 this.push(classes[i]); | |
| 467 } | |
| 468 this._updateClassName = function() { | |
| 469 elem.className = this.toString(); | |
| 470 }; | |
| 471 }, | |
| 472 classListProto = ClassList[protoProp] = [], | |
| 473 classListGetter = function() { | |
| 474 return new ClassList(this); | |
| 475 }; | |
| 476 | |
| 477 // Most DOMException implementations don't allow calling DOMException's | |
| 478 // toString() on non-DOMExceptions. Error's toString() is sufficient here. | |
| 479 DOMEx[protoProp] = Error[protoProp]; | |
| 480 classListProto.item = function(i) { | |
| 481 return this[i] || null; | |
| 482 }; | |
| 483 classListProto.contains = function(token) { | |
| 484 token += ''; | |
| 485 return checkTokenAndGetIndex(this, token) !== -1; | |
| 486 }; | |
| 487 classListProto.add = function(token) { | |
| 488 token += ''; | |
| 489 if (checkTokenAndGetIndex(this, token) === -1) { | |
| 490 this.push(token); | |
| 491 this._updateClassName(); | |
| 492 } | |
| 493 }; | |
| 494 classListProto.remove = function(token) { | |
| 495 token += ''; | |
| 496 var index = checkTokenAndGetIndex(this, token); | |
| 497 if (index !== -1) { | |
| 498 this.splice(index, 1); | |
| 499 this._updateClassName(); | |
| 500 } | |
| 501 }; | |
| 502 classListProto.toggle = function(token) { | |
| 503 token += ''; | |
| 504 if (checkTokenAndGetIndex(this, token) === -1) { | |
| 505 this.add(token); | |
| 506 } else { | |
| 507 this.remove(token); | |
| 508 } | |
| 509 }; | |
| 510 classListProto.toString = function() { | |
| 511 return this.join(' '); | |
| 512 }; | |
| 513 | |
| 514 if (objCtr.defineProperty) { | |
| 515 var classListDescriptor = { | |
| 516 get: classListGetter, | |
| 517 enumerable: true, | |
| 518 configurable: true | |
| 519 }; | |
| 520 objCtr.defineProperty(elemCtrProto, classListProp, classListDescriptor); | |
| 521 } else if (objCtr[protoProp].__defineGetter__) { | |
| 522 elemCtrProto.__defineGetter__(classListProp, classListGetter); | |
| 523 } | |
| 524 }()); | |
| 525 } | |
| 526 | |
| 527 /* Hack to add Function.bind to older browsers that don't yet support it. From: | |
|
arv (Not doing code reviews)
2011/03/10 19:25:59
Why do we care about old browsers?
Rick Byers
2011/03/11 02:44:33
See above.
arv (Not doing code reviews)
2011/03/11 20:08:46
Mobile Safari does not have classList or bind?
Rick Byers
2011/03/15 21:47:54
Right. I just double-checked that the recent iOS
| |
| 528 https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Function /bind | |
| 529 */ | |
| 530 if (!Function.prototype.bind) { | |
| 531 /** | |
| 532 * @param {Object} selfObj Specifies the object which |this| should | |
| 533 * point to when the function is run. If the value is null or undefined, | |
| 534 * it will default to the global object. | |
| 535 * @param {...*} var_args Additional arguments that are partially | |
| 536 * applied to the function. | |
| 537 * @return {!Function} A partially-applied form of the function bind() was | |
| 538 * invoked as a method of. | |
| 539 * @suppress {duplicate} | |
| 540 */ | |
| 541 Function.prototype.bind = function(selfObj, var_args) { | |
| 542 var slice = [].slice, | |
| 543 args = slice.call(arguments, 1), | |
| 544 self = this, | |
| 545 /** @constructor */ | |
| 546 nop = function() {}, | |
| 547 bound = function() { | |
| 548 return self.apply(this instanceof nop ? this : (selfObj || {}), | |
| 549 args.concat(slice.call(arguments))); | |
| 550 }; | |
| 551 nop.prototype = self.prototype; | |
| 552 bound.prototype = new nop(); | |
| 553 return bound; | |
| 554 }; | |
| 555 } | |
| 556 | |
| OLD | NEW |