Javascript  |  563行  |  17.66 KB

// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 *  @fileoverview NTP Standalone hack
 *  This file contains the code necessary to make the Touch NTP work
 *  as a stand-alone application (as opposed to being embedded into chrome).
 *  This is useful for rapid development and testing, but does not actually form
 *  part of the product.
 *
 *  Note that, while the product portion of the touch NTP is designed to work
 *  just in the latest version of Chrome, this hack attempts to add some support
 *  for working in older browsers to enable testing and demonstration on
 *  existing tablet platforms.  In particular, this code has been tested to work
 *  on Mobile Safari in iOS 4.2.  The goal is that the need to support any other
 *  browser should not leak out of this file - and so we will hack global JS
 *  objects as necessary here to present the illusion of running on the latest
 *  version of Chrome.
 */

// Note that this file never gets concatenated and embeded into Chrome, so we
// can enable strict mode for the whole file just like normal.
'use strict';


/**
 * For non-Chrome browsers, create a dummy chrome object
 */
if (!window.chrome) {
  var chrome = {};
}


/**
 *  A replacement chrome.send method that supplies static data for the
 *  key APIs used by the NTP.
 *
 *  Note that the real chrome object also supplies data for most-viewed and
 *  recently-closed pages, but the tangent NTP doesn't use that data so we
 *  don't bother simulating it here.
 *
 *  We create this object by applying an anonymous function so that we can have
 *  local variables (avoid polluting the global object)
 */
chrome.send = (function() {
  var apps = [{
    app_launch_index: 2,
    description: 'The prickly puzzle game where popping balloons has ' +
        'never been so much fun!',
    icon_big: 'standalone/poppit-icon.png',
    icon_small: 'standalone/poppit-favicon.png',
    id: 'mcbkbpnkkkipelfledbfocopglifcfmi',
    launch_container: 2,
    launch_type: 1,
    launch_url: 'http://poppit.pogo.com/hd/PoppitHD.html',
    name: 'Poppit',
    options_url: ''
  },
  {
    app_launch_index: 1,
    description: 'Fast, searchable email with less spam.',
    icon_big: 'standalone/gmail-icon.png',
    icon_small: 'standalone/gmail-favicon.png',
    id: 'pjkljhegncpnkpknbcohdijeoejaedia',
    launch_container: 2,
    launch_type: 1,
    launch_url: 'https://mail.google.com/',
    name: 'Gmail',
    options_url: 'https://mail.google.com/mail/#settings'
  },
  {
    app_launch_index: 3,
    description: 'Read over 3 million Google eBooks on the web.',
    icon_big: 'standalone/googlebooks-icon.png',
    icon_small: 'standalone/googlebooks-favicon.png',
    id: 'mmimngoggfoobjdlefbcabngfnmieonb',
    launch_container: 2,
    launch_type: 1,
    launch_url: 'http://books.google.com/ebooks?source=chrome-app',
    name: 'Google Books',
    options_url: ''
  },
  {
    app_launch_index: 4,
    description: 'Find local business information, directions, and ' +
        'street-level imagery around the world with Google Maps.',
    icon_big: 'standalone/googlemaps-icon.png',
    icon_small: 'standalone/googlemaps-favicon.png',
    id: 'lneaknkopdijkpnocmklfnjbeapigfbh',
    launch_container: 2,
    launch_type: 1,
    launch_url: 'http://maps.google.com/',
    name: 'Google Maps',
    options_url: ''
  },
  {
    app_launch_index: 5,
    description: 'Create the longest path possible and challenge your ' +
        'friends in the game of Entanglement.',
    icon_big: 'standalone/entaglement-icon.png',
    id: 'aciahcmjmecflokailenpkdchphgkefd',
    launch_container: 2,
    launch_type: 1,
    launch_url: 'http://entanglement.gopherwoodstudios.com/',
    name: 'Entanglement',
    options_url: ''
  },
  {
    name: 'NYTimes',
    app_launch_index: 6,
    description: 'The New York Times App for the Chrome Web Store.',
    icon_big: 'standalone/nytimes-icon.png',
    id: 'ecmphppfkcfflgglcokcbdkofpfegoel',
    launch_container: 2,
    launch_type: 1,
    launch_url: 'http://www.nytimes.com/chrome/',
    options_url: '',
    page_index: 2
  },
  {
    app_launch_index: 7,
    description: 'The world\'s most popular online video community.',
    id: 'blpcfgokakmgnkcojhhkbfbldkacnbeo',
    icon_big: 'standalone/youtube-icon.png',
    launch_container: 2,
    launch_type: 1,
    launch_url: 'http://www.youtube.com/',
    name: 'YouTube',
    options_url: '',
    page_index: 3
  }];

  // For testing
  apps = spamApps(apps);

  /**
   * Invoke the getAppsCallback function with a snapshot of the current app
   * database.
   */
  function sendGetAppsCallback()
  {
    // We don't want to hand out our array directly because the NTP will
    // assume it owns the array and is free to modify it.  For now we make a
    // one-level deep copy of the array (since cloning the whole thing is
    // more work and unnecessary at the moment).
    var appsData = {
      showPromo: false,
      showLauncher: true,
      apps: apps.slice(0)
    };
    getAppsCallback(appsData);
  }

  /**
   * To make testing real-world scenarios easier, this expands our list of
   * apps by duplicating them a number of times
   */
  function spamApps(apps)
  {
    // Create an object that extends another object
    // This is an easy/efficient way to make slightly modified copies of our
    // app objects without having to do a deep copy
    function createObject(proto) {
      /** @constructor */
      var F = function() {};
      F.prototype = proto;
      return new F();
    }

    var newApps = [];
    var pages = Math.floor(Math.random() * 8) + 1;
    var idx = 1;
    for (var p = 0; p < pages; p++) {
      var count = Math.floor(Math.random() * 18) + 1;
      for (var a = 0; a < count; a++) {
        var i = Math.floor(Math.random() * apps.length);
        var newApp = createObject(apps[i]);
        newApp.page_index = p;
        newApp.app_launch_index = idx;
        // Uniqify the ID
        newApp.id = apps[i].id + '-' + idx;
        idx++;
        newApps.push(newApp);
      }
    }
    return newApps;
  }

  /**
   * Like Array.prototype.indexOf but calls a predicate to test for match
   *
   * @param {Array} array The array to search.
   * @param {function(Object): boolean} predicate The function to invoke on
   *     each element.
   * @return {number} First index at which predicate returned true, or -1.
   */
  function indexOfPred(array, predicate) {
    for (var i = 0; i < array.length; i++) {
      if (predicate(array[i]))
        return i;
    }
    return -1;
  }

  /**
   * Get index into apps of an application object
   * Requires the specified app to be present
   *
   * @param {string} id The ID of the application to locate.
   * @return {number} The index in apps for an object with the specified ID.
   */
  function getAppIndex(id) {
    var i = indexOfPred(apps, function(e) { return e.id === id;});
    if (i == -1)
      alert('Error: got unexpected App ID');
    return i;
  }

  /**
   * Get an application object given the application ID
   * Requires
   * @param {string} id The application ID to search for.
   * @return {Object} The corresponding application object.
   */
  function getApp(id) {
    return apps[getAppIndex(id)];
  }

  /**
   * Simlulate the launching of an application
   *
   * @param {string} id The ID of the application to launch.
   */
  function launchApp(id) {
    // Note that we don't do anything with the icon location.
    // That's used by Chrome only on Windows to animate the icon during
    // launch.
    var app = getApp(id);
    switch (parseInt(app.launch_type, 10)) {
      case 0: // pinned
      case 1: // regular
        // Replace the current tab with the app.
        // Pinned seems to omit the tab title, but I doubt it's
        // possible for us to do that here
        window.location = (app.launch_url);
        break;

      case 2: // fullscreen
      case 3: // window
        // attempt to launch in a new window
        window.close();
        window.open(app.launch_url, app.name,
            'resizable=yes,scrollbars=yes,status=yes');
        break;

      default:
        alert('Unexpected launch type: ' + app.launch_type);
    }
  }

  /**
   * Simulate uninstall of an app
   * @param {string} id The ID of the application to uninstall.
   */
  function uninstallApp(id) {
    var i = getAppIndex(id);
    // This confirmation dialog doesn't look exactly the same as the
    // standard NTP one, but it's close enough.
    if (window.confirm('Uninstall \"' + apps[i].name + '\"?')) {
      apps.splice(i, 1);
      sendGetAppsCallback();
    }
  }

  /**
   * Update the app_launch_index of all apps
   * @param {Array.<string>} appIds All app IDs in their desired order.
   */
  function reorderApps(movedAppId, appIds) {
    assert(apps.length == appIds.length, 'Expected all apps in reorderApps');

    // Clear the launch indicies so we can easily verify no dups
    apps.forEach(function(a) {
      a.app_launch_index = -1;
    });

    for (var i = 0; i < appIds.length; i++) {
      var a = getApp(appIds[i]);
      assert(a.app_launch_index == -1,
             'Found duplicate appId in reorderApps');
      a.app_launch_index = i;
    }
    sendGetAppsCallback();
  }

  /**
   * Update the page number of an app
   * @param {string} id The ID of the application to move.
   * @param {number} page The page index to place the app.
   */
  function setPageIndex(id, page) {
    var app = getApp(id);
    app.page_index = page;
  }

  // The 'send' function
  /**
   * The chrome server communication entrypoint.
   *
   * @param {string} command Name of the command to send.
   * @param {Array} args Array of command-specific arguments.
   */
  return function(command, args) {
    // Chrome API is async
    window.setTimeout(function() {
      switch (command) {
        // called to populate the list of applications
        case 'getApps':
          sendGetAppsCallback();
          break;

        // Called when an app is launched
        // Ignore additional arguments - they've been changing over time and
        // we don't use them in our NTP anyway.
        case 'launchApp':
          launchApp(args[0]);
          break;

        // Called when an app is uninstalled
        case 'uninstallApp':
          uninstallApp(args[0]);
          break;

        // Called when an app is repositioned in the touch NTP
        case 'reorderApps':
          reorderApps(args[0], args[1]);
          break;

        // Called when an app is moved to a different page
        case 'setPageIndex':
          setPageIndex(args[0], parseInt(args[1], 10));
          break;

        default:
          throw new Error('Unexpected chrome command: ' + command);
          break;
      }
    }, 0);
  };
})();

/* A static templateData with english resources */
var templateData = {
  title: 'Standalone New Tab',
  web_store_title: 'Web Store',
  web_store_url: 'https://chrome.google.com/webstore?hl=en-US'
};

/* Hook construction of chrome://theme URLs */
function themeUrlMapper(resourceName) {
  if (resourceName == 'IDR_WEBSTORE_ICON') {
    return 'standalone/webstore_icon.png';
  }
  return undefined;
}

/*
 * On iOS we need a hack to avoid spurious click events
 * In particular, if the user delays briefly between first touching and starting
 * to drag, when the user releases a click event will be generated.
 * Note that this seems to happen regardless of whether we do preventDefault on
 * touchmove events.
 */
if (/iPhone|iPod|iPad/.test(navigator.userAgent) &&
    !(/Chrome/.test(navigator.userAgent))) {
  // We have a real iOS device (no a ChromeOS device pretending to be iOS)
  (function() {
    // True if a gesture is occuring that should cause clicks to be swallowed
    var gestureActive = false;

    // The position a touch was last started
    var lastTouchStartPosition;

    // Distance which a touch needs to move to be considered a drag
    var DRAG_DISTANCE = 3;

    document.addEventListener('touchstart', function(event) {
      lastTouchStartPosition = {
        x: event.touches[0].clientX,
        y: event.touches[0].clientY
      };
      // A touchstart ALWAYS preceeds a click (valid or not), so cancel any
      // outstanding gesture. Also, any multi-touch is a gesture that should
      // prevent clicks.
      gestureActive = event.touches.length > 1;
    }, true);

    document.addEventListener('touchmove', function(event) {
      // When we see a move, measure the distance from the last touchStart
      // If this is a multi-touch then the work here is irrelevant
      // (gestureActive is already true)
      var t = event.touches[0];
      if (Math.abs(t.clientX - lastTouchStartPosition.x) > DRAG_DISTANCE ||
          Math.abs(t.clientY - lastTouchStartPosition.y) > DRAG_DISTANCE) {
        gestureActive = true;
      }
    }, true);

    document.addEventListener('click', function(event) {
      // If we got here without gestureActive being set then it means we had
      // a touchStart without any real dragging before touchEnd - we can allow
      // the click to proceed.
      if (gestureActive) {
        event.preventDefault();
        event.stopPropagation();
      }
    }, true);
  })();
}

/*  Hack to add Element.classList to older browsers that don't yet support it.
    From https://developer.mozilla.org/en/DOM/element.classList.
*/
if (typeof Element !== 'undefined' &&
    !Element.prototype.hasOwnProperty('classList')) {
  (function() {
    var classListProp = 'classList',
        protoProp = 'prototype',
        elemCtrProto = Element[protoProp],
        objCtr = Object,
        strTrim = String[protoProp].trim || function() {
          return this.replace(/^\s+|\s+$/g, '');
        },
        arrIndexOf = Array[protoProp].indexOf || function(item) {
          for (var i = 0, len = this.length; i < len; i++) {
            if (i in this && this[i] === item) {
              return i;
            }
          }
          return -1;
        },
        // Vendors: please allow content code to instantiate DOMExceptions
        /** @constructor  */
        DOMEx = function(type, message) {
          this.name = type;
          this.code = DOMException[type];
          this.message = message;
        },
        checkTokenAndGetIndex = function(classList, token) {
          if (token === '') {
            throw new DOMEx(
                'SYNTAX_ERR',
                'An invalid or illegal string was specified'
            );
          }
          if (/\s/.test(token)) {
            throw new DOMEx(
                'INVALID_CHARACTER_ERR',
                'String contains an invalid character'
            );
          }
          return arrIndexOf.call(classList, token);
        },
        /** @constructor
         *  @extends {Array} */
        ClassList = function(elem) {
          var trimmedClasses = strTrim.call(elem.className),
              classes = trimmedClasses ? trimmedClasses.split(/\s+/) : [];

          for (var i = 0, len = classes.length; i < len; i++) {
            this.push(classes[i]);
          }
          this._updateClassName = function() {
            elem.className = this.toString();
          };
        },
        classListProto = ClassList[protoProp] = [],
        classListGetter = function() {
          return new ClassList(this);
        };

    // Most DOMException implementations don't allow calling DOMException's
    // toString() on non-DOMExceptions. Error's toString() is sufficient here.
    DOMEx[protoProp] = Error[protoProp];
    classListProto.item = function(i) {
      return this[i] || null;
    };
    classListProto.contains = function(token) {
      token += '';
      return checkTokenAndGetIndex(this, token) !== -1;
    };
    classListProto.add = function(token) {
      token += '';
      if (checkTokenAndGetIndex(this, token) === -1) {
        this.push(token);
        this._updateClassName();
      }
    };
    classListProto.remove = function(token) {
      token += '';
      var index = checkTokenAndGetIndex(this, token);
      if (index !== -1) {
        this.splice(index, 1);
        this._updateClassName();
      }
    };
    classListProto.toggle = function(token) {
      token += '';
      if (checkTokenAndGetIndex(this, token) === -1) {
        this.add(token);
      } else {
        this.remove(token);
      }
    };
    classListProto.toString = function() {
      return this.join(' ');
    };

    if (objCtr.defineProperty) {
      var classListDescriptor = {
        get: classListGetter,
        enumerable: true,
        configurable: true
      };
      objCtr.defineProperty(elemCtrProto, classListProp, classListDescriptor);
    } else if (objCtr[protoProp].__defineGetter__) {
      elemCtrProto.__defineGetter__(classListProp, classListGetter);
    }
  }());
}

/* Hack to add Function.bind to older browsers that don't yet support it. From:
   https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Function/bind
*/
if (!Function.prototype.bind) {
  /**
   * @param {Object} selfObj Specifies the object which |this| should
   *     point to when the function is run. If the value is null or undefined,
   *     it will default to the global object.
   * @param {...*} var_args Additional arguments that are partially
   *     applied to the function.
   * @return {!Function} A partially-applied form of the function bind() was
   *     invoked as a method of.
   *  @suppress {duplicate}
   */
  Function.prototype.bind = function(selfObj, var_args) {
    var slice = [].slice,
        args = slice.call(arguments, 1),
        self = this,
        /** @constructor  */
        nop = function() {},
        bound = function() {
          return self.apply(this instanceof nop ? this : (selfObj || {}),
                              args.concat(slice.call(arguments)));
        };
    nop.prototype = self.prototype;
    bound.prototype = new nop();
    return bound;
  };
}