// 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; }; }