Index: chrome/browser/resources/touch_ntp/standalone/standalone_hack.js |
diff --git a/chrome/browser/resources/touch_ntp/standalone/standalone_hack.js b/chrome/browser/resources/touch_ntp/standalone/standalone_hack.js |
new file mode 100644 |
index 0000000000000000000000000000000000000000..d636f999c76a314da2bbe1357ed42dde7c7460c7 |
--- /dev/null |
+++ b/chrome/browser/resources/touch_ntp/standalone/standalone_hack.js |
@@ -0,0 +1,562 @@ |
+// 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; |
+ }; |
+} |
+ |