Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(73)

Side by Side Diff: chrome/browser/resources/touch_ntp/standalone/standalone_hack.js

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

Powered by Google App Engine
This is Rietveld 408576698