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