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 Touch-based new tab page | |
7 * This is the main code for the new tab page used by touch-enabled Chrome | |
8 * browsers. For now this is still a prototype. | |
9 */ | |
10 | |
11 /** | |
12 * The Slider object to use for changing app pages. | |
13 * @type {Slider|undefined} | |
14 */ | |
15 var slider; | |
16 | |
17 /** | |
18 * Template to use for creating new 'apps-page' elements | |
19 * @type {!Element|undefined} | |
20 */ | |
21 var appsPageTemplate; | |
22 | |
23 /** | |
24 * Template to use for creating new 'app-container' elements | |
25 * @type {!Element|undefined} | |
26 */ | |
27 var appTemplate; | |
28 | |
29 /** | |
30 * Template to use for creating new 'dot' elements | |
31 * @type {!Element|undefined} | |
32 */ | |
33 var dotTemplate; | |
34 | |
35 /** | |
36 * The 'apps-page-list' element. | |
37 * @type {!Element} | |
38 */ | |
39 var appsPageList = getRequiredElement('apps-page-list'); | |
40 | |
41 /** | |
42 * A list of all 'apps-page' elements. | |
43 * @type {!NodeList|undefined} | |
44 */ | |
45 var appsPages; | |
46 | |
47 /** | |
48 * The 'dots-list' element. | |
49 * @type {!Element} | |
50 */ | |
51 var dotList = getRequiredElement('dot-list'); | |
52 | |
53 /** | |
54 * A list of all 'dots' elements. | |
55 * @type {!NodeList|undefined} | |
56 */ | |
57 var dots; | |
58 | |
59 /** | |
60 * The 'trash' element. Note that technically this is unnecessary, | |
61 * JavaScript creates the object for us based on the id. But I don't want | |
62 * to rely on the ID being the same, and JSCompiler doesn't know about it. | |
63 * @type {!Element} | |
64 */ | |
65 var trash = getRequiredElement('trash'); | |
66 | |
67 /** | |
68 * The time in milliseconds for most transitions. This should match what's | |
69 * in newtab.css. Unfortunately there's no better way to try to time | |
70 * something to occur until after a transition has completed. | |
71 * @type {number} | |
72 * @const | |
73 */ | |
74 var DEFAULT_TRANSITION_TIME = 500; | |
75 | |
76 /** | |
77 * All the Grabber objects currently in use on the page | |
78 * @type {Array.<Grabber>} | |
79 */ | |
80 var grabbers = []; | |
81 | |
82 /** | |
83 * Holds all event handlers tied to apps (and so subject to removal when the | |
84 * app list is refreshed) | |
85 * @type {!EventTracker} | |
86 */ | |
87 var appEvents = new EventTracker(); | |
88 | |
89 /** | |
90 * Invoked at startup once the DOM is available to initialize the app. | |
91 */ | |
92 function initializeNtp() { | |
93 // Prevent touch events from triggering any sort of native scrolling | |
94 document.addEventListener('touchmove', function(e) { | |
95 e.preventDefault(); | |
96 }, true); | |
97 | |
98 // Get the template elements and remove them from the DOM. Things are | |
99 // simpler if we start with 0 pages and 0 apps and don't leave hidden | |
100 // template elements behind in the DOM. | |
101 appTemplate = getRequiredElement('app-template'); | |
102 appTemplate.removeAttribute('id'); | |
arv (Not doing code reviews)
2011/03/11 20:08:47
I don't really see a need to remove the attribute
Rick Byers
2011/03/15 21:47:54
I just noticed that it left empty 'id' attributes
| |
103 appTemplate.id = ''; | |
104 | |
105 appsPages = appsPageList.getElementsByClassName('apps-page'); | |
106 assert(appsPages.length == 1, | |
107 'Expected exactly one apps-page in the apps-page-list.'); | |
108 appsPageTemplate = appsPages[0]; | |
109 appsPageList.removeChild(appsPages[0]); | |
110 | |
111 dots = dotList.getElementsByClassName('dot'); | |
112 assert(dots.length == 1, | |
113 'Expected exactly one dot in the dots-list.'); | |
114 dotTemplate = dots[0]; | |
115 dotList.removeChild(dots[0]); | |
116 | |
117 // Initialize the slider without any cards at the moment | |
118 var appsFrame = getRequiredElement('apps-frame'); | |
119 slider = new Slider(appsFrame, appsPageList, [], 0, appsFrame.offsetWidth); | |
120 slider.initialize(); | |
121 | |
122 // Ensure the slider is resized appropriately with the window | |
123 window.addEventListener('resize', function() { | |
124 slider.resize(appsFrame.offsetWidth); | |
125 }, false); | |
126 | |
127 // Handle the page being changed | |
128 appsPageList.addEventListener( | |
129 Slider.EventType.CARD_CHANGED, | |
130 function(e) { | |
131 // Update the active dot | |
132 var curDot = dotList.getElementsByClassName('selected')[0]; | |
133 if (curDot) | |
134 curDot.classList.remove('selected'); | |
135 var newPageIndex = e.sender.getCurrentCard(); | |
136 dots[newPageIndex].classList.add('selected'); | |
137 // If an app was being dragged, move it to the end of the new page | |
138 if (draggingAppContainer) | |
139 appsPages[newPageIndex].appendChild(draggingAppContainer); | |
140 }, false); | |
141 | |
142 // Add a drag handler to the body (for drags that don't land on an existing | |
143 // app) | |
144 document.body.addEventListener(Grabber.EventType.DRAG_ENTER, appDragEnter, | |
arv (Not doing code reviews)
2011/03/11 20:08:47
document.addEventListener also works
Rick Byers
2011/03/15 21:47:54
Done.
| |
145 false); | |
arv (Not doing code reviews)
2011/03/11 20:08:47
you can skip optional false
Rick Byers
2011/03/15 21:47:54
Cool. As an aside, is the only way for me to know
arv (Not doing code reviews)
2011/03/15 23:53:43
The WebKit DOM bindings does type coercion. In JS,
| |
146 | |
147 // Handle dropping an app anywhere other than on the trash | |
148 document.body.addEventListener(Grabber.EventType.DROP, appDrop, | |
149 false); | |
150 | |
151 // Add handles to manage the transition into/out-of rearrange mode | |
152 // Note that we assume here that we only use a Grabber for moving apps, | |
153 // so ANY GRAB event means we're enterring rearrange mode. | |
154 appsFrame.addEventListener(Grabber.EventType.GRAB, enterRearrangeMode, | |
155 false); | |
156 appsFrame.addEventListener(Grabber.EventType.RELEASE, leaveRearrangeMode, | |
157 false); | |
158 | |
159 // Add handlers for the tash can | |
160 trash.addEventListener(Grabber.EventType.DRAG_ENTER, function(e) { | |
161 trash.classList.add('hover'); | |
162 e.sender.element.classList.add('trashing'); | |
arv (Not doing code reviews)
2011/03/11 20:08:47
Yeah, this is what I hate with working with non Ch
Rick Byers
2011/03/15 21:47:54
Nope, that would be wrong (counfuses the source an
| |
163 e.stopPropagation(); | |
164 }, false); | |
165 trash.addEventListener(Grabber.EventType.DRAG_LEAVE, function(e) { | |
166 e.sender.element.classList.remove('trashing'); | |
167 trash.classList.remove('hover'); | |
168 }, false); | |
169 trash.addEventListener(Grabber.EventType.DROP, appTrash, false); | |
170 | |
171 // Fill in the apps | |
172 chrome.send('getApps'); | |
arv (Not doing code reviews)
2011/03/11 20:08:47
Call this earlier for faster NTP
Rick Byers
2011/03/15 21:47:54
Done.
| |
173 } | |
174 | |
175 /** | |
176 * Simple common assertion API | |
177 * @param {*} condition The condition to test. Note that this may be used to | |
178 * test whether a value is defined or not, and we don't want to force a | |
179 * cast to Boolean. | |
180 * @param {string=} opt_message A message to use in any error. | |
181 */ | |
182 function assert(condition, opt_message) { | |
183 if (!condition) { | |
184 var msg = 'Assertion failed'; | |
185 if (opt_message) | |
186 msg = msg + ': ' + opt_message; | |
187 throw new Error(msg); | |
188 } | |
189 } | |
190 | |
191 /** | |
192 * Get an element that's known to exist by its ID. We use this instead of just | |
193 * calling getElementById and not checking the result because this lets us | |
194 * satisfy the JSCompiler type system. | |
195 * @param {string} id The identifier name. | |
196 * @return {!Element} the Element. | |
197 */ | |
198 function getRequiredElement(id) | |
199 { | |
arv (Not doing code reviews)
2011/03/11 20:08:47
{ after new line
Rick Byers
2011/03/15 21:47:54
Done.
| |
200 var element = /** @type !Element */ (document.getElementById(id)); | |
arv (Not doing code reviews)
2011/03/11 20:08:47
Oh noes... type casting is a plague that I loathed
Rick Byers
2011/03/15 21:47:54
You were right that I was being sloppy - I fixed i
| |
201 assert(Boolean(element), 'Missing required element: ' + id); | |
arv (Not doing code reviews)
2011/03/11 20:08:47
no need to convert to boolean here
Rick Byers
2011/03/15 21:47:54
Done.
| |
202 return element; | |
203 } | |
204 | |
205 /** | |
206 * Remove all children of an element which have a given class in | |
207 * their classList. | |
208 * @param {!Element} element The parent element to examine. | |
209 * @param {string} className The class to look for. | |
210 */ | |
211 function removeChildrenByClassName(element, className) { | |
212 for (var child = element.firstElementChild; child;) { | |
213 var prev = child; | |
214 child = child.nextElementSibling; | |
215 if (prev.classList.contains(className)) | |
216 element.removeChild(prev); | |
217 } | |
218 } | |
219 | |
220 /** | |
221 * Callback invoked by chrome with the apps available. | |
222 * | |
223 * Note that calls to this function can occur at any time, not just in response | |
224 * to a getApps request. For example, when a user installs/uninstalls an app on | |
225 * another synchronized devices. | |
226 * @param {Object} data An object with all the data on available | |
227 * applications. | |
228 */ | |
229 function getAppsCallback(data) | |
230 { | |
231 // Clean up any existing grabber objects - cancelling any outstanding drag. | |
232 // Ideally an async app update wouldn't disrupt an active drag but | |
233 // that would require us to re-use existing elements and detect how the apps | |
234 // have changed, which would be a lot of work. | |
235 // Note that was have to explicitly clean up the grabber objects so they stop | |
arv (Not doing code reviews)
2011/03/11 20:08:47
typo
Rick Byers
2011/03/15 21:47:54
Done.
| |
236 // listening to events and break the DOM<->JS cycles necessary to enable | |
237 // collection of all these objects. | |
238 grabbers.forEach(function(g) { | |
239 // Note that this may raise DRAG_END/RELEASE events to clean up an | |
240 // oustanding drag. | |
241 g.dispose(); | |
242 }); | |
243 assert(!draggingAppContainer && !draggingAppOriginalPosition && | |
244 !draggingAppOriginalPage); | |
245 grabbers = []; | |
246 appEvents.removeAll(); | |
247 | |
248 // Clear any existing apps pages and dots. | |
249 // TODO(rbyers): It might be nice to preserve animation of dots after an | |
250 // uninstall. Could we re-use the existing page and dot elements? It seems | |
251 // unfortunate to have Chrome send us the entire apps list after an uninstall. | |
252 removeChildrenByClassName(appsPageList, 'apps-page'); | |
253 removeChildrenByClassName(dotList, 'dot'); | |
254 | |
255 // Get the array of apps and add any special synthesized entries | |
256 var apps = data.apps; | |
257 apps.push(makeWebstoreApp()); | |
258 | |
259 // Sort by launch index | |
260 apps.sort(function(a, b) { | |
261 return a.app_launch_index - b.app_launch_index; | |
262 }); | |
263 | |
264 // Add the apps, creating pages as necessary | |
265 for (var i = 0; i < apps.length; i++) { | |
266 var app = apps[i]; | |
267 var pageIndex = (app.page_index || 0); | |
268 while (pageIndex >= appsPages.length) { | |
269 var origPageCount = appsPages.length; | |
270 createAppPage(); | |
271 // Confirm that appsPages is a live object, updated when a new page is | |
272 // added (otherwise we'd have an infinite loop) | |
273 assert(appsPages.length == origPageCount + 1, 'expected new page'); | |
274 } | |
275 appendApp(appsPages[pageIndex], app); | |
276 } | |
277 | |
278 // Tell the slider about the pages | |
279 updateSliderCards(); | |
280 | |
281 // Mark the current page | |
282 dots[slider.getCurrentCard()].classList.add('selected'); | |
arv (Not doing code reviews)
2011/03/11 20:08:47
slider.currentCard... "use getters for getters" ru
Rick Byers
2011/03/15 21:47:54
Done.
| |
283 } | |
284 | |
285 /** | |
286 * Make a synthesized app object representing the chrome web store. It seems | |
287 * like this could just as easily come from the back-end, and then would support | |
288 * being rearranged, etc. | |
289 * @return {Object} The app object as would be sent from the webui back-end. | |
290 */ | |
291 function makeWebstoreApp() { | |
292 return { | |
293 id: '', // Empty ID signifies this is a special synthesized app | |
294 page_index: 0, | |
295 app_launch_index: -1, // always first | |
296 name: templateData.web_store_title, | |
297 launch_url: templateData.web_store_url, | |
298 icon_big: getThemeUrl('IDR_WEBSTORE_ICON') | |
299 }; | |
300 } | |
301 | |
302 /** | |
303 * Given a theme resource name, construct a URL for it. | |
304 * @param {string} resourceName The name of the resource. | |
305 * @return {string} A url which can be used to load the resource. | |
306 */ | |
307 function getThemeUrl(resourceName) { | |
308 // Allow standalone_hack.js to hook this mapping (since chrome:// URLs | |
309 // won't work for a standalone page) | |
310 if (typeof(themeUrlMapper) == 'function') { | |
arv (Not doing code reviews)
2011/03/11 20:08:47
typeof is a unary operator and not a function
Rick Byers
2011/03/15 21:47:54
Done.
| |
311 var u = themeUrlMapper(resourceName); | |
312 if (u) | |
313 return u; | |
314 } | |
315 return 'chrome://theme/' + resourceName; | |
316 } | |
317 | |
318 /** | |
319 * Callback invoked by chrome whenever an app preference changes. | |
320 * The normal NTP uses this to keep track of the current launch-type of an app, | |
321 * updating the choices in the context menu. We don't have such a menu so don't | |
322 * use this at all (but it still needs to be here for chrome to call). | |
323 * @param {Object} data An object with all the data on available | |
324 * applications. | |
325 */ | |
326 function appsPrefChangeCallback(data) { | |
327 } | |
328 | |
329 /** | |
330 * Invoked whenever the pages in apps-page-list have changed so that | |
331 * the Slider knows about the new elements. | |
332 */ | |
333 function updateSliderCards() | |
334 { | |
335 var pageNo = slider.getCurrentCard(); | |
336 if (pageNo >= appsPages.length) | |
337 pageNo = appsPages.length - 1; | |
338 var pageArray = []; | |
339 for (var i = 0; i < appsPages.length; i++) | |
340 pageArray[i] = appsPages[i]; | |
341 slider.setCards(pageArray, pageNo); | |
342 } | |
343 | |
344 /** | |
345 * Create a new app element and attach it to the end of the specified app page. | |
346 * @param {!Element} parent The element where the app should be inserted. | |
347 * @param {!Object} app The application object to create an app for. | |
348 */ | |
349 function appendApp(parent, app) { | |
350 // Make a deep copy of the template and clear its ID | |
351 var containerElement = /** @type {!Element} */ (appTemplate.cloneNode(true)); | |
352 var appElement = containerElement.getElementsByClassName('app')[0]; | |
353 assert(appElement, 'Expected app-template to have an app child'); | |
354 assert(typeof(app.id) == 'string', | |
355 'Expected every app to have an ID or empty string'); | |
356 appElement.setAttribute('app-id', app.id); | |
357 | |
358 // Find the span element (if any) and fill it in with the app name | |
359 var span = appElement.getElementsByTagName('span')[0]; | |
arv (Not doing code reviews)
2011/03/11 20:08:47
appElement.querySelector('span')
Rick Byers
2011/03/15 21:47:54
Done.
| |
360 if (span) | |
361 span.textContent = app.name; | |
362 | |
363 // Fill in the image | |
364 // We use a mask of the same image so CSS rules can highlight just the image | |
365 // when it's touched. | |
366 var appImg = appElement.getElementsByTagName('img')[0]; | |
arv (Not doing code reviews)
2011/03/11 20:08:47
querySelector
Rick Byers
2011/03/15 21:47:54
Done.
| |
367 if (appImg) { | |
368 appImg.src = app.icon_big; | |
369 appImg.style.webkitMaskImage = 'url(' + app.icon_big + ')'; | |
arv (Not doing code reviews)
2011/03/11 20:08:47
We ran into a URL bug in WebKit for the NTP. I gue
Rick Byers
2011/03/15 21:47:54
Thanks! I'm not sure what the restrictions are on
| |
370 // We put a click handler just on the app image - so clicking on the margins | |
371 // between apps doesn't do anything | |
372 if (app.id) { | |
373 appEvents.add(appImg, 'click', appClick, false); | |
374 } else { | |
375 // Special case of synthesized apps - can't launch directly so just change | |
376 // the URL as if we clicked a link. We may want to eventually support | |
377 // tracking clicks with ping messages, but really it seems it would be | |
378 // better for the back-end to just create virtual apps for such cases. | |
379 appEvents.add(appImg, 'click', function(e) { | |
380 window.location = app.launch_url; | |
381 }, false); | |
382 } | |
383 } | |
384 | |
385 // Only real apps with back-end storage (for their launch index, etc.) can be | |
386 // rearranged. | |
387 if (app.id) { | |
388 // Create a grabber to support moving apps around | |
389 // Note that we move the app rather than the container. This is so that an | |
390 // element remains in the original position so we can detect when an app is | |
391 // dropped in its starting location. | |
392 var grabber = new Grabber(/** @type {!Element} */ (appElement)); | |
arv (Not doing code reviews)
2011/03/11 20:08:47
sigh
| |
393 grabbers.push(grabber); | |
394 | |
395 // Register to be made aware of when we are dragged | |
396 appEvents.add(appElement, Grabber.EventType.DRAG_START, appDragStart, | |
397 false); | |
398 appEvents.add(appElement, Grabber.EventType.DRAG_END, appDragEnd, | |
399 false); | |
400 | |
401 // Register to be made aware of any app drags on top of our container | |
402 appEvents.add(containerElement, Grabber.EventType.DRAG_ENTER, appDragEnter, | |
403 false); | |
404 } else { | |
405 // Prevent any built-in drag-and-drop support from activating for the | |
406 // element. | |
407 appEvents.add(appElement, 'dragstart', function(e) { | |
408 e.preventDefault(); | |
409 }, true); | |
410 } | |
411 | |
412 // Insert at the end of the provided page | |
413 parent.appendChild(containerElement); | |
414 } | |
415 | |
416 /** | |
417 * Creates a new page for apps | |
418 * | |
419 * @return {!Element} The apps-page element created. | |
420 * @param {boolean=} opt_animate If true, add the class 'new' to the created | |
421 * dot. | |
422 */ | |
423 function createAppPage(opt_animate) | |
424 { | |
425 // Make a shallow copy of the app page template. | |
426 var newPage = /** @type !Element */ (appsPageTemplate.cloneNode(false)); | |
427 appsPageList.appendChild(newPage); | |
428 | |
429 // Make a deep copy of the dot template to add a new one. | |
430 var dotCount = dots.length; | |
431 var newDot = /** @type !Element */ (dotTemplate.cloneNode(true)); | |
432 if (opt_animate) | |
433 newDot.classList.add('new'); | |
434 dotList.appendChild(newDot); | |
435 | |
436 // Add click handler to the dot to change the page. | |
437 // TODO(rbyers): Perhaps this should be TouchHandler.START_EVENT_ (so we don't | |
438 // rely on synthesized click events, and the change takes effect before | |
439 // releasing). However, click events seems to be synthesized for a region | |
440 // outside the border, and a 10px box is too small to require touch events to | |
441 // fall inside of. We could get around this by adding a box around the dot for | |
442 // accepting the touch events. | |
443 var switchPage = function(e) { | |
arv (Not doing code reviews)
2011/03/11 20:08:47
function switchPage(e) {
Rick Byers
2011/03/15 21:47:54
Done.
| |
444 slider.setCurrentCard(dotCount, true); | |
445 e.stopPropagation(); | |
446 } | |
arv (Not doing code reviews)
2011/03/11 20:08:47
missing semicolon
Rick Byers
2011/03/15 21:47:54
But not when using the syntax you prefer, right?
| |
447 appEvents.add(newDot, 'click', switchPage, false); | |
448 | |
449 // Change pages whenever an app is dragged over a dot. | |
450 appEvents.add(newDot, Grabber.EventType.DRAG_ENTER, switchPage, false); | |
451 | |
452 return newPage; | |
453 } | |
454 | |
455 /** | |
456 * Invoked when an app is clicked | |
457 * @param {Event} e The click event. | |
458 */ | |
459 function appClick(e) { | |
460 var target = /** @type {!Element} */ (e.currentTarget); | |
461 var app = getParentByClassName(target, 'app'); | |
462 assert(app, 'appClick should have been on a descendant of an app'); | |
463 | |
464 var appId = app.getAttribute('app-id'); | |
465 assert(appId, 'unexpected app without appId'); | |
466 | |
467 // Tell chrome to launch the app. | |
468 var NTP_APPS_MAXIMIZED = 0; | |
469 chrome.send('launchApp', [appId, NTP_APPS_MAXIMIZED]); | |
470 | |
471 // Don't propagate the click (in case we have a link or something) | |
arv (Not doing code reviews)
2011/03/11 20:08:47
This comment explains why we preventDefault. It do
Rick Byers
2011/03/15 21:47:54
What is your preferred style for when to call stop
| |
472 e.stopPropagation(); | |
473 e.preventDefault(); | |
474 } | |
475 | |
476 /** | |
477 * Search an elements ancestor chain for the nearest element that is a member of | |
478 * the specified class. | |
479 * @param {!Element} element The element to start searching from. | |
480 * @param {string} className The name of the class to locate. | |
481 * @return {Element} The first ancestor of the specified class or null. | |
482 */ | |
483 function getParentByClassName(element, className) | |
484 { | |
485 for (var e = element; | |
486 e && e.nodeType == Node.ELEMENT_NODE; | |
487 e = e.parentNode) { | |
arv (Not doing code reviews)
2011/03/11 20:08:47
e.parentElement allows you to skip the nodeTyoe ch
Rick Byers
2011/03/15 21:47:54
Done.
| |
488 if (e.classList.contains(className)) | |
489 return /** @type {Element} */ (e); | |
490 } | |
491 return null; | |
492 } | |
493 | |
494 /** | |
495 * The container where the app currently being dragged came from. | |
496 * @type {Element} | |
497 */ | |
498 var draggingAppContainer = null; | |
arv (Not doing code reviews)
2011/03/11 20:08:47
Do you really need to init these to null? Most of
Rick Byers
2011/03/15 21:47:54
Nope, I originally used undefined and switched to
| |
499 | |
500 /** | |
501 * The apps-page that the app currently being dragged camed from. | |
502 * @type {Element} | |
503 */ | |
504 var draggingAppOriginalPage = null; | |
505 | |
506 /** | |
507 * The element that was originally after the app currently being dragged (or | |
508 * null if it was the last on the page). | |
509 * @type {Element} | |
510 */ | |
511 var draggingAppOriginalPosition = null; | |
512 | |
513 /** | |
514 * Invoked when app dragging begins. | |
515 * @param {Event} e The event from the Grabber indicating the drag. | |
516 */ | |
517 function appDragStart(e) { | |
518 // Pull the element out to the appsFrame using fixed positioning. This ensures | |
519 // that the app is not affected (remains under the finger) if the slider | |
520 // changes cards and is translated. An alternate approach would be to use | |
521 // fixed positioning for the slider (so that changes to its position don't | |
522 // affect children that aren't positioned relative to it), but we don't yet | |
523 // have GPU acceleration for this. Note that we use the appsFrame | |
524 var element = e.sender.element; | |
525 | |
526 var pos = element.getBoundingClientRect(); | |
527 element.style.webkitTransform = ''; | |
528 | |
529 element.style.position = 'fixed'; | |
530 // Don't want to zoom around the middle since the left/top co-ordinates | |
531 // are post-transform values. | |
532 element.style.webkitTransformOrigin = 'left top'; | |
533 element.style.left = pos.left + 'px'; | |
534 element.style.top = pos.top + 'px'; | |
535 | |
536 // Keep track of what app is being dragged and where it came from | |
537 assert(draggingAppContainer == null, | |
538 'got DRAG_START without DRAG_END'); | |
539 draggingAppContainer = element.parentNode; | |
540 assert(draggingAppContainer.classList.contains('app-container')); | |
541 draggingAppOriginalPosition = draggingAppContainer.nextSibling; | |
542 draggingAppOriginalPage = draggingAppContainer.parentNode; | |
543 | |
544 // Move the app out of the container | |
545 // Note that appendChild also removes the element from its current parent. | |
546 getRequiredElement('apps-frame').appendChild(element); | |
547 } | |
548 | |
549 /** | |
550 * Invoked when app dragging terminates (either successfully or not) | |
551 * @param {CustomEvent} e The event from the Grabber. | |
552 */ | |
553 function appDragEnd(e) { | |
554 // Stop floating the app | |
555 var appBeingDragged = e.sender.element; | |
556 assert(appBeingDragged.classList.contains('app')); | |
557 appBeingDragged.style.position = ''; | |
558 appBeingDragged.style.webkitTransformOrigin = ''; | |
559 appBeingDragged.style.left = ''; | |
560 appBeingDragged.style.top = ''; | |
561 | |
562 // If we have an active drag (i.e. it wasn't aborted by an app update) | |
563 if (draggingAppContainer) { | |
564 // Put the app back into it's container | |
565 if (appBeingDragged.parentNode != draggingAppContainer) | |
566 draggingAppContainer.appendChild(appBeingDragged); | |
567 | |
568 // If we care about the container's original position | |
569 if (draggingAppOriginalPage) | |
570 { | |
571 // Then put the container back where it came from | |
572 if (draggingAppOriginalPosition) { | |
573 draggingAppOriginalPage.insertBefore(draggingAppContainer, | |
574 draggingAppOriginalPosition); | |
575 } else { | |
576 draggingAppOriginalPage.appendChild(draggingAppContainer); | |
577 } | |
578 } | |
579 } | |
580 | |
581 draggingAppContainer = null; | |
582 draggingAppOriginalPage = null; | |
583 draggingAppOriginalPosition = null; | |
584 } | |
585 | |
586 /** | |
587 * Invoked when an app is dragged over another app. Updates the DOM to affect | |
588 * the rearrangement (but doesn't commit the change until the app is dropped). | |
589 * @param {Event} e The event from the Grabber indicating the drag. | |
590 */ | |
591 function appDragEnter(e) | |
592 { | |
593 assert(draggingAppContainer, 'expected stored container'); | |
594 var sourceContainer = /** @type {!Element} */ (draggingAppContainer); | |
595 | |
596 e.stopPropagation(); | |
597 | |
598 var curPage = appsPages[slider.getCurrentCard()]; | |
599 var followingContainer = null; | |
600 | |
601 // If we dragged over a specific app, determine which one to insert before | |
602 if (e.currentTarget != document.body) { | |
603 | |
604 // Start by assuming we'll insert the app before the one dragged over | |
605 followingContainer = e.currentTarget; | |
606 assert(followingContainer.classList.contains('app-container'), | |
607 'expected drag over container'); | |
608 assert(followingContainer.parentNode == curPage); | |
609 if (followingContainer == draggingAppContainer) | |
610 return; | |
611 | |
612 // But if it's after the current container position then we'll need to | |
613 // move ahead by one to account for the container being removed. | |
614 if (curPage == draggingAppContainer.parentNode) { | |
615 for (var c = draggingAppContainer; c; c = c.nextSibling) { | |
arv (Not doing code reviews)
2011/03/11 20:08:47
nextElementSibling maybe to skip non element nodes
Rick Byers
2011/03/15 21:47:54
Done.
| |
616 if (c == followingContainer) { | |
617 followingContainer = followingContainer.nextSibling; | |
618 break; | |
619 } | |
620 } | |
621 } | |
622 } | |
623 | |
624 // Move the container to the appropriate place on the page | |
625 if (followingContainer) { | |
arv (Not doing code reviews)
2011/03/11 20:08:47
This if is not needed. insertBefore(node, null) is
Rick Byers
2011/03/15 21:47:54
Nice
| |
626 curPage.insertBefore(draggingAppContainer, followingContainer); | |
627 } else { | |
628 curPage.appendChild(draggingAppContainer); | |
629 } | |
630 } | |
631 | |
632 /** | |
633 * Invoked when an app is dropped on the trash | |
634 * @param {Event} e The event from the Grabber indicating the drop. | |
635 */ | |
636 function appTrash(e) { | |
637 var appElement = e.sender.element; | |
638 assert(appElement.classList.contains('app')); | |
639 var appId = appElement.getAttribute('app-id'); | |
640 assert(appId); | |
641 | |
642 // Mark this drop as handled | |
643 e.stopPropagation(); | |
arv (Not doing code reviews)
2011/03/11 20:08:47
I don't understand what propagation has to do with
Rick Byers
2011/03/15 21:47:54
Does the expanded comment help? I want to treat a
| |
644 | |
645 // Tell chrome to uninstall the app (prompting the user) | |
646 chrome.send('uninstallApp', [appId]); | |
647 | |
648 // Return the trash can to normal (we don't get a DRAG_LEAVE) | |
649 getRequiredElement('trash').classList.remove('hover'); | |
650 appElement.classList.remove('trashing'); | |
651 } | |
652 | |
653 /** | |
654 * Called when an app is dropped anywhere other than the trash can. Commits any | |
655 * movement that has occurred. | |
656 * @param {Event} e The event from the Grabber indicating the drop. | |
657 */ | |
658 function appDrop(e) { | |
659 if (!draggingAppContainer) | |
660 // Drag was aborted (eg. due to an app update) - do nothing | |
661 return; | |
662 | |
663 // If the app is dropped back into it's original position then do nothing | |
664 assert(draggingAppOriginalPage); | |
665 if (draggingAppContainer.parentNode == draggingAppOriginalPage && | |
666 draggingAppContainer.nextSibling == draggingAppOriginalPosition) | |
667 return; | |
668 | |
669 // Determine which app was being dragged | |
670 var appElement = e.sender.element; | |
671 assert(appElement.classList.contains('app')); | |
672 var appId = appElement.getAttribute('app-id'); | |
673 assert(appId); | |
674 | |
675 // Update the page index for the app if it's changed. This doesn't trigger a | |
676 // call to getAppsCallback so we want to do it before reorderApps | |
677 var pageIndex = slider.getCurrentCard(); | |
678 assert(pageIndex >= 0 && pageIndex < appsPages.length, | |
679 'page number out of range'); | |
680 if (appsPages[pageIndex] != draggingAppOriginalPage) | |
681 chrome.send('setPageIndex', [appId, pageIndex]); | |
682 | |
683 // Put the app being dragged back into it's container | |
684 draggingAppContainer.appendChild(appElement); | |
685 | |
686 // Create a list of all appIds in the order now present in the DOM | |
687 var appIds = []; | |
688 for (var page = 0; page < appsPages.length; page++) { | |
689 var appsOnPage = appsPages[page].getElementsByClassName('app'); | |
690 for (var i = 0; i < appsOnPage.length; i++) { | |
691 var id = appsOnPage[i].getAttribute('app-id'); | |
692 if (id) | |
693 appIds.push(id); | |
694 } | |
695 } | |
696 | |
697 // We are going to commit this repositioning - clear the original position | |
698 draggingAppOriginalPage = null; | |
699 draggingAppOriginalPosition = null; | |
700 | |
701 // Tell chrome to update its database to persist this new order of apps This | |
702 // will cause getAppsCallback to be invoked and the apps to be redrawn. | |
703 chrome.send('reorderApps', [appId, appIds]); | |
704 appMoved = true; | |
705 } | |
706 | |
707 /** | |
708 * Set to true if we're currently in rearrange mode and an app has | |
709 * been successfully dropped to a new location. This indicates that | |
710 * a getAppsCallback call is pending and we can rely on the DOM being | |
711 * updated by that. | |
712 * @type {boolean} | |
713 */ | |
714 var appMoved = false; | |
715 | |
716 /** | |
717 * Invoked whenever some app is grabbed | |
718 * @param {Event} e The Grabber Grab event. | |
719 */ | |
720 function enterRearrangeMode(e) | |
721 { | |
722 // Stop the slider from sliding for this touch | |
723 slider.cancelTouch(); | |
724 | |
725 // Add an extra blank page in case the user wants to create a new page | |
726 createAppPage(true); | |
727 var pageAdded = appsPages.length - 1; | |
728 window.setTimeout(function() { | |
729 dots[pageAdded].classList.remove('new'); | |
730 }, 0); | |
731 | |
732 updateSliderCards(); | |
733 | |
734 // Cause the dot-list to grow | |
735 getRequiredElement('footer').classList.add('rearrange-mode'); | |
736 | |
737 assert(!appMoved, 'appMoved should not be set yet'); | |
738 } | |
739 | |
740 /** | |
741 * Invoked whenever some app is released | |
742 * @param {Event} e The Grabber RELEASE event. | |
743 */ | |
744 function leaveRearrangeMode(e) | |
745 { | |
746 // Return the dot-list to normal | |
747 getRequiredElement('footer').classList.remove('rearrange-mode'); | |
748 | |
749 // If we didn't successfully re-arrange an app, then we won't be | |
750 // refreshing the app view in getAppCallback and need to explicitly remove the | |
751 // extra empty page we added. We don't want to do this in the normal case | |
752 // because if we did actually drop an app there, we want to retain that page | |
753 // as our current page number. | |
754 if (!appMoved) { | |
755 assert(appsPages[appsPages.length - 1]. | |
756 getElementsByClassName('app-container').length == 0, | |
757 'Last app page should be empty'); | |
758 removePage(appsPages.length - 1); | |
759 } | |
760 appMoved = false; | |
761 } | |
762 | |
763 /** | |
764 * Remove the page with the specified index and update the slider. | |
765 * @param {number} pageNo The index of the page to remove. | |
766 */ | |
767 function removePage(pageNo) | |
768 { | |
769 var page = appsPages[pageNo]; | |
770 | |
771 // Remove the page from the DOM | |
772 page.parentNode.removeChild(page); | |
773 | |
774 // Remove the corresponding dot | |
775 // Need to give it a chance to animate though | |
776 var dot = dots[pageNo]; | |
777 dot.classList.add('new'); | |
778 window.setTimeout(function() { | |
779 // If we've re-created the apps (eg. because an app was uninstalled) then | |
780 // we will have removed the old dots from the document already, so skip. | |
781 if (dot.parentNode) | |
782 dot.parentNode.removeChild(dot); | |
783 }, DEFAULT_TRANSITION_TIME); | |
784 | |
785 updateSliderCards(); | |
786 } | |
787 | |
788 // There doesn't seem to be any need to wait for DOMContentLoaded | |
789 initializeNtp(); | |
790 | |
OLD | NEW |