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() {
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
;
14 function setCurrentlyDraggingTile(tile
) {
15 currentlyDraggingTile
= tile
;
17 ntp
.enterRearrangeMode();
19 ntp
.leaveRearrangeMode();
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).
29 function setCurrentDropEffect(dataTransfer
, effect
) {
30 dataTransfer
.dropEffect
= effect
;
31 if (currentlyDraggingTile
)
32 currentlyDraggingTile
.lastDropEffect
= dataTransfer
.dropEffect
;
36 * Creates a new Tile object. Tiles wrap content on a TilePage, providing
37 * some styling and drag functionality.
39 * @extends {HTMLDivElement}
41 function Tile(contents
) {
42 var tile
= cr
.doc
.createElement('div');
43 tile
.__proto__
= Tile
.prototype;
44 tile
.initialize(contents
);
50 __proto__
: HTMLDivElement
.prototype,
52 initialize: function(contents
) {
53 // 'real' as opposed to doppleganger.
54 this.className
= 'tile real';
55 this.appendChild(contents
);
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();
69 return Array
.prototype.indexOf
.call(this.tilePage
.tileElements_
, this);
73 return findAncestorByClass(this, 'tile-page');
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.
82 setGridPosition: function(x
, y
) {
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.
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
);
101 * The handler for dragstart events fired on |this|.
102 * @param {Event} e The event for the drag.
105 onDragStart_: function(e
) {
106 // The user may start dragging again during a previous drag's finishing
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
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
:
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
;
140 * The handler for drag events fired on |this|.
141 * @param {Event} e The event for the drag.
144 onDragMove_: function(e
) {
145 if (e
.view
!= window
|| (e
.x
== 0 && e
.y
== 0)) {
146 this.dragClone
.hidden
= true;
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
);
156 * The handler for dragend events fired on |this|.
157 * @param {Event} e The event for the drag.
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
;
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;
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
-
190 this.dragClone
.style
.top
=
192 this.parentNode
.getBoundingClientRect().top
-
194 } else if (this.dragClone
.hidden
) {
195 this.finalizeDrag_();
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() {
206 this.dragClone
.classList
.add('dropped-on-other-page');
211 delete this.lastDropEffect
;
212 this.landedOnTrash
= false;
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.
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');
235 this.appendChild(clone
);
236 this.doppleganger_
= clone
;
241 this.doppleganger_
.style
.WebkitTransform
= 'translate(' + x
+ 'px, ' +
246 * Destroys the current doppleganger.
248 clearDoppleganger: function() {
249 if (this.doppleganger_
) {
250 this.removeChild(this.doppleganger_
);
251 this.doppleganger_
= null;
256 * Returns status of doppleganger.
257 * @return {boolean} True if there is a doppleganger showing for |this|.
259 hasDoppleganger: function() {
260 return !!this.doppleganger_
;
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).
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();
283 * Called when the drag representation node is done migrating to its final
285 * @param {Event} e The transition end event.
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_();
296 * Called when an app is removed from Chrome. Animates its disappearance.
297 * @param {boolean=} opt_animate Whether the animation should be animated.
299 doRemove: function(opt_animate
) {
301 this.firstChild
.classList
.add('removing-tile-contents');
303 this.tilePage
.removeTile(this, false);
307 * Callback for the webkitAnimationEnd event on the tile's contents.
308 * @param {Event} e The event object.
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);
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.
325 function tileWidthFraction(rowTileCount
, tileSpacingFraction
) {
326 return rowTileCount
+ (rowTileCount
- 1) * tileSpacingFraction
;
330 * Calculates an assortment of tile-related values for a grid with the
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.
338 function tileValuesForGrid(width
, numRowTiles
, tileSpacingFraction
) {
339 var tileWidth
= width
/ tileWidthFraction(numRowTiles
, tileSpacingFraction
);
340 var offsetX
= tileWidth
* (1 + tileSpacingFraction
);
341 var interTileSpacing
= offsetX
- tileWidth
;
344 tileWidth
: tileWidth
,
346 interTileSpacing
: interTileSpacing
,
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;
356 * Creates a new TilePage object. This object contains tiles and controls
358 * @param {Object} gridValues Pixel values that define the size and layout
361 * @extends {HTMLDivElement}
363 function TilePage(gridValues
) {
364 var el
= cr
.doc
.createElement('div');
365 el
.gridValues_
= gridValues
;
366 el
.__proto__
= TilePage
.prototype;
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.
377 TilePage
.initGridValues = function(grid
) {
378 // The amount of space we need to display a narrow grid (all narrow grids
381 grid
.minTileWidth
* tileWidthFraction(grid
.minColCount
,
382 grid
.tileSpacingFraction
);
383 // The minimum amount of space we need to display a wide grid.
385 grid
.minTileWidth
* tileWidthFraction(grid
.maxColCount
,
386 grid
.tileSpacingFraction
);
387 // The largest we will ever display a wide grid.
389 grid
.maxTileWidth
* tileWidthFraction(grid
.maxColCount
,
390 grid
.tileSpacingFraction
);
391 // Tile-related pixel values for the narrow display.
392 grid
.narrowTileValues
= tileValuesForGrid(grid
.narrowWidth
,
394 grid
.tileSpacingFraction
);
395 // Tile-related pixel values for the minimum narrow display.
396 grid
.wideTileValues
= tileValuesForGrid(grid
.minWideWidth
,
398 grid
.tileSpacingFraction
);
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
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;
472 return this.tileElements_
;
476 return this.tileElements_
.length
;
480 return Array
.prototype.indexOf
.call(this.parentNode
.children
, this) ==
481 ntp
.getCardSlider().currentCard
;
485 * The size of the margin (unused space) on the sides of the tile grid, in
490 return this.layoutValues_
.leftMargin
;
494 * Returns the width of the scrollbar, in pixels, if it is active, or 0
498 get scrollbarWidth() {
499 return this.scrollbar_
.hidden
? 0 : 13;
503 * Returns any extra padding to insert to the bottom of a tile page. By
504 * default there is none, but subclasses can override.
507 get extraBottomPadding() {
512 * The notification content of this tile (if any, otherwise null).
513 * @type {!HTMLElement}
516 return this.topMargin_
.nextElementSibling
.id
== 'notification-container' ?
517 this.topMargin_
.nextElementSibling
: null;
520 * The notification content of this tile (if any, otherwise null).
521 * @type {!HTMLElement}
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_();
531 * Fetches the size, in pixels, of the padding-top of the tile contents.
534 get contentPadding() {
535 if (typeof this.contentPadding_
== 'undefined') {
536 this.contentPadding_
=
537 parseInt(window
.getComputedStyle(this.content_
).paddingTop
, 10);
539 return this.contentPadding_
;
543 * Removes the tilePage from the DOM and cleans up event handlers.
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.');
553 this.parentNode
.removeChild(this);
557 * Cleans up resources that are no longer needed after this TilePage
558 * instance is removed from the DOM.
561 tearDown_: function() {
562 this.eventTracker
.removeAll();
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.
571 appendTile: function(tileElement
, animate
) {
572 this.addTileAt(tileElement
, this.tileElements_
.length
, animate
);
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).
583 addTileAt: function(tileElement
, index
, animate
) {
584 this.classList
.remove('animating-tile-page');
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
);
608 * Notify interested subscribers that a tile has been removed from this
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.
614 fireAddedEvent: function(tile
, index
, wasAnimated
) {
615 var e
= document
.createEvent('Event');
616 e
.initEvent('tilePage:tile_added', true, true);
617 e
.addedIndex
= index
;
619 e
.wasAnimated
= wasAnimated
;
620 this.dispatchEvent(e
);
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.
629 removeTile: function(tile
, opt_animate
, opt_dontNotify
) {
631 this.classList
.add('animating-tile-page');
633 var index
= tile
.index
;
634 tile
.parentNode
.removeChild(tile
);
635 this.calculateLayoutValues_();
637 this.updateFocusableElement();
640 this.fireRemovedEvent(tile
, index
, !!opt_animate
);
644 * Notify interested subscribers that a tile has been removed from this
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.
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
);
660 * Removes all tiles from the page.
662 removeAllTiles: function() {
663 this.tileGrid_
.innerHTML
= '';
667 * Called when the page is selected (in the card selector).
668 * @param {Event} e A custom cardselected event.
671 handleCardSelection_: function(e
) {
672 this.updateFocusableElement();
674 // When we are selected, we re-calculate the layout values. (See comment
676 this.calculateLayoutValues_();
680 * Called when the page loses selection (in the card selector).
681 * @param {Event} e A custom carddeselected event.
684 handleCardDeselection_: function(e
) {
685 if (this.currentFocusElement_
)
686 this.currentFocusElement_
.tabIndex
= -1;
690 * When we get focus, pass it on to the focus element.
691 * @param {Event} e The focus event.
694 handleFocus_: function(e
) {
695 if (this.focusableElements_
.length
== 0)
698 this.updateFocusElement_();
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.
707 handleMouseDown_: function(e
) {
708 var focusable
= findAncestorByClass(/** @type {Element} */(e
.target
),
711 this.focusElementIndex_
=
712 Array
.prototype.indexOf
.call(this.focusableElements_
,
714 this.updateFocusElement_();
716 // This prevents the tile page from getting focus when the user clicks
717 // inside the grid but outside of any tile.
723 * Handle arrow key focus nav.
724 * @param {Event} e The focus event.
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
)
732 // Wrap the given index to |this.focusableElements_|.
733 var wrap = function(idx
) {
734 return (idx
+ this.focusableElements_
.length
) %
735 this.focusableElements_
.length
;
738 switch (e
.keyIdentifier
) {
741 var direction
= e
.keyIdentifier
== 'Right' ? 1 : -1;
742 this.focusElementIndex_
= wrap(this.focusElementIndex_
+ direction
);
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;
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)
761 this.focusElementIndex_
= newFocusIdx
;
768 this.updateFocusElement_();
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.
780 updateFocusableElement: function() {
781 if (this.focusableElements_
.length
== 0 || !this.selected
) {
782 this.focusElementIndex_
= -1;
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;
799 * Focuses the element at |this.focusElementIndex_|. Makes the previous
800 * focus element, if any, no longer eligible for tab focus.
803 updateFocusElement_: function() {
804 this.updateFocusableElement();
805 if (this.focusElementIndex_
>= 0)
806 this.focusableElements_
[this.focusElementIndex_
].focus();
810 * The current focus element is that element which is eligible for focus.
811 * @type {HTMLElement} The node.
814 get currentFocusElement_() {
815 return this.querySelector('.focusable[tabindex="1"]');
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.
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
),
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;
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
,
857 // We need to update the top margin as well.
858 this.updateTopMargin_();
860 this.firePageLayoutEvent_();
864 * Dispatches the custom pagelayout event.
867 firePageLayoutEvent_: function() {
868 cr
.dispatchSimpleEvent(this, 'pagelayout', true, true);
872 * @return {number} The amount of margin that should be animated (in pixels)
873 * for the current grid layout.
875 getAnimatedLeftMargin_: function() {
876 if (this.layoutValues_
.wide
)
879 var grid
= this.gridValues_
;
880 return (grid
.minWideWidth
- MIN_WIDE_MARGIN
- grid
.narrowWidth
) / 2;
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.
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
,
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
);
929 tile
.clearDoppleganger();
932 if (index
== this.tileElements_
.length
- 1) {
933 this.tileGrid_
.style
.height
= (realY
+ layout
.rowHeight
) + 'px';
934 this.queueUpdateScrollbars_();
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
945 * @param {number} y The y coordinate, in pixels, relative to the top of
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
) /
957 if (col
< 0 || col
>= layout
.numRowTiles
)
961 col
= layout
.numRowTiles
- 1 - col
;
963 var row
= Math
.floor((y
- gridClientRect
.top
) / layout
.rowHeight
);
964 return row
* layout
.numRowTiles
+ col
;
968 * Window resize event handler. Window resizes may trigger re-layouts.
969 * @param {Object} e The resize event.
971 onResize_: function(e
) {
972 if (this.lastWidth_
== this.clientWidth
&&
973 this.lastHeight_
== this.clientHeight
) {
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_();
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.
994 updateMask_: function() {
995 if (!this.isCurrentDragTarget
) {
996 this.tileGrid_
.style
.WebkitMaskBoxImage
= '';
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
1010 '-webkit-linear-gradient(left,' +
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, ' +
1018 this.tileGrid_
.style
.WebkitMaskBoxImage
= gradient
;
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
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_
);
1053 this.topMarginIsForWide_
= layout
.wide
;
1054 this.topMarginPx_
= newMargin
;
1055 this.topMargin_
.style
.marginTop
=
1056 toCssPx(this.topMarginPx_
- this.animatedTopMarginPx_
);
1060 * Position the notification if there's one showing.
1062 positionNotification_: function() {
1063 var notification
= this.notification
;
1064 if (!notification
|| notification
.hidden
)
1067 // Update the horizontal position.
1068 var animatedLeftMargin
= this.getAnimatedLeftMargin_();
1069 notification
.style
.WebkitMarginStart
= animatedLeftMargin
+ 'px';
1070 var leftOffset
= (this.layoutValues_
.leftMargin
- animatedLeftMargin
) *
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';
1089 * Handles final setup that can only happen after |this| is inserted into
1093 onNodeInsertedIntoDocument_: function(e
) {
1094 this.calculateLayoutValues_();
1095 this.heightChanged_();
1099 * Called when the height of |this| has changed: update the size of
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';
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.
1119 appendFooter: function(footerNode
) {
1120 this.footerNode_
= footerNode
;
1121 this.content_
.appendChild(footerNode
);
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
1132 * @param {Event} e The mousewheel event.
1134 handleMouseWheel: function(e
) {
1135 // The ctrl-wheel should triggle the zoom in/out actions in Chromium for
1137 if (e
.wheelDeltaY
== 0 || e
.ctrlKey
)
1140 this.content_
.scrollTop
-= e
.wheelDeltaY
/ 3;
1145 * Handler for the 'scroll' event on |content_|.
1146 * @param {Event} e The scroll event.
1149 onScroll_: function(e
) {
1150 this.queueUpdateScrollbars_();
1154 * ID of scrollbar update timer. If 0, there's no scrollbar re-calc queued.
1157 scrollbarUpdate_
: 0,
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.
1166 queueUpdateScrollbars_: function() {
1167 if (this.scrollbarUpdate_
)
1170 this.scrollbarUpdate_
= window
.setTimeout(
1171 this.doUpdateScrollbars_
.bind(this), 0);
1175 * Does the work of calculating the visibility, height and position of the
1176 * scrollbar thumb (there is no track or buttons).
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;
1191 this.scrollbar_
.hidden
= false;
1194 var thumbTop
= content
.offsetTop
+
1195 content
.scrollTop
/ adjustedScrollHeight
* content
.clientHeight
;
1196 var thumbHeight
= content
.clientHeight
/ adjustedScrollHeight
*
1199 this.scrollbar_
.style
.top
= thumbTop
+ 'px';
1200 this.scrollbar_
.style
.height
= thumbHeight
+ 'px';
1201 this.firePageLayoutEvent_();
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|.
1210 heightForWidth: function(width
) {
1216 get isCurrentDragTarget() {
1217 return this.dragWrapper_
.isCurrentDragTarget
;
1221 * Thunk for dragleave events fired on |tileGrid_|.
1222 * @param {Event} e A MouseEvent for the drag.
1224 doDragLeave: function(e
) {
1229 * Performs all actions necessary when a drag enters the tile page.
1230 * @param {Event} e A mouseover event for the drag enter.
1232 doDragEnter: function(e
) {
1233 // Applies the mask so doppleganger tiles disappear into the fog.
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
1244 if (!this.withinPageDrag_
)
1245 this.updateTopMargin_();
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.
1255 doDragOver: function(e
) {
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
);
1266 * Performs all actions necessary when the user completes a drop.
1267 * @param {Event} e A mouseover event for the drag drop.
1269 doDrop: function(e
) {
1270 e
.stopPropagation();
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_
);
1284 var originalPage
= currentlyDraggingTile
?
1285 currentlyDraggingTile
.tilePage
: null;
1286 this.addDragData(e
.dataTransfer
, adjustedIndex
);
1288 originalPage
.cleanupDrag();
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.
1297 this.classList
.remove('animating-tile-page');
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.
1305 appendDraggingTile: function() {
1306 var originalPage
= currentlyDraggingTile
.tilePage
;
1307 if (originalPage
== this)
1310 this.addDragData(null, this.tileElements_
.length
);
1312 originalPage
.cleanupDrag();
1316 * Makes sure all the tiles are in the right place after a drag is over.
1318 cleanupDrag: function() {
1319 this.repositionTiles_(currentlyDraggingTile
);
1320 // Remove the drag mask.
1325 * Reposition all the tiles (possibly ignoring one).
1326 * @param {Node=} opt_ignoreNode An optional node to ignore.
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
);
1337 * Updates the visual indicator for the drop location for the active drag.
1338 * @param {number} newDragIndex
1341 updateDropIndicator_: function(newDragIndex
) {
1342 var oldDragIndex
= this.currentDropIndex_
;
1343 if (newDragIndex
== oldDragIndex
)
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_
)
1352 else if (i
> this.dragItemIndex_
)
1353 var adjustment
= i
<= newDragIndex
? -1 : 0;
1355 var adjustment
= i
>= newDragIndex
? 1 : 0;
1357 this.positionTile_(i
, adjustment
);
1359 this.currentDropIndex_
= newDragIndex
;
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.
1368 shouldAcceptDrag: function(e
) {
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.
1378 addDragData: function(dataTransfer
, index
) {
1383 * Called when a tile has been moved (via dragging). Override this to make
1385 * @param {Node} draggedTile The tile that was dropped.
1386 * @param {number} prevIndex The previous index of the tile.
1388 tileMoved: function(draggedTile
, prevIndex
) {
1392 * Sets the drop effect on |dataTransfer| to the desired value (e.g.
1394 * @param {Object} dataTransfer The drag event dataTransfer object.
1396 setDropEffect: function(dataTransfer
) {
1402 getCurrentlyDraggingTile
: getCurrentlyDraggingTile
,
1403 setCurrentDropEffect
: setCurrentDropEffect
,
1404 // Not used outside, just for usage in JSDoc inside this file.