| Index: chrome/browser/resources/touch_ntp/newtab.js
|
| diff --git a/chrome/browser/resources/touch_ntp/newtab.js b/chrome/browser/resources/touch_ntp/newtab.js
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..91df34dccccf47e0b3fdd96f6faa5016b5e3c5c7
|
| --- /dev/null
|
| +++ b/chrome/browser/resources/touch_ntp/newtab.js
|
| @@ -0,0 +1,805 @@
|
| +// Copyright (c) 2011 The Chromium Authors. All rights reserved.
|
| +// Use of this source code is governed by a BSD-style license that can be
|
| +// found in the LICENSE file.
|
| +
|
| +/**
|
| + * @fileoverview Touch-based new tab page
|
| + * This is the main code for the new tab page used by touch-enabled Chrome
|
| + * browsers. For now this is still a prototype.
|
| + */
|
| +
|
| +// Use an anonymous function to enable strict mode just for this file (which
|
| +// will be concatenated with other files when embedded in Chrome
|
| +var ntp = (function() {
|
| + 'use strict';
|
| +
|
| + /**
|
| + * The Slider object to use for changing app pages.
|
| + * @type {Slider|undefined}
|
| + */
|
| + var slider;
|
| +
|
| + /**
|
| + * Template to use for creating new 'apps-page' elements
|
| + * @type {!Element|undefined}
|
| + */
|
| + var appsPageTemplate;
|
| +
|
| + /**
|
| + * Template to use for creating new 'app-container' elements
|
| + * @type {!Element|undefined}
|
| + */
|
| + var appTemplate;
|
| +
|
| + /**
|
| + * Template to use for creating new 'dot' elements
|
| + * @type {!Element|undefined}
|
| + */
|
| + var dotTemplate;
|
| +
|
| + /**
|
| + * The 'apps-page-list' element.
|
| + * @type {!Element}
|
| + */
|
| + var appsPageList = getRequiredElement('apps-page-list');
|
| +
|
| + /**
|
| + * A list of all 'apps-page' elements.
|
| + * @type {!NodeList|undefined}
|
| + */
|
| + var appsPages;
|
| +
|
| + /**
|
| + * The 'dots-list' element.
|
| + * @type {!Element}
|
| + */
|
| + var dotList = getRequiredElement('dot-list');
|
| +
|
| + /**
|
| + * A list of all 'dots' elements.
|
| + * @type {!NodeList|undefined}
|
| + */
|
| + var dots;
|
| +
|
| + /**
|
| + * The 'trash' element. Note that technically this is unnecessary,
|
| + * JavaScript creates the object for us based on the id. But I don't want
|
| + * to rely on the ID being the same, and JSCompiler doesn't know about it.
|
| + * @type {!Element}
|
| + */
|
| + var trash = getRequiredElement('trash');
|
| +
|
| + /**
|
| + * The time in milliseconds for most transitions. This should match what's
|
| + * in newtab.css. Unfortunately there's no better way to try to time
|
| + * something to occur until after a transition has completed.
|
| + * @type {number}
|
| + * @const
|
| + */
|
| + var DEFAULT_TRANSITION_TIME = 500;
|
| +
|
| + /**
|
| + * All the Grabber objects currently in use on the page
|
| + * @type {Array.<Grabber>}
|
| + */
|
| + var grabbers = [];
|
| +
|
| + /**
|
| + * Holds all event handlers tied to apps (and so subject to removal when the
|
| + * app list is refreshed)
|
| + * @type {!EventTracker}
|
| + */
|
| + var appEvents = new EventTracker();
|
| +
|
| + /**
|
| + * Invoked at startup once the DOM is available to initialize the app.
|
| + */
|
| + function initializeNtp() {
|
| + // Request data on the apps so we can fill them in.
|
| + // Note that this is kicked off asynchronously. 'getAppsCallback' will be
|
| + // invoked at some point after this function returns.
|
| + chrome.send('getApps');
|
| +
|
| + // Prevent touch events from triggering any sort of native scrolling
|
| + document.addEventListener('touchmove', function(e) {
|
| + e.preventDefault();
|
| + }, true);
|
| +
|
| + // Get the template elements and remove them from the DOM. Things are
|
| + // simpler if we start with 0 pages and 0 apps and don't leave hidden
|
| + // template elements behind in the DOM.
|
| + appTemplate = getRequiredElement('app-template');
|
| + appTemplate.id = null;
|
| +
|
| + appsPages = appsPageList.getElementsByClassName('apps-page');
|
| + assert(appsPages.length == 1,
|
| + 'Expected exactly one apps-page in the apps-page-list.');
|
| + appsPageTemplate = appsPages[0];
|
| + appsPageList.removeChild(appsPages[0]);
|
| +
|
| + dots = dotList.getElementsByClassName('dot');
|
| + assert(dots.length == 1,
|
| + 'Expected exactly one dot in the dots-list.');
|
| + dotTemplate = dots[0];
|
| + dotList.removeChild(dots[0]);
|
| +
|
| + // Initialize the slider without any cards at the moment
|
| + var appsFrame = getRequiredElement('apps-frame');
|
| + slider = new Slider(appsFrame, appsPageList, [], 0, appsFrame.offsetWidth);
|
| + slider.initialize();
|
| +
|
| + // Ensure the slider is resized appropriately with the window
|
| + window.addEventListener('resize', function() {
|
| + slider.resize(appsFrame.offsetWidth);
|
| + });
|
| +
|
| + // Handle the page being changed
|
| + appsPageList.addEventListener(
|
| + Slider.EventType.CARD_CHANGED,
|
| + function(e) {
|
| + // Update the active dot
|
| + var curDot = dotList.getElementsByClassName('selected')[0];
|
| + if (curDot)
|
| + curDot.classList.remove('selected');
|
| + var newPageIndex = e.slider.currentCard;
|
| + dots[newPageIndex].classList.add('selected');
|
| + // If an app was being dragged, move it to the end of the new page
|
| + if (draggingAppContainer)
|
| + appsPages[newPageIndex].appendChild(draggingAppContainer);
|
| + });
|
| +
|
| + // Add a drag handler to the body (for drags that don't land on an existing
|
| + // app)
|
| + document.addEventListener(Grabber.EventType.DRAG_ENTER, appDragEnter);
|
| +
|
| + // Handle dropping an app anywhere other than on the trash
|
| + document.addEventListener(Grabber.EventType.DROP, appDrop);
|
| +
|
| + // Add handles to manage the transition into/out-of rearrange mode
|
| + // Note that we assume here that we only use a Grabber for moving apps,
|
| + // so ANY GRAB event means we're enterring rearrange mode.
|
| + appsFrame.addEventListener(Grabber.EventType.GRAB, enterRearrangeMode);
|
| + appsFrame.addEventListener(Grabber.EventType.RELEASE, leaveRearrangeMode);
|
| +
|
| + // Add handlers for the tash can
|
| + trash.addEventListener(Grabber.EventType.DRAG_ENTER, function(e) {
|
| + trash.classList.add('hover');
|
| + e.grabbedElement.classList.add('trashing');
|
| + e.stopPropagation();
|
| + });
|
| + trash.addEventListener(Grabber.EventType.DRAG_LEAVE, function(e) {
|
| + e.grabbedElement.classList.remove('trashing');
|
| + trash.classList.remove('hover');
|
| + });
|
| + trash.addEventListener(Grabber.EventType.DROP, appTrash);
|
| + }
|
| +
|
| + /**
|
| + * Simple common assertion API
|
| + * @param {*} condition The condition to test. Note that this may be used to
|
| + * test whether a value is defined or not, and we don't want to force a
|
| + * cast to Boolean.
|
| + * @param {string=} opt_message A message to use in any error.
|
| + */
|
| + function assert(condition, opt_message) {
|
| + 'use strict';
|
| + if (!condition) {
|
| + var msg = 'Assertion failed';
|
| + if (opt_message)
|
| + msg = msg + ': ' + opt_message;
|
| + throw new Error(msg);
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Get an element that's known to exist by its ID. We use this instead of just
|
| + * calling getElementById and not checking the result because this lets us
|
| + * satisfy the JSCompiler type system.
|
| + * @param {string} id The identifier name.
|
| + * @return {!Element} the Element.
|
| + */
|
| + function getRequiredElement(id) {
|
| + var element = document.getElementById(id);
|
| + assert(element, 'Missing required element: ' + id);
|
| + return element;
|
| + }
|
| +
|
| + /**
|
| + * Remove all children of an element which have a given class in
|
| + * their classList.
|
| + * @param {!Element} element The parent element to examine.
|
| + * @param {string} className The class to look for.
|
| + */
|
| + function removeChildrenByClassName(element, className) {
|
| + for (var child = element.firstElementChild; child;) {
|
| + var prev = child;
|
| + child = child.nextElementSibling;
|
| + if (prev.classList.contains(className))
|
| + element.removeChild(prev);
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Callback invoked by chrome with the apps available.
|
| + *
|
| + * Note that calls to this function can occur at any time, not just in
|
| + * response to a getApps request. For example, when a user installs/uninstalls
|
| + * an app on another synchronized devices.
|
| + * @param {Object} data An object with all the data on available
|
| + * applications.
|
| + */
|
| + function getAppsCallback(data)
|
| + {
|
| + // Clean up any existing grabber objects - cancelling any outstanding drag.
|
| + // Ideally an async app update wouldn't disrupt an active drag but
|
| + // that would require us to re-use existing elements and detect how the apps
|
| + // have changed, which would be a lot of work.
|
| + // Note that we have to explicitly clean up the grabber objects so they stop
|
| + // listening to events and break the DOM<->JS cycles necessary to enable
|
| + // collection of all these objects.
|
| + grabbers.forEach(function(g) {
|
| + // Note that this may raise DRAG_END/RELEASE events to clean up an
|
| + // oustanding drag.
|
| + g.dispose();
|
| + });
|
| + assert(!draggingAppContainer && !draggingAppOriginalPosition &&
|
| + !draggingAppOriginalPage);
|
| + grabbers = [];
|
| + appEvents.removeAll();
|
| +
|
| + // Clear any existing apps pages and dots.
|
| + // TODO(rbyers): It might be nice to preserve animation of dots after an
|
| + // uninstall. Could we re-use the existing page and dot elements? It seems
|
| + // unfortunate to have Chrome send us the entire apps list after an
|
| + // uninstall.
|
| + removeChildrenByClassName(appsPageList, 'apps-page');
|
| + removeChildrenByClassName(dotList, 'dot');
|
| +
|
| + // Get the array of apps and add any special synthesized entries
|
| + var apps = data.apps;
|
| + apps.push(makeWebstoreApp());
|
| +
|
| + // Sort by launch index
|
| + apps.sort(function(a, b) {
|
| + return a.app_launch_index - b.app_launch_index;
|
| + });
|
| +
|
| + // Add the apps, creating pages as necessary
|
| + for (var i = 0; i < apps.length; i++) {
|
| + var app = apps[i];
|
| + var pageIndex = (app.page_index || 0);
|
| + while (pageIndex >= appsPages.length) {
|
| + var origPageCount = appsPages.length;
|
| + createAppPage();
|
| + // Confirm that appsPages is a live object, updated when a new page is
|
| + // added (otherwise we'd have an infinite loop)
|
| + assert(appsPages.length == origPageCount + 1, 'expected new page');
|
| + }
|
| + appendApp(appsPages[pageIndex], app);
|
| + }
|
| +
|
| + // Tell the slider about the pages
|
| + updateSliderCards();
|
| +
|
| + // Mark the current page
|
| + dots[slider.currentCard].classList.add('selected');
|
| + }
|
| +
|
| + /**
|
| + * Make a synthesized app object representing the chrome web store. It seems
|
| + * like this could just as easily come from the back-end, and then would
|
| + * support being rearranged, etc.
|
| + * @return {Object} The app object as would be sent from the webui back-end.
|
| + */
|
| + function makeWebstoreApp() {
|
| + return {
|
| + id: '', // Empty ID signifies this is a special synthesized app
|
| + page_index: 0,
|
| + app_launch_index: -1, // always first
|
| + name: templateData.web_store_title,
|
| + launch_url: templateData.web_store_url,
|
| + icon_big: getThemeUrl('IDR_WEBSTORE_ICON')
|
| + };
|
| + }
|
| +
|
| + /**
|
| + * Given a theme resource name, construct a URL for it.
|
| + * @param {string} resourceName The name of the resource.
|
| + * @return {string} A url which can be used to load the resource.
|
| + */
|
| + function getThemeUrl(resourceName) {
|
| + // Allow standalone_hack.js to hook this mapping (since chrome:// URLs
|
| + // won't work for a standalone page)
|
| + if (typeof themeUrlMapper == 'function') {
|
| + var u = themeUrlMapper(resourceName);
|
| + if (u)
|
| + return u;
|
| + }
|
| + return 'chrome://theme/' + resourceName;
|
| + }
|
| +
|
| + /**
|
| + * Callback invoked by chrome whenever an app preference changes.
|
| + * The normal NTP uses this to keep track of the current launch-type of an
|
| + * app, updating the choices in the context menu. We don't have such a menu
|
| + * so don't use this at all (but it still needs to be here for chrome to
|
| + * call).
|
| + * @param {Object} data An object with all the data on available
|
| + * applications.
|
| + */
|
| + function appsPrefChangeCallback(data) {
|
| + }
|
| +
|
| + /**
|
| + * Invoked whenever the pages in apps-page-list have changed so that
|
| + * the Slider knows about the new elements.
|
| + */
|
| + function updateSliderCards() {
|
| + var pageNo = slider.currentCard;
|
| + if (pageNo >= appsPages.length)
|
| + pageNo = appsPages.length - 1;
|
| + var pageArray = [];
|
| + for (var i = 0; i < appsPages.length; i++)
|
| + pageArray[i] = appsPages[i];
|
| + slider.setCards(pageArray, pageNo);
|
| + }
|
| +
|
| + /**
|
| + * Create a new app element and attach it to the end of the specified app
|
| + * page.
|
| + * @param {!Element} parent The element where the app should be inserted.
|
| + * @param {!Object} app The application object to create an app for.
|
| + */
|
| + function appendApp(parent, app) {
|
| + // Make a deep copy of the template and clear its ID
|
| + var containerElement = appTemplate.cloneNode(true);
|
| + var appElement = containerElement.getElementsByClassName('app')[0];
|
| + assert(appElement, 'Expected app-template to have an app child');
|
| + assert(typeof(app.id) == 'string',
|
| + 'Expected every app to have an ID or empty string');
|
| + appElement.setAttribute('app-id', app.id);
|
| +
|
| + // Find the span element (if any) and fill it in with the app name
|
| + var span = appElement.querySelector('span');
|
| + if (span)
|
| + span.textContent = app.name;
|
| +
|
| + // Fill in the image
|
| + // We use a mask of the same image so CSS rules can highlight just the image
|
| + // when it's touched.
|
| + var appImg = appElement.querySelector('img');
|
| + if (appImg) {
|
| + appImg.src = app.icon_big;
|
| + appImg.style.webkitMaskImage = url(app.icon_big);
|
| + // We put a click handler just on the app image - so clicking on the
|
| + // margins between apps doesn't do anything
|
| + if (app.id) {
|
| + appEvents.add(appImg, 'click', appClick, false);
|
| + } else {
|
| + // Special case of synthesized apps - can't launch directly so just
|
| + // change the URL as if we clicked a link. We may want to eventually
|
| + // support tracking clicks with ping messages, but really it seems it
|
| + // would be better for the back-end to just create virtual apps for such
|
| + // cases.
|
| + appEvents.add(appImg, 'click', function(e) {
|
| + window.location = app.launch_url;
|
| + }, false);
|
| + }
|
| + }
|
| +
|
| + // Only real apps with back-end storage (for their launch index, etc.) can
|
| + // be rearranged.
|
| + if (app.id) {
|
| + // Create a grabber to support moving apps around
|
| + // Note that we move the app rather than the container. This is so that an
|
| + // element remains in the original position so we can detect when an app
|
| + // is dropped in its starting location.
|
| + var grabber = new Grabber(appElement);
|
| + grabbers.push(grabber);
|
| +
|
| + // Register to be made aware of when we are dragged
|
| + appEvents.add(appElement, Grabber.EventType.DRAG_START, appDragStart,
|
| + false);
|
| + appEvents.add(appElement, Grabber.EventType.DRAG_END, appDragEnd,
|
| + false);
|
| +
|
| + // Register to be made aware of any app drags on top of our container
|
| + appEvents.add(containerElement, Grabber.EventType.DRAG_ENTER,
|
| + appDragEnter, false);
|
| + } else {
|
| + // Prevent any built-in drag-and-drop support from activating for the
|
| + // element.
|
| + appEvents.add(appElement, 'dragstart', function(e) {
|
| + e.preventDefault();
|
| + }, true);
|
| + }
|
| +
|
| + // Insert at the end of the provided page
|
| + parent.appendChild(containerElement);
|
| + }
|
| +
|
| + /**
|
| + * Creates a new page for apps
|
| + *
|
| + * @return {!Element} The apps-page element created.
|
| + * @param {boolean=} opt_animate If true, add the class 'new' to the created
|
| + * dot.
|
| + */
|
| + function createAppPage(opt_animate)
|
| + {
|
| + // Make a shallow copy of the app page template.
|
| + var newPage = appsPageTemplate.cloneNode(false);
|
| + appsPageList.appendChild(newPage);
|
| +
|
| + // Make a deep copy of the dot template to add a new one.
|
| + var dotCount = dots.length;
|
| + var newDot = dotTemplate.cloneNode(true);
|
| + if (opt_animate)
|
| + newDot.classList.add('new');
|
| + dotList.appendChild(newDot);
|
| +
|
| + // Add click handler to the dot to change the page.
|
| + // TODO(rbyers): Perhaps this should be TouchHandler.START_EVENT_ (so we
|
| + // don't rely on synthesized click events, and the change takes effect
|
| + // before releasing). However, click events seems to be synthesized for a
|
| + // region outside the border, and a 10px box is too small to require touch
|
| + // events to fall inside of. We could get around this by adding a box around
|
| + // the dot for accepting the touch events.
|
| + function switchPage(e) {
|
| + slider.selectCard(dotCount, true);
|
| + e.stopPropagation();
|
| + }
|
| + appEvents.add(newDot, 'click', switchPage, false);
|
| +
|
| + // Change pages whenever an app is dragged over a dot.
|
| + appEvents.add(newDot, Grabber.EventType.DRAG_ENTER, switchPage, false);
|
| +
|
| + return newPage;
|
| + }
|
| +
|
| + /**
|
| + * Invoked when an app is clicked
|
| + * @param {Event} e The click event.
|
| + */
|
| + function appClick(e) {
|
| + var target = e.currentTarget;
|
| + var app = getParentByClassName(target, 'app');
|
| + assert(app, 'appClick should have been on a descendant of an app');
|
| +
|
| + var appId = app.getAttribute('app-id');
|
| + assert(appId, 'unexpected app without appId');
|
| +
|
| + // Tell chrome to launch the app.
|
| + var NTP_APPS_MAXIMIZED = 0;
|
| + chrome.send('launchApp', [appId, NTP_APPS_MAXIMIZED]);
|
| +
|
| + // Don't allow the click to trigger a link or anything
|
| + e.preventDefault();
|
| + }
|
| +
|
| + /**
|
| + * Search an elements ancestor chain for the nearest element that is a member
|
| + * of the specified class.
|
| + * @param {!Element} element The element to start searching from.
|
| + * @param {string} className The name of the class to locate.
|
| + * @return {Element} The first ancestor of the specified class or null.
|
| + */
|
| + function getParentByClassName(element, className)
|
| + {
|
| + for (var e = element; e; e = e.parentElement) {
|
| + if (e.classList.contains(className))
|
| + return e;
|
| + }
|
| + return null;
|
| + }
|
| +
|
| + /**
|
| + * The container where the app currently being dragged came from.
|
| + * @type {!Element|undefined}
|
| + */
|
| + var draggingAppContainer;
|
| +
|
| + /**
|
| + * The apps-page that the app currently being dragged camed from.
|
| + * @type {!Element|undefined}
|
| + */
|
| + var draggingAppOriginalPage;
|
| +
|
| + /**
|
| + * The element that was originally after the app currently being dragged (or
|
| + * null if it was the last on the page).
|
| + * @type {!Element|undefined}
|
| + */
|
| + var draggingAppOriginalPosition;
|
| +
|
| + /**
|
| + * Invoked when app dragging begins.
|
| + * @param {Grabber.Event} e The event from the Grabber indicating the drag.
|
| + */
|
| + function appDragStart(e) {
|
| + // Pull the element out to the appsFrame using fixed positioning. This
|
| + // ensures that the app is not affected (remains under the finger) if the
|
| + // slider changes cards and is translated. An alternate approach would be
|
| + // to use fixed positioning for the slider (so that changes to its position
|
| + // don't affect children that aren't positioned relative to it), but we
|
| + // don't yet have GPU acceleration for this. Note that we use the appsFrame
|
| + var element = e.grabbedElement;
|
| +
|
| + var pos = element.getBoundingClientRect();
|
| + element.style.webkitTransform = '';
|
| +
|
| + element.style.position = 'fixed';
|
| + // Don't want to zoom around the middle since the left/top co-ordinates
|
| + // are post-transform values.
|
| + element.style.webkitTransformOrigin = 'left top';
|
| + element.style.left = pos.left + 'px';
|
| + element.style.top = pos.top + 'px';
|
| +
|
| + // Keep track of what app is being dragged and where it came from
|
| + assert(!draggingAppContainer, 'got DRAG_START without DRAG_END');
|
| + draggingAppContainer = element.parentNode;
|
| + assert(draggingAppContainer.classList.contains('app-container'));
|
| + draggingAppOriginalPosition = draggingAppContainer.nextSibling;
|
| + draggingAppOriginalPage = draggingAppContainer.parentNode;
|
| +
|
| + // Move the app out of the container
|
| + // Note that appendChild also removes the element from its current parent.
|
| + getRequiredElement('apps-frame').appendChild(element);
|
| + }
|
| +
|
| + /**
|
| + * Invoked when app dragging terminates (either successfully or not)
|
| + * @param {Grabber.Event} e The event from the Grabber.
|
| + */
|
| + function appDragEnd(e) {
|
| + // Stop floating the app
|
| + var appBeingDragged = e.grabbedElement;
|
| + assert(appBeingDragged.classList.contains('app'));
|
| + appBeingDragged.style.position = '';
|
| + appBeingDragged.style.webkitTransformOrigin = '';
|
| + appBeingDragged.style.left = '';
|
| + appBeingDragged.style.top = '';
|
| +
|
| + // Ensure the trash can is not active (we won't necessarily get a DRAG_LEAVE
|
| + // for it - eg. if we drop on it, or the drag is cancelled)
|
| + trash.classList.remove('hover');
|
| + appBeingDragged.classList.remove('trashing');
|
| +
|
| + // If we have an active drag (i.e. it wasn't aborted by an app update)
|
| + if (draggingAppContainer) {
|
| + // Put the app back into it's container
|
| + if (appBeingDragged.parentNode != draggingAppContainer)
|
| + draggingAppContainer.appendChild(appBeingDragged);
|
| +
|
| + // If we care about the container's original position
|
| + if (draggingAppOriginalPage)
|
| + {
|
| + // Then put the container back where it came from
|
| + if (draggingAppOriginalPosition) {
|
| + draggingAppOriginalPage.insertBefore(draggingAppContainer,
|
| + draggingAppOriginalPosition);
|
| + } else {
|
| + draggingAppOriginalPage.appendChild(draggingAppContainer);
|
| + }
|
| + }
|
| + }
|
| +
|
| + draggingAppContainer = undefined;
|
| + draggingAppOriginalPage = undefined;
|
| + draggingAppOriginalPosition = undefined;
|
| + }
|
| +
|
| + /**
|
| + * Invoked when an app is dragged over another app. Updates the DOM to affect
|
| + * the rearrangement (but doesn't commit the change until the app is dropped).
|
| + * @param {Grabber.Event} e The event from the Grabber indicating the drag.
|
| + */
|
| + function appDragEnter(e)
|
| + {
|
| + assert(draggingAppContainer, 'expected stored container');
|
| + var sourceContainer = draggingAppContainer;
|
| +
|
| + // Ensure enter events delivered to an app-container don't also get
|
| + // delivered to the document.
|
| + e.stopPropagation();
|
| +
|
| + var curPage = appsPages[slider.currentCard];
|
| + var followingContainer = null;
|
| +
|
| + // If we dragged over a specific app, determine which one to insert before
|
| + if (e.currentTarget != document) {
|
| +
|
| + // Start by assuming we'll insert the app before the one dragged over
|
| + followingContainer = e.currentTarget;
|
| + assert(followingContainer.classList.contains('app-container'),
|
| + 'expected drag over container');
|
| + assert(followingContainer.parentNode == curPage);
|
| + if (followingContainer == draggingAppContainer)
|
| + return;
|
| +
|
| + // But if it's after the current container position then we'll need to
|
| + // move ahead by one to account for the container being removed.
|
| + if (curPage == draggingAppContainer.parentNode) {
|
| + for (var c = draggingAppContainer; c; c = c.nextElementSibling) {
|
| + if (c == followingContainer) {
|
| + followingContainer = followingContainer.nextElementSibling;
|
| + break;
|
| + }
|
| + }
|
| + }
|
| + }
|
| +
|
| + // Move the container to the appropriate place on the page
|
| + curPage.insertBefore(draggingAppContainer, followingContainer);
|
| + }
|
| +
|
| + /**
|
| + * Invoked when an app is dropped on the trash
|
| + * @param {Grabber.Event} e The event from the Grabber indicating the drop.
|
| + */
|
| + function appTrash(e) {
|
| + var appElement = e.grabbedElement;
|
| + assert(appElement.classList.contains('app'));
|
| + var appId = appElement.getAttribute('app-id');
|
| + assert(appId);
|
| +
|
| + // Mark this drop as handled so that the catch-all drop handler
|
| + // on the document doesn't see this event.
|
| + e.stopPropagation();
|
| +
|
| + // Tell chrome to uninstall the app (prompting the user)
|
| + chrome.send('uninstallApp', [appId]);
|
| + }
|
| +
|
| + /**
|
| + * Called when an app is dropped anywhere other than the trash can. Commits
|
| + * any movement that has occurred.
|
| + * @param {Grabber.Event} e The event from the Grabber indicating the drop.
|
| + */
|
| + function appDrop(e) {
|
| + if (!draggingAppContainer)
|
| + // Drag was aborted (eg. due to an app update) - do nothing
|
| + return;
|
| +
|
| + // If the app is dropped back into it's original position then do nothing
|
| + assert(draggingAppOriginalPage);
|
| + if (draggingAppContainer.parentNode == draggingAppOriginalPage &&
|
| + draggingAppContainer.nextSibling == draggingAppOriginalPosition)
|
| + return;
|
| +
|
| + // Determine which app was being dragged
|
| + var appElement = e.grabbedElement;
|
| + assert(appElement.classList.contains('app'));
|
| + var appId = appElement.getAttribute('app-id');
|
| + assert(appId);
|
| +
|
| + // Update the page index for the app if it's changed. This doesn't trigger
|
| + // a call to getAppsCallback so we want to do it before reorderApps
|
| + var pageIndex = slider.currentCard;
|
| + assert(pageIndex >= 0 && pageIndex < appsPages.length,
|
| + 'page number out of range');
|
| + if (appsPages[pageIndex] != draggingAppOriginalPage)
|
| + chrome.send('setPageIndex', [appId, pageIndex]);
|
| +
|
| + // Put the app being dragged back into it's container
|
| + draggingAppContainer.appendChild(appElement);
|
| +
|
| + // Create a list of all appIds in the order now present in the DOM
|
| + var appIds = [];
|
| + for (var page = 0; page < appsPages.length; page++) {
|
| + var appsOnPage = appsPages[page].getElementsByClassName('app');
|
| + for (var i = 0; i < appsOnPage.length; i++) {
|
| + var id = appsOnPage[i].getAttribute('app-id');
|
| + if (id)
|
| + appIds.push(id);
|
| + }
|
| + }
|
| +
|
| + // We are going to commit this repositioning - clear the original position
|
| + draggingAppOriginalPage = undefined;
|
| + draggingAppOriginalPosition = undefined;
|
| +
|
| + // Tell chrome to update its database to persist this new order of apps This
|
| + // will cause getAppsCallback to be invoked and the apps to be redrawn.
|
| + chrome.send('reorderApps', [appId, appIds]);
|
| + appMoved = true;
|
| + }
|
| +
|
| + /**
|
| + * Set to true if we're currently in rearrange mode and an app has
|
| + * been successfully dropped to a new location. This indicates that
|
| + * a getAppsCallback call is pending and we can rely on the DOM being
|
| + * updated by that.
|
| + * @type {boolean}
|
| + */
|
| + var appMoved = false;
|
| +
|
| + /**
|
| + * Invoked whenever some app is grabbed
|
| + * @param {Grabber.Event} e The Grabber Grab event.
|
| + */
|
| + function enterRearrangeMode(e)
|
| + {
|
| + // Stop the slider from sliding for this touch
|
| + slider.cancelTouch();
|
| +
|
| + // Add an extra blank page in case the user wants to create a new page
|
| + createAppPage(true);
|
| + var pageAdded = appsPages.length - 1;
|
| + window.setTimeout(function() {
|
| + dots[pageAdded].classList.remove('new');
|
| + }, 0);
|
| +
|
| + updateSliderCards();
|
| +
|
| + // Cause the dot-list to grow
|
| + getRequiredElement('footer').classList.add('rearrange-mode');
|
| +
|
| + assert(!appMoved, 'appMoved should not be set yet');
|
| + }
|
| +
|
| + /**
|
| + * Invoked whenever some app is released
|
| + * @param {Grabber.Event} e The Grabber RELEASE event.
|
| + */
|
| + function leaveRearrangeMode(e)
|
| + {
|
| + // Return the dot-list to normal
|
| + getRequiredElement('footer').classList.remove('rearrange-mode');
|
| +
|
| + // If we didn't successfully re-arrange an app, then we won't be
|
| + // refreshing the app view in getAppCallback and need to explicitly remove
|
| + // the extra empty page we added. We don't want to do this in the normal
|
| + // case because if we did actually drop an app there, we want to retain that
|
| + // page as our current page number.
|
| + if (!appMoved) {
|
| + assert(appsPages[appsPages.length - 1].
|
| + getElementsByClassName('app-container').length == 0,
|
| + 'Last app page should be empty');
|
| + removePage(appsPages.length - 1);
|
| + }
|
| + appMoved = false;
|
| + }
|
| +
|
| + /**
|
| + * Remove the page with the specified index and update the slider.
|
| + * @param {number} pageNo The index of the page to remove.
|
| + */
|
| + function removePage(pageNo)
|
| + {
|
| + var page = appsPages[pageNo];
|
| +
|
| + // Remove the page from the DOM
|
| + page.parentNode.removeChild(page);
|
| +
|
| + // Remove the corresponding dot
|
| + // Need to give it a chance to animate though
|
| + var dot = dots[pageNo];
|
| + dot.classList.add('new');
|
| + window.setTimeout(function() {
|
| + // If we've re-created the apps (eg. because an app was uninstalled) then
|
| + // we will have removed the old dots from the document already, so skip.
|
| + if (dot.parentNode)
|
| + dot.parentNode.removeChild(dot);
|
| + }, DEFAULT_TRANSITION_TIME);
|
| +
|
| + updateSliderCards();
|
| + }
|
| +
|
| + // Return an object with all the exports
|
| + return {
|
| + assert: assert,
|
| + appsPrefChangeCallback: appsPrefChangeCallback,
|
| + getAppsCallback: getAppsCallback,
|
| + initialize: initializeNtp
|
| + };
|
| +})();
|
| +
|
| +// publish ntp globals
|
| +var assert = ntp.assert;
|
| +var getAppsCallback = ntp.getAppsCallback;
|
| +var appsPrefChangeCallback = ntp.appsPrefChangeCallback;
|
| +
|
| +// Initialize immediately once globals are published (there doesn't seem to be
|
| +// any need to wait for DOMContentLoaded)
|
| +ntp.initialize();
|
|
|