Home | History | Annotate | Download | only in ntp4
      1 // Copyright (c) 2012 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 cr.define('ntp', function() {
      6   'use strict';
      7 
      8   // We can't pass the currently dragging tile via dataTransfer because of
      9   // http://crbug.com/31037
     10   var currentlyDraggingTile = null;
     11   function getCurrentlyDraggingTile() {
     12     return currentlyDraggingTile;
     13   }
     14   function setCurrentlyDraggingTile(tile) {
     15     currentlyDraggingTile = tile;
     16     if (tile)
     17       ntp.enterRearrangeMode();
     18     else
     19       ntp.leaveRearrangeMode();
     20   }
     21 
     22   /**
     23    * Changes the current dropEffect of a drag. This modifies the native cursor
     24    * and serves as an indicator of what we should do at the end of the drag as
     25    * well as give indication to the user if a drop would succeed if they let go.
     26    * @param {DataTransfer} dataTransfer A dataTransfer object from a drag event.
     27    * @param {string} effect A drop effect to change to (i.e. copy, move, none).
     28    */
     29   function setCurrentDropEffect(dataTransfer, effect) {
     30     dataTransfer.dropEffect = effect;
     31     if (currentlyDraggingTile)
     32       currentlyDraggingTile.lastDropEffect = dataTransfer.dropEffect;
     33   }
     34 
     35   /**
     36    * Creates a new Tile object. Tiles wrap content on a TilePage, providing
     37    * some styling and drag functionality.
     38    * @constructor
     39    * @extends {HTMLDivElement}
     40    */
     41   function Tile(contents) {
     42     var tile = cr.doc.createElement('div');
     43     tile.__proto__ = Tile.prototype;
     44     tile.initialize(contents);
     45 
     46     return tile;
     47   }
     48 
     49   Tile.prototype = {
     50     __proto__: HTMLDivElement.prototype,
     51 
     52     initialize: function(contents) {
     53       // 'real' as opposed to doppleganger.
     54       this.className = 'tile real';
     55       this.appendChild(contents);
     56       contents.tile = this;
     57 
     58       this.addEventListener('dragstart', this.onDragStart_);
     59       this.addEventListener('drag', this.onDragMove_);
     60       this.addEventListener('dragend', this.onDragEnd_);
     61 
     62       this.firstChild.addEventListener(
     63           'webkitAnimationEnd', this.onContentsAnimationEnd_.bind(this));
     64 
     65       this.eventTracker = new EventTracker();
     66     },
     67 
     68     get index() {
     69       return Array.prototype.indexOf.call(this.tilePage.tileElements_, this);
     70     },
     71 
     72     get tilePage() {
     73       return findAncestorByClass(this, 'tile-page');
     74     },
     75 
     76     /**
     77      * Position the tile at |x, y|, and store this as the grid location, i.e.
     78      * where the tile 'belongs' when it's not being dragged.
     79      * @param {number} x The x coordinate, in pixels.
     80      * @param {number} y The y coordinate, in pixels.
     81      */
     82     setGridPosition: function(x, y) {
     83       this.gridX = x;
     84       this.gridY = y;
     85       this.moveTo(x, y);
     86     },
     87 
     88     /**
     89      * Position the tile at |x, y|.
     90      * @param {number} x The x coordinate, in pixels.
     91      * @param {number} y The y coordinate, in pixels.
     92      */
     93     moveTo: function(x, y) {
     94       // left overrides right in LTR, and right takes precedence in RTL.
     95       this.style.left = toCssPx(x);
     96       this.style.right = toCssPx(x);
     97       this.style.top = toCssPx(y);
     98     },
     99 
    100     /**
    101      * The handler for dragstart events fired on |this|.
    102      * @param {Event} e The event for the drag.
    103      * @private
    104      */
    105     onDragStart_: function(e) {
    106       // The user may start dragging again during a previous drag's finishing
    107       // animation.
    108       if (this.classList.contains('dragging'))
    109         this.finalizeDrag_();
    110 
    111       setCurrentlyDraggingTile(this);
    112 
    113       e.dataTransfer.effectAllowed = 'copyMove';
    114       this.firstChild.setDragData(e.dataTransfer);
    115 
    116       // The drag clone is the node we use as a representation during the drag.
    117       // It's attached to the top level document element so that it floats above
    118       // image masks.
    119       this.dragClone = this.cloneNode(true);
    120       this.dragClone.style.right = '';
    121       this.dragClone.classList.add('drag-representation');
    122       $('card-slider-frame').appendChild(this.dragClone);
    123       this.eventTracker.add(this.dragClone, 'webkitTransitionEnd',
    124                             this.onDragCloneTransitionEnd_.bind(this));
    125 
    126       this.classList.add('dragging');
    127       // offsetLeft is mirrored in RTL. Un-mirror it.
    128       var offsetLeft = isRTL() ?
    129           this.parentNode.clientWidth - this.offsetLeft :
    130           this.offsetLeft;
    131       this.dragOffsetX = e.x - offsetLeft - this.parentNode.offsetLeft;
    132       this.dragOffsetY = e.y - this.offsetTop -
    133           // Unlike offsetTop, this value takes scroll position into account.
    134           this.parentNode.getBoundingClientRect().top;
    135 
    136       this.onDragMove_(e);
    137     },
    138 
    139     /**
    140      * The handler for drag events fired on |this|.
    141      * @param {Event} e The event for the drag.
    142      * @private
    143      */
    144     onDragMove_: function(e) {
    145       if (e.view != window || (e.x == 0 && e.y == 0)) {
    146         this.dragClone.hidden = true;
    147         return;
    148       }
    149 
    150       this.dragClone.hidden = false;
    151       this.dragClone.style.left = toCssPx(e.x - this.dragOffsetX);
    152       this.dragClone.style.top = toCssPx(e.y - this.dragOffsetY);
    153     },
    154 
    155     /**
    156      * The handler for dragend events fired on |this|.
    157      * @param {Event} e The event for the drag.
    158      * @private
    159      */
    160     onDragEnd_: function(e) {
    161       this.dragClone.hidden = false;
    162       this.dragClone.classList.add('placing');
    163 
    164       setCurrentlyDraggingTile(null);
    165 
    166       // tilePage will be null if we've already been removed.
    167       var tilePage = this.tilePage;
    168       if (tilePage)
    169         tilePage.positionTile_(this.index);
    170 
    171       // Take an appropriate action with the drag clone.
    172       if (this.landedOnTrash) {
    173         this.dragClone.classList.add('deleting');
    174       } else if (tilePage) {
    175         // TODO(dbeam): Until we fix dropEffect to the correct behavior it will
    176         // differ on windows - crbug.com/39399.  That's why we use the custom
    177         // this.lastDropEffect instead of e.dataTransfer.dropEffect.
    178         if (tilePage.selected && this.lastDropEffect != 'copy') {
    179           // The drag clone can still be hidden from the last drag move event.
    180           this.dragClone.hidden = false;
    181           // The tile's contents may have moved following the respositioning;
    182           // adjust for that.
    183           var contentDiffX = this.dragClone.firstChild.offsetLeft -
    184               this.firstChild.offsetLeft;
    185           var contentDiffY = this.dragClone.firstChild.offsetTop -
    186               this.firstChild.offsetTop;
    187           this.dragClone.style.left =
    188               toCssPx(this.gridX + this.parentNode.offsetLeft -
    189                          contentDiffX);
    190           this.dragClone.style.top =
    191               toCssPx(this.gridY +
    192                          this.parentNode.getBoundingClientRect().top -
    193                          contentDiffY);
    194         } else if (this.dragClone.hidden) {
    195           this.finalizeDrag_();
    196         } else {
    197           // The CSS3 transitions spec intentionally leaves it up to individual
    198           // user agents to determine when styles should be applied. On some
    199           // platforms (at the moment, Windows), when you apply both classes
    200           // immediately a transition may not occur correctly. That's why we're
    201           // using a setTimeout here to queue adding the class until the
    202           // previous class (currently: .placing) sets up a transition.
    203           // http://dev.w3.org/csswg/css3-transitions/#starting
    204           window.setTimeout(function() {
    205             if (this.dragClone)
    206               this.dragClone.classList.add('dropped-on-other-page');
    207           }.bind(this), 0);
    208         }
    209       }
    210 
    211       delete this.lastDropEffect;
    212       this.landedOnTrash = false;
    213     },
    214 
    215     /**
    216      * Creates a clone of this node offset by the coordinates. Used for the
    217      * dragging effect where a tile appears to float off one side of the grid
    218      * and re-appear on the other.
    219      * @param {number} x x-axis offset, in pixels.
    220      * @param {number} y y-axis offset, in pixels.
    221      */
    222     showDoppleganger: function(x, y) {
    223       // We always have to clear the previous doppleganger to make sure we get
    224       // style updates for the contents of this tile.
    225       this.clearDoppleganger();
    226 
    227       var clone = this.cloneNode(true);
    228       clone.classList.remove('real');
    229       clone.classList.add('doppleganger');
    230       var clonelets = clone.querySelectorAll('.real');
    231       for (var i = 0; i < clonelets.length; i++) {
    232         clonelets[i].classList.remove('real');
    233       }
    234 
    235       this.appendChild(clone);
    236       this.doppleganger_ = clone;
    237 
    238       if (isRTL())
    239         x *= -1;
    240 
    241       this.doppleganger_.style.WebkitTransform = 'translate(' + x + 'px, ' +
    242                                                                 y + 'px)';
    243     },
    244 
    245     /**
    246      * Destroys the current doppleganger.
    247      */
    248     clearDoppleganger: function() {
    249       if (this.doppleganger_) {
    250         this.removeChild(this.doppleganger_);
    251         this.doppleganger_ = null;
    252       }
    253     },
    254 
    255     /**
    256      * Returns status of doppleganger.
    257      * @return {boolean} True if there is a doppleganger showing for |this|.
    258      */
    259     hasDoppleganger: function() {
    260       return !!this.doppleganger_;
    261     },
    262 
    263     /**
    264      * Cleans up after the drag is over. This is either called when the
    265      * drag representation finishes animating to the final position, or when
    266      * the next drag starts (if the user starts a 2nd drag very quickly).
    267      * @private
    268      */
    269     finalizeDrag_: function() {
    270       assert(this.classList.contains('dragging'));
    271 
    272       var clone = this.dragClone;
    273       this.dragClone = null;
    274 
    275       clone.parentNode.removeChild(clone);
    276       this.eventTracker.remove(clone, 'webkitTransitionEnd');
    277       this.classList.remove('dragging');
    278       if (this.firstChild.finalizeDrag)
    279         this.firstChild.finalizeDrag();
    280     },
    281 
    282     /**
    283      * Called when the drag representation node is done migrating to its final
    284      * resting spot.
    285      * @param {Event} e The transition end event.
    286      */
    287     onDragCloneTransitionEnd_: function(e) {
    288       if (this.classList.contains('dragging') &&
    289           (e.propertyName == 'left' || e.propertyName == 'top' ||
    290            e.propertyName == '-webkit-transform')) {
    291         this.finalizeDrag_();
    292       }
    293     },
    294 
    295     /**
    296      * Called when an app is removed from Chrome. Animates its disappearance.
    297      * @param {boolean=} opt_animate Whether the animation should be animated.
    298      */
    299     doRemove: function(opt_animate) {
    300       if (opt_animate)
    301         this.firstChild.classList.add('removing-tile-contents');
    302       else
    303         this.tilePage.removeTile(this, false);
    304     },
    305 
    306     /**
    307      * Callback for the webkitAnimationEnd event on the tile's contents.
    308      * @param {Event} e The event object.
    309      */
    310     onContentsAnimationEnd_: function(e) {
    311       if (this.firstChild.classList.contains('new-tile-contents'))
    312         this.firstChild.classList.remove('new-tile-contents');
    313       if (this.firstChild.classList.contains('removing-tile-contents'))
    314         this.tilePage.removeTile(this, true);
    315     },
    316   };
    317 
    318   /**
    319    * Gives the proportion of the row width that is devoted to a single icon.
    320    * @param {number} rowTileCount The number of tiles in a row.
    321    * @param {number} tileSpacingFraction The proportion of the tile width which
    322    *     will be used as spacing between tiles.
    323    * @return {number} The ratio between icon width and row width.
    324    */
    325   function tileWidthFraction(rowTileCount, tileSpacingFraction) {
    326     return rowTileCount + (rowTileCount - 1) * tileSpacingFraction;
    327   }
    328 
    329   /**
    330    * Calculates an assortment of tile-related values for a grid with the
    331    * given dimensions.
    332    * @param {number} width The pixel width of the grid.
    333    * @param {number} numRowTiles The number of tiles in a row.
    334    * @param {number} tileSpacingFraction The proportion of the tile width which
    335    *     will be used as spacing between tiles.
    336    * @return {Object} A mapping of pixel values.
    337    */
    338   function tileValuesForGrid(width, numRowTiles, tileSpacingFraction) {
    339     var tileWidth = width / tileWidthFraction(numRowTiles, tileSpacingFraction);
    340     var offsetX = tileWidth * (1 + tileSpacingFraction);
    341     var interTileSpacing = offsetX - tileWidth;
    342 
    343     return {
    344       tileWidth: tileWidth,
    345       offsetX: offsetX,
    346       interTileSpacing: interTileSpacing,
    347     };
    348   }
    349 
    350   // The smallest amount of horizontal blank space to display on the sides when
    351   // displaying a wide arrangement. There is an additional 26px of margin from
    352   // the tile page padding.
    353   var MIN_WIDE_MARGIN = 18;
    354 
    355   /**
    356    * Creates a new TilePage object. This object contains tiles and controls
    357    * their layout.
    358    * @param {Object} gridValues Pixel values that define the size and layout
    359    *     of the tile grid.
    360    * @constructor
    361    * @extends {HTMLDivElement}
    362    */
    363   function TilePage(gridValues) {
    364     var el = cr.doc.createElement('div');
    365     el.gridValues_ = gridValues;
    366     el.__proto__ = TilePage.prototype;
    367     el.initialize();
    368 
    369     return el;
    370   }
    371 
    372   /**
    373    * Takes a collection of grid layout pixel values and updates them with
    374    * additional tiling values that are calculated from TilePage constants.
    375    * @param {Object} grid The grid layout pixel values to update.
    376    */
    377   TilePage.initGridValues = function(grid) {
    378     // The amount of space we need to display a narrow grid (all narrow grids
    379     // are this size).
    380     grid.narrowWidth =
    381         grid.minTileWidth * tileWidthFraction(grid.minColCount,
    382                                               grid.tileSpacingFraction);
    383     // The minimum amount of space we need to display a wide grid.
    384     grid.minWideWidth =
    385         grid.minTileWidth * tileWidthFraction(grid.maxColCount,
    386                                               grid.tileSpacingFraction);
    387     // The largest we will ever display a wide grid.
    388     grid.maxWideWidth =
    389         grid.maxTileWidth * tileWidthFraction(grid.maxColCount,
    390                                               grid.tileSpacingFraction);
    391     // Tile-related pixel values for the narrow display.
    392     grid.narrowTileValues = tileValuesForGrid(grid.narrowWidth,
    393                                               grid.minColCount,
    394                                               grid.tileSpacingFraction);
    395     // Tile-related pixel values for the minimum narrow display.
    396     grid.wideTileValues = tileValuesForGrid(grid.minWideWidth,
    397                                             grid.maxColCount,
    398                                             grid.tileSpacingFraction);
    399   };
    400 
    401   TilePage.prototype = {
    402     __proto__: HTMLDivElement.prototype,
    403 
    404     initialize: function() {
    405       this.className = 'tile-page';
    406 
    407       // Div that acts as a custom scrollbar. The scrollbar has to live
    408       // outside the content div so it doesn't flicker when scrolling (due to
    409       // repainting after the scroll, then repainting again when moved in the
    410       // onScroll handler). |scrollbar_| is only aesthetic, and it only
    411       // represents the thumb. Actual events are still handled by the invisible
    412       // native scrollbars. This div gives us more flexibility with the visuals.
    413       this.scrollbar_ = this.ownerDocument.createElement('div');
    414       this.scrollbar_.className = 'tile-page-scrollbar';
    415       this.scrollbar_.hidden = true;
    416       this.appendChild(this.scrollbar_);
    417 
    418       // This contains everything but the scrollbar.
    419       this.content_ = this.ownerDocument.createElement('div');
    420       this.content_.className = 'tile-page-content';
    421       this.appendChild(this.content_);
    422 
    423       // Div that sets the vertical position of the tile grid.
    424       this.topMargin_ = this.ownerDocument.createElement('div');
    425       this.topMargin_.className = 'top-margin';
    426       this.content_.appendChild(this.topMargin_);
    427 
    428       // Div that holds the tiles.
    429       this.tileGrid_ = this.ownerDocument.createElement('div');
    430       this.tileGrid_.className = 'tile-grid';
    431       this.tileGrid_.style.minWidth = this.gridValues_.narrowWidth + 'px';
    432       this.tileGrid_.setAttribute('role', 'menu');
    433       this.tileGrid_.setAttribute('aria-label',
    434           loadTimeData.getString(
    435               'tile_grid_screenreader_accessible_description'));
    436 
    437       this.content_.appendChild(this.tileGrid_);
    438 
    439       // Ordered list of our tiles.
    440       this.tileElements_ = this.tileGrid_.getElementsByClassName('tile real');
    441       // Ordered list of the elements which want to accept keyboard focus. These
    442       // elements will not be a part of the normal tab order; the tile grid
    443       // initially gets focused and then these elements can be focused via the
    444       // arrow keys.
    445       this.focusableElements_ =
    446           this.tileGrid_.getElementsByClassName('focusable');
    447 
    448       // These are properties used in updateTopMargin.
    449       this.animatedTopMarginPx_ = 0;
    450       this.topMarginPx_ = 0;
    451 
    452       this.eventTracker = new EventTracker();
    453       this.eventTracker.add(window, 'resize', this.onResize_.bind(this));
    454 
    455       this.addEventListener('DOMNodeInsertedIntoDocument',
    456                             this.onNodeInsertedIntoDocument_);
    457 
    458       this.content_.addEventListener('scroll', this.onScroll_.bind(this));
    459 
    460       this.dragWrapper_ = new cr.ui.DragWrapper(this.tileGrid_, this);
    461 
    462       this.addEventListener('cardselected', this.handleCardSelection_);
    463       this.addEventListener('carddeselected', this.handleCardDeselection_);
    464       this.addEventListener('focus', this.handleFocus_);
    465       this.addEventListener('keydown', this.handleKeyDown_);
    466       this.addEventListener('mousedown', this.handleMouseDown_);
    467 
    468       this.focusElementIndex_ = -1;
    469     },
    470 
    471     get tiles() {
    472       return this.tileElements_;
    473     },
    474 
    475     get tileCount() {
    476       return this.tileElements_.length;
    477     },
    478 
    479     get selected() {
    480       return Array.prototype.indexOf.call(this.parentNode.children, this) ==
    481           ntp.getCardSlider().currentCard;
    482     },
    483 
    484     /**
    485      * The size of the margin (unused space) on the sides of the tile grid, in
    486      * pixels.
    487      * @type {number}
    488      */
    489     get sideMargin() {
    490       return this.layoutValues_.leftMargin;
    491     },
    492 
    493     /**
    494      * Returns the width of the scrollbar, in pixels, if it is active, or 0
    495      * otherwise.
    496      * @type {number}
    497      */
    498     get scrollbarWidth() {
    499       return this.scrollbar_.hidden ? 0 : 13;
    500     },
    501 
    502     /**
    503      * Returns any extra padding to insert to the bottom of a tile page.  By
    504      * default there is none, but subclasses can override.
    505      * @type {number}
    506      */
    507     get extraBottomPadding() {
    508       return 0;
    509     },
    510 
    511     /**
    512      * The notification content of this tile (if any, otherwise null).
    513      * @type {!HTMLElement}
    514      */
    515     get notification() {
    516       return this.topMargin_.nextElementSibling.id == 'notification-container' ?
    517           this.topMargin_.nextElementSibling : null;
    518     },
    519     /**
    520      * The notification content of this tile (if any, otherwise null).
    521      * @type {!HTMLElement}
    522      */
    523     set notification(node) {
    524       assert(node instanceof HTMLElement, '|node| isn\'t an HTMLElement!');
    525       // NOTE: Implicitly removes from DOM if |node| is inside it.
    526       this.content_.insertBefore(node, this.topMargin_.nextElementSibling);
    527       this.positionNotification_();
    528     },
    529 
    530     /**
    531      * Fetches the size, in pixels, of the padding-top of the tile contents.
    532      * @type {number}
    533      */
    534     get contentPadding() {
    535       if (typeof this.contentPadding_ == 'undefined') {
    536         this.contentPadding_ =
    537             parseInt(getComputedStyle(this.content_).paddingTop, 10);
    538       }
    539       return this.contentPadding_;
    540     },
    541 
    542     /**
    543      * Removes the tilePage from the DOM and cleans up event handlers.
    544      */
    545     remove: function() {
    546       // This checks arguments.length as most remove functions have a boolean
    547       // |opt_animate| argument, but that's not necesarilly applicable to
    548       // removing a tilePage. Selecting a different card in an animated way and
    549       // deleting the card afterward is probably a better choice.
    550       assert(typeof arguments[0] != 'boolean',
    551              'This function takes no |opt_animate| argument.');
    552       this.tearDown_();
    553       this.parentNode.removeChild(this);
    554     },
    555 
    556     /**
    557      * Cleans up resources that are no longer needed after this TilePage
    558      * instance is removed from the DOM.
    559      * @private
    560      */
    561     tearDown_: function() {
    562       this.eventTracker.removeAll();
    563     },
    564 
    565     /**
    566      * Appends a tile to the end of the tile grid.
    567      * @param {HTMLElement} tileElement The contents of the tile.
    568      * @param {boolean} animate If true, the append will be animated.
    569      * @protected
    570      */
    571     appendTile: function(tileElement, animate) {
    572       this.addTileAt(tileElement, this.tileElements_.length, animate);
    573     },
    574 
    575     /**
    576      * Adds the given element to the tile grid.
    577      * @param {Node} tileElement The tile object/node to insert.
    578      * @param {number} index The location in the tile grid to insert it at.
    579      * @param {boolean} animate If true, the tile in question will be
    580      *     animated (other tiles, if they must reposition, do not animate).
    581      * @protected
    582      */
    583     addTileAt: function(tileElement, index, animate) {
    584       this.classList.remove('animating-tile-page');
    585       if (animate)
    586         tileElement.classList.add('new-tile-contents');
    587 
    588       // Make sure the index is positive and either in the the bounds of
    589       // this.tileElements_ or at the end (meaning append).
    590       assert(index >= 0 && index <= this.tileElements_.length);
    591 
    592       var wrapperDiv = new Tile(tileElement);
    593       // If is out of the bounds of the tile element list, .insertBefore() will
    594       // act just like appendChild().
    595       this.tileGrid_.insertBefore(wrapperDiv, this.tileElements_[index]);
    596       this.calculateLayoutValues_();
    597       this.heightChanged_();
    598 
    599       this.repositionTiles_();
    600 
    601       // If this is the first tile being added, make it focusable after add.
    602       if (this.focusableElements_.length == 1)
    603         this.updateFocusableElement();
    604       this.fireAddedEvent(wrapperDiv, index, animate);
    605     },
    606 
    607     /**
    608      * Notify interested subscribers that a tile has been removed from this
    609      * page.
    610      * @param {Tile} tile The newly added tile.
    611      * @param {number} index The index of the tile that was added.
    612      * @param {boolean} wasAnimated Whether the removal was animated.
    613      */
    614     fireAddedEvent: function(tile, index, wasAnimated) {
    615       var e = document.createEvent('Event');
    616       e.initEvent('tilePage:tile_added', true, true);
    617       e.addedIndex = index;
    618       e.addedTile = tile;
    619       e.wasAnimated = wasAnimated;
    620       this.dispatchEvent(e);
    621     },
    622 
    623     /**
    624      * Removes the given tile and animates the repositioning of the other tiles.
    625      * @param {boolean=} opt_animate Whether the removal should be animated.
    626      * @param {boolean=} opt_dontNotify Whether a page should be removed if the
    627      *     last tile is removed from it.
    628      */
    629     removeTile: function(tile, opt_animate, opt_dontNotify) {
    630       if (opt_animate)
    631         this.classList.add('animating-tile-page');
    632 
    633       var index = tile.index;
    634       tile.parentNode.removeChild(tile);
    635       this.calculateLayoutValues_();
    636       this.cleanupDrag();
    637       this.updateFocusableElement();
    638 
    639       if (!opt_dontNotify)
    640         this.fireRemovedEvent(tile, index, !!opt_animate);
    641     },
    642 
    643     /**
    644      * Notify interested subscribers that a tile has been removed from this
    645      * page.
    646      * @param {Tile} tile The tile that was removed.
    647      * @param {number} oldIndex Where the tile was positioned before removal.
    648      * @param {boolean} wasAnimated Whether the removal was animated.
    649      */
    650     fireRemovedEvent: function(tile, oldIndex, wasAnimated) {
    651       var e = document.createEvent('Event');
    652       e.initEvent('tilePage:tile_removed', true, true);
    653       e.removedIndex = oldIndex;
    654       e.removedTile = tile;
    655       e.wasAnimated = wasAnimated;
    656       this.dispatchEvent(e);
    657     },
    658 
    659     /**
    660      * Removes all tiles from the page.
    661      */
    662     removeAllTiles: function() {
    663       this.tileGrid_.innerHTML = '';
    664     },
    665 
    666     /**
    667      * Called when the page is selected (in the card selector).
    668      * @param {Event} e A custom cardselected event.
    669      * @private
    670      */
    671     handleCardSelection_: function(e) {
    672       this.updateFocusableElement();
    673 
    674       // When we are selected, we re-calculate the layout values. (See comment
    675       // in doDrop.)
    676       this.calculateLayoutValues_();
    677     },
    678 
    679     /**
    680      * Called when the page loses selection (in the card selector).
    681      * @param {Event} e A custom carddeselected event.
    682      * @private
    683      */
    684     handleCardDeselection_: function(e) {
    685       if (this.currentFocusElement_)
    686         this.currentFocusElement_.tabIndex = -1;
    687     },
    688 
    689     /**
    690      * When we get focus, pass it on to the focus element.
    691      * @param {Event} e The focus event.
    692      * @private
    693      */
    694     handleFocus_: function(e) {
    695       if (this.focusableElements_.length == 0)
    696         return;
    697 
    698       this.updateFocusElement_();
    699     },
    700 
    701     /**
    702      * Since we are doing custom focus handling, we have to manually
    703      * set focusability on click (as well as keyboard nav above).
    704      * @param {Event} e The focus event.
    705      * @private
    706      */
    707     handleMouseDown_: function(e) {
    708       var focusable = findAncestorByClass(e.target, 'focusable');
    709       if (focusable) {
    710         this.focusElementIndex_ =
    711             Array.prototype.indexOf.call(this.focusableElements_,
    712                                          focusable);
    713         this.updateFocusElement_();
    714       } else {
    715         // This prevents the tile page from getting focus when the user clicks
    716         // inside the grid but outside of any tile.
    717         e.preventDefault();
    718       }
    719     },
    720 
    721     /**
    722      * Handle arrow key focus nav.
    723      * @param {Event} e The focus event.
    724      * @private
    725      */
    726     handleKeyDown_: function(e) {
    727       // We only handle up, down, left, right without control keys.
    728       if (e.metaKey || e.shiftKey || e.altKey || e.ctrlKey)
    729         return;
    730 
    731       // Wrap the given index to |this.focusableElements_|.
    732       var wrap = function(idx) {
    733         return (idx + this.focusableElements_.length) %
    734             this.focusableElements_.length;
    735       }.bind(this);
    736 
    737       switch (e.keyIdentifier) {
    738         case 'Right':
    739         case 'Left':
    740           var direction = e.keyIdentifier == 'Right' ? 1 : -1;
    741           this.focusElementIndex_ = wrap(this.focusElementIndex_ + direction);
    742           break;
    743         case 'Up':
    744         case 'Down':
    745           // Look through all focusable elements. Find the first one that is
    746           // in the same column.
    747           var direction = e.keyIdentifier == 'Up' ? -1 : 1;
    748           var currentIndex =
    749               Array.prototype.indexOf.call(this.focusableElements_,
    750                                            this.currentFocusElement_);
    751           var newFocusIdx = wrap(currentIndex + direction);
    752           var tile = this.currentFocusElement_.parentNode;
    753           for (;; newFocusIdx = wrap(newFocusIdx + direction)) {
    754             var newTile = this.focusableElements_[newFocusIdx].parentNode;
    755             var rowTiles = this.layoutValues_.numRowTiles;
    756             if ((newTile.index - tile.index) % rowTiles == 0)
    757               break;
    758           }
    759 
    760           this.focusElementIndex_ = newFocusIdx;
    761           break;
    762 
    763         default:
    764           return;
    765       }
    766 
    767       this.updateFocusElement_();
    768 
    769       e.preventDefault();
    770       e.stopPropagation();
    771     },
    772 
    773     /**
    774      * Ensure 0 <= this.focusElementIndex_ < this.focusableElements_.length,
    775      * make the focusable element at this.focusElementIndex_ (if any) eligible
    776      * for tab focus, and the previously-focused element not eligible.
    777      * @protected
    778      */
    779     updateFocusableElement: function() {
    780       if (this.focusableElements_.length == 0 || !this.selected) {
    781         this.focusElementIndex_ = -1;
    782         return;
    783       }
    784 
    785       this.focusElementIndex_ = Math.min(this.focusableElements_.length - 1,
    786                                          this.focusElementIndex_);
    787       this.focusElementIndex_ = Math.max(0, this.focusElementIndex_);
    788 
    789       var newFocusElement = this.focusableElements_[this.focusElementIndex_];
    790       var lastFocusElement = this.currentFocusElement_;
    791       if (lastFocusElement && lastFocusElement != newFocusElement)
    792         lastFocusElement.tabIndex = -1;
    793 
    794       newFocusElement.tabIndex = 1;
    795     },
    796 
    797     /**
    798      * Focuses the element at |this.focusElementIndex_|. Makes the previous
    799      * focus element, if any, no longer eligible for tab focus.
    800      * @private
    801      */
    802     updateFocusElement_: function() {
    803       this.updateFocusableElement();
    804       if (this.focusElementIndex_ >= 0)
    805         this.focusableElements_[this.focusElementIndex_].focus();
    806     },
    807 
    808     /**
    809      * The current focus element is that element which is eligible for focus.
    810      * @type {HTMLElement} The node.
    811      * @private
    812      */
    813     get currentFocusElement_() {
    814       return this.querySelector('.focusable[tabindex="1"]');
    815     },
    816 
    817     /**
    818      * Makes some calculations for tile layout. These change depending on
    819      * height, width, and the number of tiles.
    820      * TODO(estade): optimize calls to this function. Do nothing if the page is
    821      * hidden, but call before being shown.
    822      * @private
    823      */
    824     calculateLayoutValues_: function() {
    825       var grid = this.gridValues_;
    826       var availableSpace = this.tileGrid_.clientWidth - 2 * MIN_WIDE_MARGIN;
    827       var wide = availableSpace >= grid.minWideWidth;
    828       var numRowTiles = wide ? grid.maxColCount : grid.minColCount;
    829 
    830       var effectiveGridWidth = wide ?
    831           Math.min(Math.max(availableSpace, grid.minWideWidth),
    832                    grid.maxWideWidth) :
    833           grid.narrowWidth;
    834       var realTileValues = tileValuesForGrid(effectiveGridWidth, numRowTiles,
    835                                              grid.tileSpacingFraction);
    836 
    837       // leftMargin centers the grid within the avaiable space.
    838       var minMargin = wide ? MIN_WIDE_MARGIN : 0;
    839       var leftMargin =
    840           Math.max(minMargin,
    841                    (this.tileGrid_.clientWidth - effectiveGridWidth) / 2);
    842 
    843       var rowHeight = this.heightForWidth(realTileValues.tileWidth) +
    844           realTileValues.interTileSpacing;
    845 
    846       this.layoutValues_ = {
    847         colWidth: realTileValues.offsetX,
    848         gridWidth: effectiveGridWidth,
    849         leftMargin: leftMargin,
    850         numRowTiles: numRowTiles,
    851         rowHeight: rowHeight,
    852         tileWidth: realTileValues.tileWidth,
    853         wide: wide,
    854       };
    855 
    856       // We need to update the top margin as well.
    857       this.updateTopMargin_();
    858 
    859       this.firePageLayoutEvent_();
    860     },
    861 
    862     /**
    863      * Dispatches the custom pagelayout event.
    864      * @private
    865      */
    866     firePageLayoutEvent_: function() {
    867       cr.dispatchSimpleEvent(this, 'pagelayout', true, true);
    868     },
    869 
    870     /**
    871      * @return {number} The amount of margin that should be animated (in pixels)
    872      *     for the current grid layout.
    873      */
    874     getAnimatedLeftMargin_: function() {
    875       if (this.layoutValues_.wide)
    876         return 0;
    877 
    878       var grid = this.gridValues_;
    879       return (grid.minWideWidth - MIN_WIDE_MARGIN - grid.narrowWidth) / 2;
    880     },
    881 
    882     /**
    883      * Calculates the x/y coordinates for an element and moves it there.
    884      * @param {number} index The index of the element to be positioned.
    885      * @param {number} indexOffset If provided, this is added to |index| when
    886      *     positioning the tile. The effect is that the tile will be positioned
    887      *     in a non-default location.
    888      * @private
    889      */
    890     positionTile_: function(index, indexOffset) {
    891       var grid = this.gridValues_;
    892       var layout = this.layoutValues_;
    893 
    894       indexOffset = typeof indexOffset != 'undefined' ? indexOffset : 0;
    895       // Add the offset _after_ the modulus division. We might want to show the
    896       // tile off the side of the grid.
    897       var col = index % layout.numRowTiles + indexOffset;
    898       var row = Math.floor(index / layout.numRowTiles);
    899       // Calculate the final on-screen position for the tile.
    900       var realX = col * layout.colWidth + layout.leftMargin;
    901       var realY = row * layout.rowHeight;
    902 
    903       // Calculate the portion of the tile's position that should be animated.
    904       var animatedTileValues = layout.wide ?
    905           grid.wideTileValues : grid.narrowTileValues;
    906       // Animate the difference between three-wide and six-wide.
    907       var animatedLeftMargin = this.getAnimatedLeftMargin_();
    908       var animatedX = col * animatedTileValues.offsetX + animatedLeftMargin;
    909       var animatedY = row * (this.heightForWidth(animatedTileValues.tileWidth) +
    910                              animatedTileValues.interTileSpacing);
    911 
    912       var tile = this.tileElements_[index];
    913       tile.setGridPosition(animatedX, animatedY);
    914       tile.firstChild.setBounds(layout.tileWidth,
    915                                 realX - animatedX,
    916                                 realY - animatedY);
    917 
    918       // This code calculates whether the tile needs to show a clone of itself
    919       // wrapped around the other side of the tile grid.
    920       var offTheRight = col == layout.numRowTiles ||
    921           (col == layout.numRowTiles - 1 && tile.hasDoppleganger());
    922       var offTheLeft = col == -1 || (col == 0 && tile.hasDoppleganger());
    923       if (this.isCurrentDragTarget && (offTheRight || offTheLeft)) {
    924         var sign = offTheRight ? 1 : -1;
    925         tile.showDoppleganger(-layout.numRowTiles * layout.colWidth * sign,
    926                               layout.rowHeight * sign);
    927       } else {
    928         tile.clearDoppleganger();
    929       }
    930 
    931       if (index == this.tileElements_.length - 1) {
    932         this.tileGrid_.style.height = (realY + layout.rowHeight) + 'px';
    933         this.queueUpdateScrollbars_();
    934       }
    935     },
    936 
    937     /**
    938      * Gets the index of the tile that should occupy coordinate (x, y). Note
    939      * that this function doesn't care where the tiles actually are, and will
    940      * return an index even for the space between two tiles. This function is
    941      * effectively the inverse of |positionTile_|.
    942      * @param {number} x The x coordinate, in pixels, relative to the left of
    943      *     |this|.
    944      * @param {number} y The y coordinate, in pixels, relative to the top of
    945      *     |this|.
    946      * @private
    947      */
    948     getWouldBeIndexForPoint_: function(x, y) {
    949       var grid = this.gridValues_;
    950       var layout = this.layoutValues_;
    951 
    952       var gridClientRect = this.tileGrid_.getBoundingClientRect();
    953       var col = Math.floor((x - gridClientRect.left - layout.leftMargin) /
    954                            layout.colWidth);
    955       if (col < 0 || col >= layout.numRowTiles)
    956         return -1;
    957 
    958       if (isRTL())
    959         col = layout.numRowTiles - 1 - col;
    960 
    961       var row = Math.floor((y - gridClientRect.top) / layout.rowHeight);
    962       return row * layout.numRowTiles + col;
    963     },
    964 
    965     /**
    966      * Window resize event handler. Window resizes may trigger re-layouts.
    967      * @param {Object} e The resize event.
    968      */
    969     onResize_: function(e) {
    970       if (this.lastWidth_ == this.clientWidth &&
    971           this.lastHeight_ == this.clientHeight) {
    972         return;
    973       }
    974 
    975       this.calculateLayoutValues_();
    976 
    977       this.lastWidth_ = this.clientWidth;
    978       this.lastHeight_ = this.clientHeight;
    979       this.classList.add('animating-tile-page');
    980       this.heightChanged_();
    981 
    982       this.positionNotification_();
    983       this.repositionTiles_();
    984     },
    985 
    986     /**
    987      * The tile grid has an image mask which fades at the edges. We only show
    988      * the mask when there is an active drag; it obscures doppleganger tiles
    989      * as they enter or exit the grid.
    990      * @private
    991      */
    992     updateMask_: function() {
    993       if (!this.isCurrentDragTarget) {
    994         this.tileGrid_.style.WebkitMaskBoxImage = '';
    995         return;
    996       }
    997 
    998       var leftMargin = this.layoutValues_.leftMargin;
    999       // The fade distance is the space between tiles.
   1000       var fadeDistance = (this.gridValues_.tileSpacingFraction *
   1001           this.layoutValues_.tileWidth);
   1002       fadeDistance = Math.min(leftMargin, fadeDistance);
   1003       // On Skia we don't use any fade because it works very poorly. See
   1004       // http://crbug.com/99373
   1005       if (!cr.isMac)
   1006         fadeDistance = 1;
   1007       var gradient =
   1008           '-webkit-linear-gradient(left,' +
   1009               'transparent, ' +
   1010               'transparent ' + (leftMargin - fadeDistance) + 'px, ' +
   1011               'black ' + leftMargin + 'px, ' +
   1012               'black ' + (this.tileGrid_.clientWidth - leftMargin) + 'px, ' +
   1013               'transparent ' + (this.tileGrid_.clientWidth - leftMargin +
   1014                                 fadeDistance) + 'px, ' +
   1015               'transparent)';
   1016       this.tileGrid_.style.WebkitMaskBoxImage = gradient;
   1017     },
   1018 
   1019     updateTopMargin_: function() {
   1020       var layout = this.layoutValues_;
   1021 
   1022       // The top margin is set so that the vertical midpoint of the grid will
   1023       // be 1/3 down the page.
   1024       var numTiles = this.tileCount +
   1025           (this.isCurrentDragTarget && !this.withinPageDrag_ ? 1 : 0);
   1026       var numRows = Math.max(1, Math.ceil(numTiles / layout.numRowTiles));
   1027       var usedHeight = layout.rowHeight * numRows;
   1028       var newMargin = document.documentElement.clientHeight / 3 -
   1029           usedHeight / 3 - this.contentPadding;
   1030       // The 'height' style attribute of topMargin is non-zero to work around
   1031       // webkit's collapsing margin behavior, so we have to factor that into
   1032       // our calculations here.
   1033       newMargin = Math.max(newMargin, 0) - this.topMargin_.offsetHeight;
   1034 
   1035       // |newMargin| is the final margin we actually want to show. However,
   1036       // part of that should be animated and part should not (for the same
   1037       // reason as with leftMargin). The approach is to consider differences
   1038       // when the layout changes from wide to narrow or vice versa as
   1039       // 'animatable'. These differences accumulate in animatedTopMarginPx_,
   1040       // while topMarginPx_ caches the real (total) margin. Either of these
   1041       // calculations may come out to be negative, so we use margins as the
   1042       // css property.
   1043 
   1044       if (typeof this.topMarginIsForWide_ == 'undefined')
   1045         this.topMarginIsForWide_ = layout.wide;
   1046       if (this.topMarginIsForWide_ != layout.wide) {
   1047         this.animatedTopMarginPx_ += newMargin - this.topMarginPx_;
   1048         this.topMargin_.style.marginBottom = toCssPx(this.animatedTopMarginPx_);
   1049       }
   1050 
   1051       this.topMarginIsForWide_ = layout.wide;
   1052       this.topMarginPx_ = newMargin;
   1053       this.topMargin_.style.marginTop =
   1054           toCssPx(this.topMarginPx_ - this.animatedTopMarginPx_);
   1055     },
   1056 
   1057     /**
   1058      * Position the notification if there's one showing.
   1059      */
   1060     positionNotification_: function() {
   1061       var notification = this.notification;
   1062       if (!notification || notification.hidden)
   1063         return;
   1064 
   1065       // Update the horizontal position.
   1066       var animatedLeftMargin = this.getAnimatedLeftMargin_();
   1067       notification.style.WebkitMarginStart = animatedLeftMargin + 'px';
   1068       var leftOffset = (this.layoutValues_.leftMargin - animatedLeftMargin) *
   1069                        (isRTL() ? -1 : 1);
   1070       notification.style.WebkitTransform = 'translateX(' + leftOffset + 'px)';
   1071 
   1072       // Update the allowable widths of the text.
   1073       var buttonWidth = notification.querySelector('button').offsetWidth + 8;
   1074       notification.querySelector('span').style.maxWidth =
   1075           this.layoutValues_.gridWidth - buttonWidth + 'px';
   1076 
   1077       // This makes sure the text doesn't condense smaller than the narrow size
   1078       // of the grid (e.g. when a user makes the window really small).
   1079       notification.style.minWidth =
   1080           this.gridValues_.narrowWidth - buttonWidth + 'px';
   1081 
   1082       // Update the top position.
   1083       notification.style.marginTop = -notification.offsetHeight + 'px';
   1084     },
   1085 
   1086     /**
   1087      * Handles final setup that can only happen after |this| is inserted into
   1088      * the page.
   1089      * @private
   1090      */
   1091     onNodeInsertedIntoDocument_: function(e) {
   1092       this.calculateLayoutValues_();
   1093       this.heightChanged_();
   1094     },
   1095 
   1096     /**
   1097      * Called when the height of |this| has changed: update the size of
   1098      * tileGrid.
   1099      * @private
   1100      */
   1101     heightChanged_: function() {
   1102       // The tile grid will expand to the bottom footer, or enough to hold all
   1103       // the tiles, whichever is greater. It would be nicer if tilePage were
   1104       // a flex box, and the tile grid could be box-flex: 1, but this exposes a
   1105       // bug where repositioning tiles will cause the scroll position to reset.
   1106       this.tileGrid_.style.minHeight = (this.clientHeight -
   1107           this.tileGrid_.offsetTop - this.content_.offsetTop -
   1108           this.extraBottomPadding -
   1109           (this.footerNode_ ? this.footerNode_.clientHeight : 0)) + 'px';
   1110     },
   1111 
   1112      /**
   1113       * Places an element at the bottom of the content div. Used in bare-minimum
   1114       * mode to hold #footer.
   1115       * @param {HTMLElement} footerNode The node to append to content.
   1116       */
   1117     appendFooter: function(footerNode) {
   1118       this.footerNode_ = footerNode;
   1119       this.content_.appendChild(footerNode);
   1120     },
   1121 
   1122     /**
   1123      * Scrolls the page in response to an mousewheel event, although the event
   1124      * may have been triggered on a different element. Return true if the
   1125      * event triggered scrolling, and false otherwise.
   1126      * This is called explicitly, which allows a consistent experience whether
   1127      * the user scrolls on the page or on the page switcher, because this
   1128      * function provides a common conversion factor between wheel delta and
   1129      * scroll delta.
   1130      * @param {Event} e The mousewheel event.
   1131      */
   1132     handleMouseWheel: function(e) {
   1133       if (e.wheelDeltaY == 0)
   1134         return false;
   1135 
   1136       this.content_.scrollTop -= e.wheelDeltaY / 3;
   1137       return true;
   1138     },
   1139 
   1140     /**
   1141      * Handler for the 'scroll' event on |content_|.
   1142      * @param {Event} e The scroll event.
   1143      * @private
   1144      */
   1145     onScroll_: function(e) {
   1146       this.queueUpdateScrollbars_();
   1147     },
   1148 
   1149     /**
   1150      * ID of scrollbar update timer. If 0, there's no scrollbar re-calc queued.
   1151      * @private
   1152      */
   1153     scrollbarUpdate_: 0,
   1154 
   1155     /**
   1156      * Queues an update on the custom scrollbar. Used for two reasons: first,
   1157      * coalescing of multiple updates, and second, because action like
   1158      * repositioning a tile can require a delay before they affect values
   1159      * like clientHeight.
   1160      * @private
   1161      */
   1162     queueUpdateScrollbars_: function() {
   1163       if (this.scrollbarUpdate_)
   1164         return;
   1165 
   1166       this.scrollbarUpdate_ = window.setTimeout(
   1167           this.doUpdateScrollbars_.bind(this), 0);
   1168     },
   1169 
   1170     /**
   1171      * Does the work of calculating the visibility, height and position of the
   1172      * scrollbar thumb (there is no track or buttons).
   1173      * @private
   1174      */
   1175     doUpdateScrollbars_: function() {
   1176       this.scrollbarUpdate_ = 0;
   1177 
   1178       var content = this.content_;
   1179 
   1180       // Adjust scroll-height to account for possible header-bar.
   1181       var adjustedScrollHeight = content.scrollHeight - content.offsetTop;
   1182 
   1183       if (adjustedScrollHeight <= content.clientHeight) {
   1184         this.scrollbar_.hidden = true;
   1185         return;
   1186       } else {
   1187         this.scrollbar_.hidden = false;
   1188       }
   1189 
   1190       var thumbTop = content.offsetTop +
   1191           content.scrollTop / adjustedScrollHeight * content.clientHeight;
   1192       var thumbHeight = content.clientHeight / adjustedScrollHeight *
   1193           this.clientHeight;
   1194 
   1195       this.scrollbar_.style.top = thumbTop + 'px';
   1196       this.scrollbar_.style.height = thumbHeight + 'px';
   1197       this.firePageLayoutEvent_();
   1198     },
   1199 
   1200     /**
   1201      * Get the height for a tile of a certain width. Override this function to
   1202      * get non-square tiles.
   1203      * @param {number} width The pixel width of a tile.
   1204      * @return {number} The height for |width|.
   1205      */
   1206     heightForWidth: function(width) {
   1207       return width;
   1208     },
   1209 
   1210     /** Dragging **/
   1211 
   1212     get isCurrentDragTarget() {
   1213       return this.dragWrapper_.isCurrentDragTarget;
   1214     },
   1215 
   1216     /**
   1217      * Thunk for dragleave events fired on |tileGrid_|.
   1218      * @param {Event} e A MouseEvent for the drag.
   1219      */
   1220     doDragLeave: function(e) {
   1221       this.cleanupDrag();
   1222     },
   1223 
   1224     /**
   1225      * Performs all actions necessary when a drag enters the tile page.
   1226      * @param {Event} e A mouseover event for the drag enter.
   1227      */
   1228     doDragEnter: function(e) {
   1229       // Applies the mask so doppleganger tiles disappear into the fog.
   1230       this.updateMask_();
   1231 
   1232       this.classList.add('animating-tile-page');
   1233       this.withinPageDrag_ = this.contains(currentlyDraggingTile);
   1234       this.dragItemIndex_ = this.withinPageDrag_ ?
   1235           currentlyDraggingTile.index : this.tileElements_.length;
   1236       this.currentDropIndex_ = this.dragItemIndex_;
   1237 
   1238       // The new tile may change the number of rows, hence the top margin
   1239       // will change.
   1240       if (!this.withinPageDrag_)
   1241         this.updateTopMargin_();
   1242 
   1243       this.doDragOver(e);
   1244     },
   1245 
   1246     /**
   1247      * Performs all actions necessary when the user moves the cursor during
   1248      * a drag over the tile page.
   1249      * @param {Event} e A mouseover event for the drag over.
   1250      */
   1251     doDragOver: function(e) {
   1252       e.preventDefault();
   1253 
   1254       this.setDropEffect(e.dataTransfer);
   1255       var newDragIndex = this.getWouldBeIndexForPoint_(e.pageX, e.pageY);
   1256       if (newDragIndex < 0 || newDragIndex >= this.tileElements_.length)
   1257         newDragIndex = this.dragItemIndex_;
   1258       this.updateDropIndicator_(newDragIndex);
   1259     },
   1260 
   1261     /**
   1262      * Performs all actions necessary when the user completes a drop.
   1263      * @param {Event} e A mouseover event for the drag drop.
   1264      */
   1265     doDrop: function(e) {
   1266       e.stopPropagation();
   1267       e.preventDefault();
   1268 
   1269       var index = this.currentDropIndex_;
   1270       // Only change data if this was not a 'null drag'.
   1271       if (!((index == this.dragItemIndex_) && this.withinPageDrag_)) {
   1272         var adjustedIndex = this.currentDropIndex_ +
   1273             (index > this.dragItemIndex_ ? 1 : 0);
   1274         if (this.withinPageDrag_) {
   1275           this.tileGrid_.insertBefore(
   1276               currentlyDraggingTile,
   1277               this.tileElements_[adjustedIndex]);
   1278           this.tileMoved(currentlyDraggingTile, this.dragItemIndex_);
   1279         } else {
   1280           var originalPage = currentlyDraggingTile ?
   1281               currentlyDraggingTile.tilePage : null;
   1282           this.addDragData(e.dataTransfer, adjustedIndex);
   1283           if (originalPage)
   1284             originalPage.cleanupDrag();
   1285         }
   1286 
   1287         // Dropping the icon may cause topMargin to change, but changing it
   1288         // now would cause everything to move (annoying), so we leave it
   1289         // alone. The top margin will be re-calculated next time the window is
   1290         // resized or the page is selected.
   1291       }
   1292 
   1293       this.classList.remove('animating-tile-page');
   1294       this.cleanupDrag();
   1295     },
   1296 
   1297     /**
   1298      * Appends the currently dragged tile to the end of the page. Called
   1299      * from outside the page, e.g. when dropping on a nav dot.
   1300      */
   1301     appendDraggingTile: function() {
   1302       var originalPage = currentlyDraggingTile.tilePage;
   1303       if (originalPage == this)
   1304         return;
   1305 
   1306       this.addDragData(null, this.tileElements_.length);
   1307       if (originalPage)
   1308         originalPage.cleanupDrag();
   1309     },
   1310 
   1311     /**
   1312      * Makes sure all the tiles are in the right place after a drag is over.
   1313      */
   1314     cleanupDrag: function() {
   1315       this.repositionTiles_(currentlyDraggingTile);
   1316       // Remove the drag mask.
   1317       this.updateMask_();
   1318     },
   1319 
   1320     /**
   1321      * Reposition all the tiles (possibly ignoring one).
   1322      * @param {?Node} ignoreNode An optional node to ignore.
   1323      * @private
   1324      */
   1325     repositionTiles_: function(ignoreNode) {
   1326       for (var i = 0; i < this.tileElements_.length; i++) {
   1327         if (!ignoreNode || ignoreNode !== this.tileElements_[i])
   1328           this.positionTile_(i);
   1329       }
   1330     },
   1331 
   1332     /**
   1333      * Updates the visual indicator for the drop location for the active drag.
   1334      * @param {Event} e A MouseEvent for the drag.
   1335      * @private
   1336      */
   1337     updateDropIndicator_: function(newDragIndex) {
   1338       var oldDragIndex = this.currentDropIndex_;
   1339       if (newDragIndex == oldDragIndex)
   1340         return;
   1341 
   1342       var repositionStart = Math.min(newDragIndex, oldDragIndex);
   1343       var repositionEnd = Math.max(newDragIndex, oldDragIndex);
   1344 
   1345       for (var i = repositionStart; i <= repositionEnd; i++) {
   1346         if (i == this.dragItemIndex_)
   1347           continue;
   1348         else if (i > this.dragItemIndex_)
   1349           var adjustment = i <= newDragIndex ? -1 : 0;
   1350         else
   1351           var adjustment = i >= newDragIndex ? 1 : 0;
   1352 
   1353         this.positionTile_(i, adjustment);
   1354       }
   1355       this.currentDropIndex_ = newDragIndex;
   1356     },
   1357 
   1358     /**
   1359      * Checks if a page can accept a drag with the given data.
   1360      * @param {Event} e The drag event if the drag object. Implementations will
   1361      *     likely want to check |e.dataTransfer|.
   1362      * @return {boolean} True if this page can handle the drag.
   1363      */
   1364     shouldAcceptDrag: function(e) {
   1365       return false;
   1366     },
   1367 
   1368     /**
   1369      * Called to accept a drag drop. Will not be called for in-page drops.
   1370      * @param {Object} dataTransfer The data transfer object that holds the drop
   1371      *     data. This should only be used if currentlyDraggingTile is null.
   1372      * @param {number} index The tile index at which the drop occurred.
   1373      */
   1374     addDragData: function(dataTransfer, index) {
   1375       assert(false);
   1376     },
   1377 
   1378     /**
   1379      * Called when a tile has been moved (via dragging). Override this to make
   1380      * backend updates.
   1381      * @param {Node} draggedTile The tile that was dropped.
   1382      * @param {number} prevIndex The previous index of the tile.
   1383      */
   1384     tileMoved: function(draggedTile, prevIndex) {
   1385     },
   1386 
   1387     /**
   1388      * Sets the drop effect on |dataTransfer| to the desired value (e.g.
   1389      * 'copy').
   1390      * @param {Object} dataTransfer The drag event dataTransfer object.
   1391      */
   1392     setDropEffect: function(dataTransfer) {
   1393       assert(false);
   1394     },
   1395   };
   1396 
   1397   return {
   1398     getCurrentlyDraggingTile: getCurrentlyDraggingTile,
   1399     setCurrentDropEffect: setCurrentDropEffect,
   1400     TilePage: TilePage,
   1401   };
   1402 });
   1403