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 */
30if (!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 */
46chrome.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 */
354var 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 */
361function 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 */
375if (/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*/
425if (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*/
536if (!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