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