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