Allow only one bookmark to be added for multiple fast starring
[chromium-blink-merge.git] / chrome / browser / resources / ntp4 / tile_page.js
blobbc77367282342f0811694ef1e2b0d9aae8640fb9
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.
5 cr.define('ntp', function() {
6   'use strict';
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   }
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   }
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);
46     return tile;
47   }
49   Tile.prototype = {
50     __proto__: HTMLDivElement.prototype,
52     initialize: function(contents) {
53       // 'real' as opposed to doppleganger.
54       this.className = 'tile real';
55       this.appendChild(contents);
56       contents.tile = this;
58       this.addEventListener('dragstart', this.onDragStart_);
59       this.addEventListener('drag', this.onDragMove_);
60       this.addEventListener('dragend', this.onDragEnd_);
62       this.firstChild.addEventListener(
63           'webkitAnimationEnd', this.onContentsAnimationEnd_.bind(this));
65       this.eventTracker = new EventTracker();
66     },
68     get index() {
69       return Array.prototype.indexOf.call(this.tilePage.tileElements_, this);
70     },
72     get tilePage() {
73       return findAncestorByClass(this, 'tile-page');
74     },
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     },
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     },
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_();
111       setCurrentlyDraggingTile(this);
113       e.dataTransfer.effectAllowed = 'copyMove';
114       this.firstChild.setDragData(e.dataTransfer);
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));
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;
136       this.onDragMove_(e);
137     },
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       }
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     },
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');
164       setCurrentlyDraggingTile(null);
166       // tilePage will be null if we've already been removed.
167       var tilePage = this.tilePage;
168       if (tilePage)
169         tilePage.positionTile_(this.index);
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       }
211       delete this.lastDropEffect;
212       this.landedOnTrash = false;
213     },
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();
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       }
235       this.appendChild(clone);
236       this.doppleganger_ = clone;
238       if (isRTL())
239         x *= -1;
241       this.doppleganger_.style.WebkitTransform = 'translate(' + x + 'px, ' +
242                                                                 y + 'px)';
243     },
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     },
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     },
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'));
272       var clone = this.dragClone;
273       this.dragClone = null;
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     },
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 == 'transform')) {
291         this.finalizeDrag_();
292       }
293     },
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     },
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   };
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   }
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;
343     return {
344       tileWidth: tileWidth,
345       offsetX: offsetX,
346       interTileSpacing: interTileSpacing,
347     };
348   }
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;
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();
369     return el;
370   }
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   };
401   TilePage.prototype = {
402     __proto__: HTMLDivElement.prototype,
404     initialize: function() {
405       this.className = 'tile-page';
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_);
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_);
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_);
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'));
437       this.content_.appendChild(this.tileGrid_);
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');
448       // These are properties used in updateTopMargin.
449       this.animatedTopMarginPx_ = 0;
450       this.topMarginPx_ = 0;
452       this.eventTracker = new EventTracker();
453       this.eventTracker.add(window, 'resize', this.onResize_.bind(this));
455       this.addEventListener('DOMNodeInsertedIntoDocument',
456                             this.onNodeInsertedIntoDocument_);
458       this.content_.addEventListener('scroll', this.onScroll_.bind(this));
460       this.dragWrapper_ = new cr.ui.DragWrapper(this.tileGrid_, this);
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_);
468       this.focusElementIndex_ = -1;
469     },
471     get tiles() {
472       return this.tileElements_;
473     },
475     get tileCount() {
476       return this.tileElements_.length;
477     },
479     get selected() {
480       return Array.prototype.indexOf.call(this.parentNode.children, this) ==
481           ntp.getCardSlider().currentCard;
482     },
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     },
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     },
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     },
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     },
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(window.getComputedStyle(this.content_).paddingTop, 10);
538       }
539       return this.contentPadding_;
540     },
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     },
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     },
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     },
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');
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);
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_();
599       this.repositionTiles_();
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     },
607     /**
608      * Notify interested subscribers that a tile has been removed from this
609      * page.
610      * @param {ntp.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     },
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');
633       var index = tile.index;
634       tile.parentNode.removeChild(tile);
635       this.calculateLayoutValues_();
636       this.cleanupDrag();
637       this.updateFocusableElement();
639       if (!opt_dontNotify)
640         this.fireRemovedEvent(tile, index, !!opt_animate);
641     },
643     /**
644      * Notify interested subscribers that a tile has been removed from this
645      * page.
646      * @param {ntp.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     },
659     /**
660      * Removes all tiles from the page.
661      */
662     removeAllTiles: function() {
663       this.tileGrid_.innerHTML = '';
664     },
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();
674       // When we are selected, we re-calculate the layout values. (See comment
675       // in doDrop.)
676       this.calculateLayoutValues_();
677     },
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     },
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;
698       this.updateFocusElement_();
699     },
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(/** @type {Element} */(e.target),
709                                           'focusable');
710       if (focusable) {
711         this.focusElementIndex_ =
712             Array.prototype.indexOf.call(this.focusableElements_,
713                                          focusable);
714         this.updateFocusElement_();
715       } else {
716         // This prevents the tile page from getting focus when the user clicks
717         // inside the grid but outside of any tile.
718         e.preventDefault();
719       }
720     },
722     /**
723      * Handle arrow key focus nav.
724      * @param {Event} e The focus event.
725      * @private
726      */
727     handleKeyDown_: function(e) {
728       // We only handle up, down, left, right without control keys.
729       if (e.metaKey || e.shiftKey || e.altKey || e.ctrlKey)
730         return;
732       // Wrap the given index to |this.focusableElements_|.
733       var wrap = function(idx) {
734         return (idx + this.focusableElements_.length) %
735             this.focusableElements_.length;
736       }.bind(this);
738       switch (e.keyIdentifier) {
739         case 'Right':
740         case 'Left':
741           var direction = e.keyIdentifier == 'Right' ? 1 : -1;
742           this.focusElementIndex_ = wrap(this.focusElementIndex_ + direction);
743           break;
744         case 'Up':
745         case 'Down':
746           // Look through all focusable elements. Find the first one that is
747           // in the same column.
748           var direction = e.keyIdentifier == 'Up' ? -1 : 1;
749           var currentIndex =
750               Array.prototype.indexOf.call(this.focusableElements_,
751                                            this.currentFocusElement_);
752           var newFocusIdx = wrap(currentIndex + direction);
753           var tile = this.currentFocusElement_.parentNode;
754           for (;; newFocusIdx = wrap(newFocusIdx + direction)) {
755             var newTile = this.focusableElements_[newFocusIdx].parentNode;
756             var rowTiles = this.layoutValues_.numRowTiles;
757             if ((newTile.index - tile.index) % rowTiles == 0)
758               break;
759           }
761           this.focusElementIndex_ = newFocusIdx;
762           break;
764         default:
765           return;
766       }
768       this.updateFocusElement_();
770       e.preventDefault();
771       e.stopPropagation();
772     },
774     /**
775      * Ensure 0 <= this.focusElementIndex_ < this.focusableElements_.length,
776      * make the focusable element at this.focusElementIndex_ (if any) eligible
777      * for tab focus, and the previously-focused element not eligible.
778      * @protected
779      */
780     updateFocusableElement: function() {
781       if (this.focusableElements_.length == 0 || !this.selected) {
782         this.focusElementIndex_ = -1;
783         return;
784       }
786       this.focusElementIndex_ = Math.min(this.focusableElements_.length - 1,
787                                          this.focusElementIndex_);
788       this.focusElementIndex_ = Math.max(0, this.focusElementIndex_);
790       var newFocusElement = this.focusableElements_[this.focusElementIndex_];
791       var lastFocusElement = this.currentFocusElement_;
792       if (lastFocusElement && lastFocusElement != newFocusElement)
793         lastFocusElement.tabIndex = -1;
795       newFocusElement.tabIndex = 1;
796     },
798     /**
799      * Focuses the element at |this.focusElementIndex_|. Makes the previous
800      * focus element, if any, no longer eligible for tab focus.
801      * @private
802      */
803     updateFocusElement_: function() {
804       this.updateFocusableElement();
805       if (this.focusElementIndex_ >= 0)
806         this.focusableElements_[this.focusElementIndex_].focus();
807     },
809     /**
810      * The current focus element is that element which is eligible for focus.
811      * @type {HTMLElement} The node.
812      * @private
813      */
814     get currentFocusElement_() {
815       return this.querySelector('.focusable[tabindex="1"]');
816     },
818     /**
819      * Makes some calculations for tile layout. These change depending on
820      * height, width, and the number of tiles.
821      * TODO(estade): optimize calls to this function. Do nothing if the page is
822      * hidden, but call before being shown.
823      * @private
824      */
825     calculateLayoutValues_: function() {
826       var grid = this.gridValues_;
827       var availableSpace = this.tileGrid_.clientWidth - 2 * MIN_WIDE_MARGIN;
828       var wide = availableSpace >= grid.minWideWidth;
829       var numRowTiles = wide ? grid.maxColCount : grid.minColCount;
831       var effectiveGridWidth = wide ?
832           Math.min(Math.max(availableSpace, grid.minWideWidth),
833                    grid.maxWideWidth) :
834           grid.narrowWidth;
835       var realTileValues = tileValuesForGrid(effectiveGridWidth, numRowTiles,
836                                              grid.tileSpacingFraction);
838       // leftMargin centers the grid within the avaiable space.
839       var minMargin = wide ? MIN_WIDE_MARGIN : 0;
840       var leftMargin =
841           Math.max(minMargin,
842                    (this.tileGrid_.clientWidth - effectiveGridWidth) / 2);
844       var rowHeight = this.heightForWidth(realTileValues.tileWidth) +
845           realTileValues.interTileSpacing;
847       this.layoutValues_ = {
848         colWidth: realTileValues.offsetX,
849         gridWidth: effectiveGridWidth,
850         leftMargin: leftMargin,
851         numRowTiles: numRowTiles,
852         rowHeight: rowHeight,
853         tileWidth: realTileValues.tileWidth,
854         wide: wide,
855       };
857       // We need to update the top margin as well.
858       this.updateTopMargin_();
860       this.firePageLayoutEvent_();
861     },
863     /**
864      * Dispatches the custom pagelayout event.
865      * @private
866      */
867     firePageLayoutEvent_: function() {
868       cr.dispatchSimpleEvent(this, 'pagelayout', true, true);
869     },
871     /**
872      * @return {number} The amount of margin that should be animated (in pixels)
873      *     for the current grid layout.
874      */
875     getAnimatedLeftMargin_: function() {
876       if (this.layoutValues_.wide)
877         return 0;
879       var grid = this.gridValues_;
880       return (grid.minWideWidth - MIN_WIDE_MARGIN - grid.narrowWidth) / 2;
881     },
883     /**
884      * Calculates the x/y coordinates for an element and moves it there.
885      * @param {number} index The index of the element to be positioned.
886      * @param {number=} opt_indexOffset If provided, this is added to |index|
887      *     when positioning the tile. The effect is that the tile will be
888      *     positioned in a non-default location.
889      * @private
890      */
891     positionTile_: function(index, opt_indexOffset) {
892       var grid = this.gridValues_;
893       var layout = this.layoutValues_;
895       var indexOffset = opt_indexOffset || 0;
896       // Add the offset _after_ the modulus division. We might want to show the
897       // tile off the side of the grid.
898       var col = index % layout.numRowTiles + indexOffset;
899       var row = Math.floor(index / layout.numRowTiles);
900       // Calculate the final on-screen position for the tile.
901       var realX = col * layout.colWidth + layout.leftMargin;
902       var realY = row * layout.rowHeight;
904       // Calculate the portion of the tile's position that should be animated.
905       var animatedTileValues = layout.wide ?
906           grid.wideTileValues : grid.narrowTileValues;
907       // Animate the difference between three-wide and six-wide.
908       var animatedLeftMargin = this.getAnimatedLeftMargin_();
909       var animatedX = col * animatedTileValues.offsetX + animatedLeftMargin;
910       var animatedY = row * (this.heightForWidth(animatedTileValues.tileWidth) +
911                              animatedTileValues.interTileSpacing);
913       var tile = this.tileElements_[index];
914       tile.setGridPosition(animatedX, animatedY);
915       tile.firstChild.setBounds(layout.tileWidth,
916                                 realX - animatedX,
917                                 realY - animatedY);
919       // This code calculates whether the tile needs to show a clone of itself
920       // wrapped around the other side of the tile grid.
921       var offTheRight = col == layout.numRowTiles ||
922           (col == layout.numRowTiles - 1 && tile.hasDoppleganger());
923       var offTheLeft = col == -1 || (col == 0 && tile.hasDoppleganger());
924       if (this.isCurrentDragTarget && (offTheRight || offTheLeft)) {
925         var sign = offTheRight ? 1 : -1;
926         tile.showDoppleganger(-layout.numRowTiles * layout.colWidth * sign,
927                               layout.rowHeight * sign);
928       } else {
929         tile.clearDoppleganger();
930       }
932       if (index == this.tileElements_.length - 1) {
933         this.tileGrid_.style.height = (realY + layout.rowHeight) + 'px';
934         this.queueUpdateScrollbars_();
935       }
936     },
938     /**
939      * Gets the index of the tile that should occupy coordinate (x, y). Note
940      * that this function doesn't care where the tiles actually are, and will
941      * return an index even for the space between two tiles. This function is
942      * effectively the inverse of |positionTile_|.
943      * @param {number} x The x coordinate, in pixels, relative to the left of
944      *     |this|.
945      * @param {number} y The y coordinate, in pixels, relative to the top of
946      *     |this|.
947      * @return {number}
948      * @private
949      */
950     getWouldBeIndexForPoint_: function(x, y) {
951       var grid = this.gridValues_;
952       var layout = this.layoutValues_;
954       var gridClientRect = this.tileGrid_.getBoundingClientRect();
955       var col = Math.floor((x - gridClientRect.left - layout.leftMargin) /
956                            layout.colWidth);
957       if (col < 0 || col >= layout.numRowTiles)
958         return -1;
960       if (isRTL())
961         col = layout.numRowTiles - 1 - col;
963       var row = Math.floor((y - gridClientRect.top) / layout.rowHeight);
964       return row * layout.numRowTiles + col;
965     },
967     /**
968      * Window resize event handler. Window resizes may trigger re-layouts.
969      * @param {Object} e The resize event.
970      */
971     onResize_: function(e) {
972       if (this.lastWidth_ == this.clientWidth &&
973           this.lastHeight_ == this.clientHeight) {
974         return;
975       }
977       this.calculateLayoutValues_();
979       this.lastWidth_ = this.clientWidth;
980       this.lastHeight_ = this.clientHeight;
981       this.classList.add('animating-tile-page');
982       this.heightChanged_();
984       this.positionNotification_();
985       this.repositionTiles_();
986     },
988     /**
989      * The tile grid has an image mask which fades at the edges. We only show
990      * the mask when there is an active drag; it obscures doppleganger tiles
991      * as they enter or exit the grid.
992      * @private
993      */
994     updateMask_: function() {
995       if (!this.isCurrentDragTarget) {
996         this.tileGrid_.style.WebkitMaskBoxImage = '';
997         return;
998       }
1000       var leftMargin = this.layoutValues_.leftMargin;
1001       // The fade distance is the space between tiles.
1002       var fadeDistance = (this.gridValues_.tileSpacingFraction *
1003           this.layoutValues_.tileWidth);
1004       fadeDistance = Math.min(leftMargin, fadeDistance);
1005       // On Skia we don't use any fade because it works very poorly. See
1006       // http://crbug.com/99373
1007       if (!cr.isMac)
1008         fadeDistance = 1;
1009       var gradient =
1010           '-webkit-linear-gradient(left,' +
1011               'transparent, ' +
1012               'transparent ' + (leftMargin - fadeDistance) + 'px, ' +
1013               'black ' + leftMargin + 'px, ' +
1014               'black ' + (this.tileGrid_.clientWidth - leftMargin) + 'px, ' +
1015               'transparent ' + (this.tileGrid_.clientWidth - leftMargin +
1016                                 fadeDistance) + 'px, ' +
1017               'transparent)';
1018       this.tileGrid_.style.WebkitMaskBoxImage = gradient;
1019     },
1021     updateTopMargin_: function() {
1022       var layout = this.layoutValues_;
1024       // The top margin is set so that the vertical midpoint of the grid will
1025       // be 1/3 down the page.
1026       var numTiles = this.tileCount +
1027           (this.isCurrentDragTarget && !this.withinPageDrag_ ? 1 : 0);
1028       var numRows = Math.max(1, Math.ceil(numTiles / layout.numRowTiles));
1029       var usedHeight = layout.rowHeight * numRows;
1030       var newMargin = document.documentElement.clientHeight / 3 -
1031           usedHeight / 3 - this.contentPadding;
1032       // The 'height' style attribute of topMargin is non-zero to work around
1033       // webkit's collapsing margin behavior, so we have to factor that into
1034       // our calculations here.
1035       newMargin = Math.max(newMargin, 0) - this.topMargin_.offsetHeight;
1037       // |newMargin| is the final margin we actually want to show. However,
1038       // part of that should be animated and part should not (for the same
1039       // reason as with leftMargin). The approach is to consider differences
1040       // when the layout changes from wide to narrow or vice versa as
1041       // 'animatable'. These differences accumulate in animatedTopMarginPx_,
1042       // while topMarginPx_ caches the real (total) margin. Either of these
1043       // calculations may come out to be negative, so we use margins as the
1044       // css property.
1046       if (typeof this.topMarginIsForWide_ == 'undefined')
1047         this.topMarginIsForWide_ = layout.wide;
1048       if (this.topMarginIsForWide_ != layout.wide) {
1049         this.animatedTopMarginPx_ += newMargin - this.topMarginPx_;
1050         this.topMargin_.style.marginBottom = toCssPx(this.animatedTopMarginPx_);
1051       }
1053       this.topMarginIsForWide_ = layout.wide;
1054       this.topMarginPx_ = newMargin;
1055       this.topMargin_.style.marginTop =
1056           toCssPx(this.topMarginPx_ - this.animatedTopMarginPx_);
1057     },
1059     /**
1060      * Position the notification if there's one showing.
1061      */
1062     positionNotification_: function() {
1063       var notification = this.notification;
1064       if (!notification || notification.hidden)
1065         return;
1067       // Update the horizontal position.
1068       var animatedLeftMargin = this.getAnimatedLeftMargin_();
1069       notification.style.WebkitMarginStart = animatedLeftMargin + 'px';
1070       var leftOffset = (this.layoutValues_.leftMargin - animatedLeftMargin) *
1071                        (isRTL() ? -1 : 1);
1072       notification.style.WebkitTransform = 'translateX(' + leftOffset + 'px)';
1074       // Update the allowable widths of the text.
1075       var buttonWidth = notification.querySelector('button').offsetWidth + 8;
1076       notification.querySelector('span').style.maxWidth =
1077           this.layoutValues_.gridWidth - buttonWidth + 'px';
1079       // This makes sure the text doesn't condense smaller than the narrow size
1080       // of the grid (e.g. when a user makes the window really small).
1081       notification.style.minWidth =
1082           this.gridValues_.narrowWidth - buttonWidth + 'px';
1084       // Update the top position.
1085       notification.style.marginTop = -notification.offsetHeight + 'px';
1086     },
1088     /**
1089      * Handles final setup that can only happen after |this| is inserted into
1090      * the page.
1091      * @private
1092      */
1093     onNodeInsertedIntoDocument_: function(e) {
1094       this.calculateLayoutValues_();
1095       this.heightChanged_();
1096     },
1098     /**
1099      * Called when the height of |this| has changed: update the size of
1100      * tileGrid.
1101      * @private
1102      */
1103     heightChanged_: function() {
1104       // The tile grid will expand to the bottom footer, or enough to hold all
1105       // the tiles, whichever is greater. It would be nicer if tilePage were
1106       // a flex box, and the tile grid could be box-flex: 1, but this exposes a
1107       // bug where repositioning tiles will cause the scroll position to reset.
1108       this.tileGrid_.style.minHeight = (this.clientHeight -
1109           this.tileGrid_.offsetTop - this.content_.offsetTop -
1110           this.extraBottomPadding -
1111           (this.footerNode_ ? this.footerNode_.clientHeight : 0)) + 'px';
1112     },
1114      /**
1115       * Places an element at the bottom of the content div. Used in bare-minimum
1116       * mode to hold #footer.
1117       * @param {HTMLElement} footerNode The node to append to content.
1118       */
1119     appendFooter: function(footerNode) {
1120       this.footerNode_ = footerNode;
1121       this.content_.appendChild(footerNode);
1122     },
1124     /**
1125      * Scrolls the page in response to an mousewheel event, although the event
1126      * may have been triggered on a different element. Return true if the
1127      * event triggered scrolling, and false otherwise.
1128      * This is called explicitly, which allows a consistent experience whether
1129      * the user scrolls on the page or on the page switcher, because this
1130      * function provides a common conversion factor between wheel delta and
1131      * scroll delta.
1132      * @param {Event} e The mousewheel event.
1133      */
1134     handleMouseWheel: function(e) {
1135       // The ctrl-wheel should triggle the zoom in/out actions in Chromium for
1136       // all pages.
1137       if (e.wheelDeltaY == 0 || e.ctrlKey)
1138         return false;
1140       this.content_.scrollTop -= e.wheelDeltaY / 3;
1141       return true;
1142     },
1144     /**
1145      * Handler for the 'scroll' event on |content_|.
1146      * @param {Event} e The scroll event.
1147      * @private
1148      */
1149     onScroll_: function(e) {
1150       this.queueUpdateScrollbars_();
1151     },
1153     /**
1154      * ID of scrollbar update timer. If 0, there's no scrollbar re-calc queued.
1155      * @private
1156      */
1157     scrollbarUpdate_: 0,
1159     /**
1160      * Queues an update on the custom scrollbar. Used for two reasons: first,
1161      * coalescing of multiple updates, and second, because action like
1162      * repositioning a tile can require a delay before they affect values
1163      * like clientHeight.
1164      * @private
1165      */
1166     queueUpdateScrollbars_: function() {
1167       if (this.scrollbarUpdate_)
1168         return;
1170       this.scrollbarUpdate_ = window.setTimeout(
1171           this.doUpdateScrollbars_.bind(this), 0);
1172     },
1174     /**
1175      * Does the work of calculating the visibility, height and position of the
1176      * scrollbar thumb (there is no track or buttons).
1177      * @private
1178      */
1179     doUpdateScrollbars_: function() {
1180       this.scrollbarUpdate_ = 0;
1182       var content = this.content_;
1184       // Adjust scroll-height to account for possible header-bar.
1185       var adjustedScrollHeight = content.scrollHeight - content.offsetTop;
1187       if (adjustedScrollHeight <= content.clientHeight) {
1188         this.scrollbar_.hidden = true;
1189         return;
1190       } else {
1191         this.scrollbar_.hidden = false;
1192       }
1194       var thumbTop = content.offsetTop +
1195           content.scrollTop / adjustedScrollHeight * content.clientHeight;
1196       var thumbHeight = content.clientHeight / adjustedScrollHeight *
1197           this.clientHeight;
1199       this.scrollbar_.style.top = thumbTop + 'px';
1200       this.scrollbar_.style.height = thumbHeight + 'px';
1201       this.firePageLayoutEvent_();
1202     },
1204     /**
1205      * Get the height for a tile of a certain width. Override this function to
1206      * get non-square tiles.
1207      * @param {number} width The pixel width of a tile.
1208      * @return {number} The height for |width|.
1209      */
1210     heightForWidth: function(width) {
1211       return width;
1212     },
1214     /** Dragging **/
1216     get isCurrentDragTarget() {
1217       return this.dragWrapper_.isCurrentDragTarget;
1218     },
1220     /**
1221      * Thunk for dragleave events fired on |tileGrid_|.
1222      * @param {Event} e A MouseEvent for the drag.
1223      */
1224     doDragLeave: function(e) {
1225       this.cleanupDrag();
1226     },
1228     /**
1229      * Performs all actions necessary when a drag enters the tile page.
1230      * @param {Event} e A mouseover event for the drag enter.
1231      */
1232     doDragEnter: function(e) {
1233       // Applies the mask so doppleganger tiles disappear into the fog.
1234       this.updateMask_();
1236       this.classList.add('animating-tile-page');
1237       this.withinPageDrag_ = this.contains(currentlyDraggingTile);
1238       this.dragItemIndex_ = this.withinPageDrag_ ?
1239           currentlyDraggingTile.index : this.tileElements_.length;
1240       this.currentDropIndex_ = this.dragItemIndex_;
1242       // The new tile may change the number of rows, hence the top margin
1243       // will change.
1244       if (!this.withinPageDrag_)
1245         this.updateTopMargin_();
1247       this.doDragOver(e);
1248     },
1250     /**
1251      * Performs all actions necessary when the user moves the cursor during
1252      * a drag over the tile page.
1253      * @param {Event} e A mouseover event for the drag over.
1254      */
1255     doDragOver: function(e) {
1256       e.preventDefault();
1258       this.setDropEffect(e.dataTransfer);
1259       var newDragIndex = this.getWouldBeIndexForPoint_(e.pageX, e.pageY);
1260       if (newDragIndex < 0 || newDragIndex >= this.tileElements_.length)
1261         newDragIndex = this.dragItemIndex_;
1262       this.updateDropIndicator_(newDragIndex);
1263     },
1265     /**
1266      * Performs all actions necessary when the user completes a drop.
1267      * @param {Event} e A mouseover event for the drag drop.
1268      */
1269     doDrop: function(e) {
1270       e.stopPropagation();
1271       e.preventDefault();
1273       var index = this.currentDropIndex_;
1274       // Only change data if this was not a 'null drag'.
1275       if (!((index == this.dragItemIndex_) && this.withinPageDrag_)) {
1276         var adjustedIndex = this.currentDropIndex_ +
1277             (index > this.dragItemIndex_ ? 1 : 0);
1278         if (this.withinPageDrag_) {
1279           this.tileGrid_.insertBefore(
1280               currentlyDraggingTile,
1281               this.tileElements_[adjustedIndex]);
1282           this.tileMoved(currentlyDraggingTile, this.dragItemIndex_);
1283         } else {
1284           var originalPage = currentlyDraggingTile ?
1285               currentlyDraggingTile.tilePage : null;
1286           this.addDragData(e.dataTransfer, adjustedIndex);
1287           if (originalPage)
1288             originalPage.cleanupDrag();
1289         }
1291         // Dropping the icon may cause topMargin to change, but changing it
1292         // now would cause everything to move (annoying), so we leave it
1293         // alone. The top margin will be re-calculated next time the window is
1294         // resized or the page is selected.
1295       }
1297       this.classList.remove('animating-tile-page');
1298       this.cleanupDrag();
1299     },
1301     /**
1302      * Appends the currently dragged tile to the end of the page. Called
1303      * from outside the page, e.g. when dropping on a nav dot.
1304      */
1305     appendDraggingTile: function() {
1306       var originalPage = currentlyDraggingTile.tilePage;
1307       if (originalPage == this)
1308         return;
1310       this.addDragData(null, this.tileElements_.length);
1311       if (originalPage)
1312         originalPage.cleanupDrag();
1313     },
1315     /**
1316      * Makes sure all the tiles are in the right place after a drag is over.
1317      */
1318     cleanupDrag: function() {
1319       this.repositionTiles_(currentlyDraggingTile);
1320       // Remove the drag mask.
1321       this.updateMask_();
1322     },
1324     /**
1325      * Reposition all the tiles (possibly ignoring one).
1326      * @param {Node=} opt_ignoreNode An optional node to ignore.
1327      * @private
1328      */
1329     repositionTiles_: function(opt_ignoreNode) {
1330       for (var i = 0; i < this.tileElements_.length; i++) {
1331         if (!opt_ignoreNode || opt_ignoreNode !== this.tileElements_[i])
1332           this.positionTile_(i);
1333       }
1334     },
1336     /**
1337      * Updates the visual indicator for the drop location for the active drag.
1338      * @param {number} newDragIndex
1339      * @private
1340      */
1341     updateDropIndicator_: function(newDragIndex) {
1342       var oldDragIndex = this.currentDropIndex_;
1343       if (newDragIndex == oldDragIndex)
1344         return;
1346       var repositionStart = Math.min(newDragIndex, oldDragIndex);
1347       var repositionEnd = Math.max(newDragIndex, oldDragIndex);
1349       for (var i = repositionStart; i <= repositionEnd; i++) {
1350         if (i == this.dragItemIndex_)
1351           continue;
1352         else if (i > this.dragItemIndex_)
1353           var adjustment = i <= newDragIndex ? -1 : 0;
1354         else
1355           var adjustment = i >= newDragIndex ? 1 : 0;
1357         this.positionTile_(i, adjustment);
1358       }
1359       this.currentDropIndex_ = newDragIndex;
1360     },
1362     /**
1363      * Checks if a page can accept a drag with the given data.
1364      * @param {Event} e The drag event if the drag object. Implementations will
1365      *     likely want to check |e.dataTransfer|.
1366      * @return {boolean} True if this page can handle the drag.
1367      */
1368     shouldAcceptDrag: function(e) {
1369       return false;
1370     },
1372     /**
1373      * Called to accept a drag drop. Will not be called for in-page drops.
1374      * @param {Object} dataTransfer The data transfer object that holds the drop
1375      *     data. This should only be used if currentlyDraggingTile is null.
1376      * @param {number} index The tile index at which the drop occurred.
1377      */
1378     addDragData: function(dataTransfer, index) {
1379       assertNotReached();
1380     },
1382     /**
1383      * Called when a tile has been moved (via dragging). Override this to make
1384      * backend updates.
1385      * @param {Node} draggedTile The tile that was dropped.
1386      * @param {number} prevIndex The previous index of the tile.
1387      */
1388     tileMoved: function(draggedTile, prevIndex) {
1389     },
1391     /**
1392      * Sets the drop effect on |dataTransfer| to the desired value (e.g.
1393      * 'copy').
1394      * @param {Object} dataTransfer The drag event dataTransfer object.
1395      */
1396     setDropEffect: function(dataTransfer) {
1397       assertNotReached();
1398     },
1399   };
1401   return {
1402     getCurrentlyDraggingTile: getCurrentlyDraggingTile,
1403     setCurrentDropEffect: setCurrentDropEffect,
1404     // Not used outside, just for usage in JSDoc inside this file.
1405     Tile: Tile,
1406     TilePage: TilePage,
1407   };