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