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

Side by Side Diff: chrome/browser/resources/touch_ntp/newtab.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: Tweaks based on CR feedback from arv 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 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
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698