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
5cr.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      assertNotReached();
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      assertNotReached();
1394    },
1395  };
1396
1397  return {
1398    getCurrentlyDraggingTile: getCurrentlyDraggingTile,
1399    setCurrentDropEffect: setCurrentDropEffect,
1400    TilePage: TilePage,
1401  };
1402});
1403