blob: c8862cc7be028e9d78cc0ba2b62c26e31b2d91eb [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
cr.define('ntp', function() {
'use strict';
/**
* The maximum gap from the edge of the scrolling area which will display
* the shadow with transparency. After this point the shadow will become
* 100% opaque.
* @type {number}
* @const
*/
var MAX_SCROLL_SHADOW_GAP = 16;
/**
* @type {number}
* @const
*/
var SCROLL_BAR_WIDTH = 12;
//----------------------------------------------------------------------------
// Tile
//----------------------------------------------------------------------------
/**
* A virtual Tile class. Each TilePage subclass should have its own Tile
* subclass implemented too (e.g. MostVisitedPage contains MostVisited
* tiles, and MostVisited is a Tile subclass).
* @constructor
*/
function Tile() {
console.error('Tile is a virtual class and is not supposed to be ' +
'instantiated');
}
/**
* Creates a Tile subclass. We need to use this function to create a Tile
* subclass because a Tile must also subclass a HTMLElement (which can be
* any HTMLElement), so we need to individually add methods and getters here.
* @param {Object} Subclass The prototype object of the class we want to be
* a Tile subclass.
* @param {Object} The extended Subclass object.
*/
Tile.subclass = function(Subclass) {
var Base = Tile.prototype;
for (var name in Base) {
if (!Subclass.hasOwnProperty(name))
Subclass[name] = Base[name];
}
for (var name in TileGetters) {
if (!Subclass.hasOwnProperty(name))
Subclass.__defineGetter__(name, TileGetters[name]);
}
return Subclass;
};
Tile.prototype = {
// Tile data object.
data_: null,
/**
* Initializes a Tile.
*/
initialize: function() {
this.classList.add('tile');
this.reset();
},
/**
* Resets the tile DOM.
*/
reset: function() {
},
/**
* The data for this Tile.
* @param {Object} data A dictionary of relevant data for the page.
*/
set data(data) {
// TODO(pedrosimonetti): Remove data.filler usage everywhere.
if (!data || data.filler) {
if (this.data_)
this.reset();
return;
}
this.data_ = data;
},
};
var TileGetters = {
/**
* The TileCell associated to this Tile.
* @type {TileCell}
*/
'tileCell': function() {
return findAncestorByClass(this, 'tile-cell');
},
/**
* The index of the Tile.
* @type {number}
*/
'index': function() {
assert(this.tileCell);
return this.tileCell.index;
},
};
//----------------------------------------------------------------------------
// TileCell
//----------------------------------------------------------------------------
/**
* Creates a new TileCell object. A TileCell represents a cell in the
* TilePage's grid. A TilePage uses TileCells to position Tiles in the proper
* place and to animate them individually. Each TileCell is associated to
* one Tile at a time (or none if it is a filler object), and that association
* might change when the grid is resized. When that happens, the grid is
* updated and the Tiles are moved to the proper TileCell. We cannot move the
* the TileCell itself during the resize because this transition is animated
* with CSS and there's no way to stop CSS animations, and we really want to
* animate with CSS to take advantage of hardware acceleration.
* @constructor
* @extends {HTMLDivElement}
* @param {HTMLElement} tile Tile element that will be associated to the cell.
*/
function TileCell(tile) {
var tileCell = cr.doc.createElement('div');
tileCell.__proto__ = TileCell.prototype;
tileCell.initialize(tile);
return tileCell;
}
TileCell.prototype = {
__proto__: HTMLDivElement.prototype,
/**
* Initializes a TileCell.
* @param {Tile} tile The Tile that will be assigned to this TileCell.
*/
initialize: function(tile) {
this.className = 'tile-cell';
this.assign(tile);
},
/**
* The index of the TileCell.
* @type {number}
*/
get index() {
return Array.prototype.indexOf.call(this.tilePage.tiles_,
this.tile);
},
/**
* The Tile associated to this TileCell.
* @type {Tile}
*/
get tile() {
return this.firstElementChild;
},
/**
* The TilePage associated to this TileCell.
* @type {TilePage}
*/
get tilePage() {
return findAncestorByClass(this, 'tile-page');
},
/**
* Assigns a Tile to the this TileCell.
* @type {TilePage}
*/
assign: function(tile) {
if (this.tile)
this.replaceChild(tile, this.tile);
else
this.appendChild(tile);
},
/**
* Called when an app is removed from Chrome. Animates its disappearance.
* @param {boolean=} opt_animate Whether the animation should be animated.
*/
doRemove: function(opt_animate) {
this.tilePage.removeTile(this.tile, false);
},
};
//----------------------------------------------------------------------------
// TilePage
//----------------------------------------------------------------------------
/**
* Creates a new TilePage object. This object contains tiles and controls
* their layout.
* @constructor
* @extends {HTMLDivElement}
*/
function TilePage() {
var el = cr.doc.createElement('div');
el.__proto__ = TilePage.prototype;
return el;
}
TilePage.prototype = {
__proto__: HTMLDivElement.prototype,
/**
* Reference to the Tile subclass that will be used to create the tiles.
* @constructor
* @extends {Tile}
*/
TileClass: Tile,
// The config object should be defined by a TilePage subclass if it
// wants the non-default behavior.
config: {
// The width of a cell.
cellWidth: 110,
// The start margin of a cell (left or right according to text direction).
cellMarginStart: 12,
// The maximum number of Tiles to be displayed.
maxTileCount: 6,
// Whether the TilePage content will be scrollable.
scrollable: false,
},
/**
* Initializes a TilePage.
*/
initialize: function() {
this.className = 'tile-page';
// The div that wraps the scrollable element.
this.frame_ = this.ownerDocument.createElement('div');
this.frame_.className = 'tile-page-frame';
this.appendChild(this.frame_);
// The content/scrollable element.
this.content_ = this.ownerDocument.createElement('div');
this.content_.className = 'tile-page-content';
this.frame_.appendChild(this.content_);
if (this.config.scrollable) {
this.content_.classList.add('scrollable');
// The scrollable shadow top.
this.shadowTop_ = this.ownerDocument.createElement('div');
this.shadowTop_.className = 'shadow-top';
this.content_.appendChild(this.shadowTop_);
// The scrollable shadow bottom.
this.shadowBottom_ = this.ownerDocument.createElement('div');
this.shadowBottom_.className = 'shadow-bottom';
this.content_.appendChild(this.shadowBottom_);
}
// The div that defines the tile grid viewport.
this.tileGrid_ = this.ownerDocument.createElement('div');
this.tileGrid_.className = 'tile-grid';
this.content_.appendChild(this.tileGrid_);
// The tile grid contents, which can be scrolled.
this.tileGridContent_ = this.ownerDocument.createElement('div');
this.tileGridContent_.className = 'tile-grid-content';
this.tileGrid_.appendChild(this.tileGridContent_);
// The list of Tile elements which is used to fill the TileGrid cells.
this.tiles_ = [];
// TODO(pedrosimonetti): Check duplication of these methods.
this.addEventListener('cardselected', this.handleCardSelection_);
this.addEventListener('carddeselected', this.handleCardDeselection_);
this.tileGrid_.addEventListener('webkitTransitionEnd',
this.onTileGridTransitionEnd_.bind(this));
this.content_.addEventListener('scroll', this.onScroll.bind(this));
},
/**
* The list of Tile elements.
* @type {Array<Tile>}
*/
get tiles() {
return this.tiles_;
},
/**
* The number of Tiles in this TilePage.
* @type {number}
*/
get tileCount() {
return this.tiles_.length;
},
/**
* Whether or not this TilePage is selected.
* @type {boolean}
*/
get selected() {
return Array.prototype.indexOf.call(this.parentNode.children, this) ==
ntp.getCardSlider().currentCard;
},
/**
* Removes the tilePage from the DOM and cleans up event handlers.
*/
remove: function() {
// This checks arguments.length as most remove functions have a boolean
// |opt_animate| argument, but that's not necesarilly applicable to
// removing a tilePage. Selecting a different card in an animated way and
// deleting the card afterward is probably a better choice.
assert(typeof arguments[0] != 'boolean',
'This function takes no |opt_animate| argument.');
this.parentNode.removeChild(this);
},
/**
* Notify interested subscribers that a tile has been removed from this
* page.
* @param {Tile} tile The newly added tile.
* @param {number} index The index of the tile that was added.
* @param {boolean} wasAnimated Whether the removal was animated.
*/
fireAddedEvent: function(tile, index, wasAnimated) {
var e = document.createEvent('Event');
e.initEvent('tilePage:tile_added', true, true);
e.addedIndex = index;
e.addedTile = tile;
e.wasAnimated = wasAnimated;
this.dispatchEvent(e);
},
/**
* Removes the given tile and animates the repositioning of the other tiles.
* @param {boolean=} opt_animate Whether the removal should be animated.
* @param {boolean=} opt_dontNotify Whether a page should be removed if the
* last tile is removed from it.
*/
removeTile: function(tile, opt_animate, opt_dontNotify) {
var tiles = this.tiles;
var index = tiles.indexOf(tile);
tile.parentNode.removeChild(tile);
tiles.splice(index, 1);
this.renderGrid();
if (!opt_dontNotify)
this.fireRemovedEvent(tile, index, !!opt_animate);
},
/**
* Notify interested subscribers that a tile has been removed from this
* page.
* @param {TileCell} tile The tile that was removed.
* @param {number} oldIndex Where the tile was positioned before removal.
* @param {boolean} wasAnimated Whether the removal was animated.
*/
fireRemovedEvent: function(tile, oldIndex, wasAnimated) {
var e = document.createEvent('Event');
e.initEvent('tilePage:tile_removed', true, true);
e.removedIndex = oldIndex;
e.removedTile = tile;
e.wasAnimated = wasAnimated;
this.dispatchEvent(e);
},
/**
* Removes all tiles from the page.
*/
removeAllTiles: function() {
while (this.tiles.length > 0) {
this.removeTile(this.tiles[this.tiles.length - 1]);
}
},
/**
* Called when the page is selected (in the card selector).
* @param {Event} e A custom cardselected event.
* @private
*/
handleCardSelection_: function(e) {
ntp.layout();
},
/**
* Called when the page loses selection (in the card selector).
* @param {Event} e A custom carddeselected event.
* @private
*/
handleCardDeselection_: function(e) {
},
// #########################################################################
// Extended Chrome Instant
// #########################################################################
// properties
// -------------------------------------------------------------------------
// The number of columns.
colCount_: 0,
// The number of rows.
rowCount_: 0,
// The number of visible rows. We initialize this value with zero so
// we can detect when the first time the page is rendered.
numOfVisibleRows_: 1,
// The number of the last column being animated. We initialize this value
// with zero so we can detect when the first time the page is rendered.
animatingColCount_: 0,
// The index of the topmost row visible.
pageOffset_: 0,
// Data object representing the tiles.
dataList_: null,
/**
* Appends a tile to the end of the tile grid.
* @param {Tile} tile The tile to be added.
* @param {number} index The location in the tile grid to insert it at.
* @protected
*/
appendTile: function(tile) {
var index = this.tiles_.length;
this.addTileAt(tile, index);
},
/**
* Adds the given element to the tile grid.
* @param {Tile} tile The tile to be added.
* @param {number} index The location in the tile grid to insert it at.
* @protected
*/
addTileAt: function(tile, index) {
this.tiles_.splice(index, 0, tile);
this.fireAddedEvent(tile, index, false);
this.renderGrid();
},
/**
* Create a blank tile.
* @protected
*/
createTile_: function() {
return new this.TileClass();
},
/**
* Create blank tiles.
* @param {number} count The desired number of Tiles to be created. If this
* value the maximum value defined in |config.maxTileCount|, the maximum
* value will be used instead.
* @protected
*/
createTiles_: function(count) {
count = Math.min(count, this.config.maxTileCount);
for (var i = 0; i < count; i++) {
this.appendTile(this.createTile_());
}
},
/**
* This method will create/remove necessary/unnecessary tiles, render the
* grid when the number of tiles has changed, and finally will call
* |updateTiles_| which will in turn render the individual tiles.
* @protected
*/
updateGrid: function() {
var dataListLength = this.dataList_.length;
var tileCount = this.tileCount;
// Create or remove tiles if necessary.
if (tileCount < dataListLength) {
this.createTiles_(dataListLength - tileCount);
} else if (tileCount > dataListLength) {
var tiles = this.tiles_;
while (tiles.length > dataListLength) {
var previousLength = tiles.length;
// It doesn't matter which tiles are being removed here because
// they're going to be reconstructed below when calling updateTiles_
// method, so the first tiles are being removed here.
this.removeTile(tiles[0]);
assert(tiles.length < previousLength);
}
}
this.updateTiles_();
},
/**
* Update the tiles after a change to |dataList_|.
*/
updateTiles_: function() {
var maxTileCount = this.config.maxTileCount;
var dataList = this.dataList_;
var tiles = this.tiles;
for (var i = 0; i < maxTileCount; i++) {
var data = dataList[i];
var tile = tiles[i];
// TODO(pedrosimonetti): What do we do when there's no tile here?
if (!tile)
return;
if (i >= dataList.length)
tile.reset();
else
tile.data = data;
}
},
/**
* Sets the dataList that will be used to create Tiles.
* TODO(pedrosimonetti): Use setters and getters instead.
*/
setDataList: function(dataList) {
this.dataList_ = dataList.slice(0, this.config.maxTileCount);
},
// internal helpers
// -------------------------------------------------------------------------
/**
* Gets the required width for a Tile.
* @private
*/
getTileRequiredWidth_: function() {
var config = this.config;
return config.cellWidth + config.cellMarginStart;
},
/**
* Gets the the maximum number of columns that can fit in a given width.
* @param {number} width The width in pixels.
* @private
*/
getColCountForWidth_: function(width) {
var scrollBarIsVisible = this.config.scrollable &&
this.content_.scrollHeight > this.content_.clientHeight;
var scrollBarWidth = scrollBarIsVisible ? SCROLL_BAR_WIDTH : 0;
var availableWidth = width + this.config.cellMarginStart - scrollBarWidth;
var requiredWidth = this.getTileRequiredWidth_();
var colCount = Math.floor(availableWidth / requiredWidth);
return colCount;
},
/**
* Gets the width for a given number of columns.
* @param {number} colCount The number of columns.
* @private
*/
getWidthForColCount_: function(colCount) {
var requiredWidth = this.getTileRequiredWidth_();
var width = colCount * requiredWidth - this.config.cellMarginStart;
return width;
},
/**
* Returns the position of the tile at |index|.
* @param {number} index Tile index.
* @private
* @return {!{top: number, left: number}} Position.
*/
getTilePosition_: function(index) {
var colCount = this.colCount_;
var row = Math.floor(index / colCount);
var col = index % colCount;
if (isRTL())
col = colCount - col - 1;
var config = this.config;
var top = ntp.TILE_ROW_HEIGHT * row;
var left = col * (config.cellWidth + config.cellMarginStart);
return {top: top, left: left};
},
// rendering
// -------------------------------------------------------------------------
/**
* Renders the tile grid, and the individual tiles. Rendering the grid
* consists of adding/removing tile rows and tile cells according to the
* specified size (defined by the number of columns in the grid). While
* rendering the grid, the tiles are rendered in order in their respective
* cells and tile fillers are rendered when needed. This method sets the
* private properties colCount_ and rowCount_.
*
* This method should be called every time the contents of the grid changes,
* that is, when the number, contents or order of the tiles has changed.
* @param {number=} opt_colCount The number of columns.
* @param {number=} opt_tileCount Forces a particular number of tiles to
* be drawn. This is useful for cases like the restoration/insertion
* of tiles when you need to place a tile in a place of the grid that
* is not rendered at the moment.
* @protected
*/
renderGrid: function(opt_colCount, opt_tileCount) {
var colCount = opt_colCount || this.colCount_;
var tileGridContent = this.tileGridContent_;
var tiles = this.tiles_;
var tileCount = opt_tileCount || tiles.length;
var rowCount = Math.ceil(tileCount / colCount);
var tileRows = tileGridContent.getElementsByClassName('tile-row');
for (var tile = 0, row = 0; row < rowCount; row++) {
var tileRow = tileRows[row];
// Create tile row if there's no one yet.
if (!tileRow) {
tileRow = cr.doc.createElement('div');
tileRow.className = 'tile-row';
tileGridContent.appendChild(tileRow);
}
// The tiles inside the current row.
var tileRowTiles = tileRow.childNodes;
// Remove excessive columns from a particular tile row.
var maxColCount = Math.min(colCount, tileCount - tile);
maxColCount = Math.max(0, maxColCount);
while (tileRowTiles.length > maxColCount) {
tileRow.removeChild(tileRow.lastElementChild);
}
// For each column in the current row.
for (var col = 0; col < colCount; col++, tile++) {
var tileCell;
var tileElement;
if (tileRowTiles[col]) {
tileCell = tileRowTiles[col];
} else {
var span = cr.doc.createElement('span');
tileCell = new TileCell(span);
}
// Render Tiles.
tileElement = tiles[tile];
if (tile < tileCount && tileElement) {
tileCell.classList.remove('filler');
if (!tileCell.tile)
tileCell.appendChild(tileElement);
else if (tileElement != tileCell.tile)
tileCell.replaceChild(tileElement, tileCell.tile);
} else if (!tileCell.classList.contains('filler')) {
tileCell.classList.add('filler');
tileElement = cr.doc.createElement('span');
tileElement.className = 'tile';
if (tileCell.tile)
tileCell.replaceChild(tileElement, tileCell.tile);
else
tileCell.appendChild(tileElement);
}
if (!tileRowTiles[col])
tileRow.appendChild(tileCell);
}
}
// Remove excessive tile rows from the tile grid.
while (tileRows.length > rowCount) {
tileGridContent.removeChild(tileGridContent.lastElementChild);
}
this.colCount_ = colCount;
this.rowCount_ = rowCount;
// If we are manually changing the tile count (which can happen during
// the restoration/insertion animation) we should not fire the scroll
// event once some cells might contain dummy tiles which will cause
// an error.
if (!opt_tileCount)
this.onScroll();
},
// layout
// -------------------------------------------------------------------------
/**
* Calculates the layout of the tile page according to the current Bottom
* Panel's size. This method will resize the containers of the tile page,
* and re-render the grid when its dimension changes (number of columns or
* visible rows changes). This method also sets the private properties
* |numOfVisibleRows_| and |animatingColCount_|.
*
* This method should be called every time the dimension of the grid changes
* or when you need to reinforce its dimension.
* @param {boolean=} opt_animate Whether the layout be animated.
*/
layout: function(opt_animate) {
var contentHeight = ntp.getContentHeight();
this.content_.style.height = contentHeight + 'px';
var contentWidth = ntp.getContentWidth();
var colCount = this.getColCountForWidth_(contentWidth);
var lastColCount = this.colCount_;
var animatingColCount = this.animatingColCount_;
if (colCount != animatingColCount) {
if (opt_animate)
this.tileGrid_.classList.add('animate-grid-width');
if (colCount > animatingColCount) {
// If the grid is expanding, it needs to be rendered first so the
// revealing tiles are visible as soon as the animation starts.
if (colCount != lastColCount)
this.renderGrid(colCount);
// Hides affected columns and forces the reflow.
this.showTileCols_(animatingColCount, false);
// Trigger reflow, making the tiles completely hidden.
this.tileGrid_.offsetTop;
// Fades in the affected columns.
this.showTileCols_(animatingColCount, true);
} else {
// Fades out the affected columns.
this.showTileCols_(colCount, false);
}
var newWidth = this.getWidthForColCount_(colCount);
this.tileGrid_.style.width = newWidth + 'px';
// TODO(pedrosimonetti): move to handler below.
var self = this;
this.onTileGridTransitionEndHandler_ = function() {
if (colCount < lastColCount)
self.renderGrid(colCount);
else
self.showTileCols_(0, true);
};
}
this.animatingColCount_ = colCount;
this.frame_.style.width = contentWidth + 'px';
this.onScroll();
},
// tile repositioning animation
// -------------------------------------------------------------------------
/**
* Tile repositioning state.
* @type {{index: number, isRemoving: number}}
*/
tileRepositioningState_: null,
/**
* Gets the repositioning state.
* @return {{index: number, isRemoving: number}} The repositioning data.
*/
getTileRepositioningState: function() {
return this.tileRepositioningState_;
},
/**
* Sets the repositioning state that will be used to animate the tiles.
* @param {number} index The tile's index.
* @param {boolean} isRemoving Whether the tile is being removed.
*/
setTileRepositioningState: function(index, isRemoving) {
this.tileRepositioningState_ = {
index: index,
isRemoving: isRemoving
};
},
/**
* Resets the repositioning state.
*/
resetTileRepositioningState: function() {
this.tileRepositioningState_ = null;
},
/**
* Animates a tile removal.
* @param {number} index The index of the tile to be removed.
* @param {Object} newDataList The new data list.
*/
animateTileRemoval: function(index, newDataList) {
var tiles = this.tiles_;
var tileCount = tiles.length;
assert(tileCount > 0);
var tileCells = this.querySelectorAll('.tile-cell');
var extraTileIndex = tileCount - 1;
var extraCell = tileCells[extraTileIndex];
var extraTileData = newDataList[extraTileIndex];
var repositioningStartIndex = index + 1;
var repositioningEndIndex = tileCount;
this.initializeRepositioningAnimation_(index, repositioningEndIndex,
true);
var tileBeingRemoved = tiles[index];
tileBeingRemoved.scrollTop;
// The extra tile is the new one that will appear. It can be a normal
// tile (when there's extra data for it), or a filler tile.
var extraTile = createTile(this, extraTileData);
if (!extraTileData)
extraCell.classList.add('filler');
// The extra tile is being assigned in order to put it in the right spot.
extraCell.assign(extraTile);
this.executeRepositioningAnimation_(tileBeingRemoved, extraTile,
repositioningStartIndex, repositioningEndIndex, true);
// Cleans up the animation.
var onPositioningTransitionEnd = function(e) {
var propertyName = e.propertyName;
if (!(propertyName == '-webkit-transform' ||
propertyName == 'opacity')) {
return;
}
lastAnimatingTile.removeEventListener('webkitTransitionEnd',
onPositioningTransitionEnd);
this.finalizeRepositioningAnimation_(tileBeingRemoved,
repositioningStartIndex, repositioningEndIndex, true);
this.removeTile(tileBeingRemoved);
// If the extra tile is a real one (not a filler), then it needs to be
// added to the tile list. The tile has been placed in the right spot
// but the tile page still doesn't know about this new tile.
if (extraTileData)
this.appendTile(extraTile);
}.bind(this);
// Listens to the animation end.
var lastAnimatingTile = extraTile;
lastAnimatingTile.addEventListener('webkitTransitionEnd',
onPositioningTransitionEnd);
},
/**
* Animates a tile restoration.
* @param {number} index The index of the tile to be restored.
* @param {Object} newDataList The new data list.
*/
animateTileRestoration: function(index, newDataList) {
var tiles = this.tiles_;
var tileCount = tiles.length;
var tileCells = this.getElementsByClassName('tile-cell');
// If the desired position is outside the grid, then the grid must be
// expanded so there will be a cell in the desired position.
if (index >= tileCells.length)
this.renderGrid(null, index + 1);
var extraTileIndex = Math.min(tileCount, this.config.maxTileCount - 1);
var extraCell = tileCells[extraTileIndex];
var extraTileData = newDataList[extraTileIndex + 1];
var repositioningStartIndex = index;
var repositioningEndIndex = tileCount - (extraTileData ? 1 : 0);
this.initializeRepositioningAnimation_(index, repositioningEndIndex);
var restoredData = newDataList[index];
var tileBeingRestored = createTile(this, restoredData);
// Temporarily assume the |index| cell so the tile can be animated in
// the right spot.
tileCells[index].appendChild(tileBeingRestored);
if (this.config.scrollable)
this.content_.scrollTop = tileCells[index].offsetTop;
var extraTile;
if (extraCell)
extraTile = extraCell.tile;
this.executeRepositioningAnimation_(tileBeingRestored, extraTile,
repositioningStartIndex, repositioningEndIndex, false);
// Cleans up the animation.
var onPositioningTransitionEnd = function(e) {
var propertyName = e.propertyName;
if (!(propertyName == '-webkit-transform' ||
propertyName == 'opacity')) {
return;
}
lastAnimatingTile.removeEventListener('webkitTransitionEnd',
onPositioningTransitionEnd);
// When there's an extra data, it means the tile is a real one (not a
// filler), and therefore it needs to be removed from the tile list.
if (extraTileData)
this.removeTile(extraTile);
this.finalizeRepositioningAnimation_(tileBeingRestored,
repositioningStartIndex, repositioningEndIndex, false);
this.addTileAt(tileBeingRestored, index);
}.bind(this);
// Listens to the animation end.
var lastAnimatingTile = tileBeingRestored;
lastAnimatingTile.addEventListener('webkitTransitionEnd',
onPositioningTransitionEnd);
},
// animation helpers
// -------------------------------------------------------------------------
/**
* Moves a tile to a new position.
* @param {Tile} tile A tile.
* @param {number} left Left coordinate.
* @param {number} top Top coordinate.
* @private
*/
moveTileTo_: function(tile, left, top) {
tile.style.left = left + 'px';
tile.style.top = top + 'px';
},
/**
* Resets a tile's position.
* @param {Tile} tile A tile.
* @private
*/
resetTilePosition_: function(tile) {
tile.style.left = '';
tile.style.top = '';
},
/**
* Initializes the repositioning animation.
* @param {number} startIndex Index of the first tile to be repositioned.
* @param {number} endIndex Index of the last tile to be repositioned.
* @param {boolean} isRemoving Whether the tile is being removed.
* @private
*/
initializeRepositioningAnimation_: function(startIndex, endIndex,
isRemoving) {
// Move tiles from relative to absolute position.
var tiles = this.tiles_;
var tileGridContent = this.tileGridContent_;
for (var i = startIndex; i < endIndex; i++) {
var tile = tiles[i];
var position = this.getTilePosition_(i);
this.moveTileTo_(tile, position.left, position.top);
tile.style.zIndex = endIndex - i;
tileGridContent.appendChild(tile);
}
tileGridContent.classList.add('animate-tile-repositioning');
if (!isRemoving)
tileGridContent.classList.add('undo-removal');
},
/**
* Executes the repositioning animation.
* @param {Tile} targetTile The tile that is being removed/restored.
* @param {Tile} extraTile The extra tile that is going to appear/disappear.
* @param {number} startIndex Index of the first tile to be repositioned.
* @param {number} endIndex Index of the last tile to be repositioned.
* @param {boolean} isRemoving Whether the tile is being removed.
* @private
*/
executeRepositioningAnimation_: function(targetTile, extraTile, startIndex,
endIndex, isRemoving) {
targetTile.classList.add('target-tile');
// Alternate the visualization of the target and extra tiles.
fadeTile(targetTile, !isRemoving);
if (extraTile)
fadeTile(extraTile, isRemoving);
// Move tiles to the new position.
var tiles = this.tiles_;
var positionDiff = isRemoving ? -1 : 1;
for (var i = startIndex; i < endIndex; i++) {
var position = this.getTilePosition_(i + positionDiff);
this.moveTileTo_(tiles[i], position.left, position.top);
}
},
/**
* Finalizes the repositioning animation.
* @param {Tile} targetTile The tile that is being removed/restored.
* @param {number} startIndex Index of the first tile to be repositioned.
* @param {number} endIndex Index of the last tile to be repositioned.
* @param {boolean} isRemoving Whether the tile is being removed.
* @private
*/
finalizeRepositioningAnimation_: function(targetTile, startIndex, endIndex,
isRemoving) {
// Remove temporary class names.
var tileGridContent = this.tileGridContent_;
tileGridContent.classList.remove('animate-tile-repositioning');
tileGridContent.classList.remove('undo-removal');
targetTile.classList.remove('target-tile');
// Move tiles back to relative position.
var tiles = this.tiles_;
var tileCells = this.querySelectorAll('.tile-cell');
var positionDiff = isRemoving ? -1 : 1;
for (var i = startIndex; i < endIndex; i++) {
var tile = tiles[i];
this.resetTilePosition_(tile);
tile.style.zIndex = '';
var tileCell = tileCells[i + positionDiff];
if (tileCell)
tileCell.assign(tile);
}
},
/**
* Animates the display of columns.
* @param {number} col The column number.
* @param {boolean} show Whether or not to show the row.
*/
showTileCols_: function(col, show) {
var prop = show ? 'remove' : 'add';
var max = 10; // TODO(pedrosimonetti): Add const?
var tileGridContent = this.tileGridContent_;
for (var i = col; i < max; i++) {
tileGridContent.classList[prop]('hide-col-' + i);
}
},
// event handlers
// -------------------------------------------------------------------------
/**
* Handles the scroll event.
* @protected
*/
onScroll: function() {
// If the TilePage is scrollable, then the opacity of shadow top and
// bottom must adjusted, indicating when there's an overflow content.
if (this.config.scrollable) {
var content = this.content_;
var topGap = Math.min(MAX_SCROLL_SHADOW_GAP, content.scrollTop);
var bottomGap = Math.min(MAX_SCROLL_SHADOW_GAP, content.scrollHeight -
content.scrollTop - content.clientHeight);
this.shadowTop_.style.opacity = topGap / MAX_SCROLL_SHADOW_GAP;
this.shadowBottom_.style.opacity = bottomGap / MAX_SCROLL_SHADOW_GAP;
}
},
/**
* Handles the end of the horizontal tile grid transition.
* @param {Event} e The tile grid webkitTransitionEnd event.
*/
onTileGridTransitionEnd_: function(e) {
if (!this.selected)
return;
// We should remove the classes that control transitions when the
// transition ends so when the text is resized (Ctrl + '+'), no other
// transition should happen except those defined in the specification.
// For example, the tile has a transition for its 'width' property which
// is used when the tile is being hidden. But when you resize the text,
// and therefore the tile changes its 'width', this change should not be
// animated.
// When the tile grid width transition ends, we need to remove the class
// 'animate-grid-width' which handles the tile grid width transition, and
// individual tile transitions. TODO(pedrosimonetti): Investigate if we
// can improve the performance here by using a more efficient selector.
var tileGrid = this.tileGrid_;
if (e.target == tileGrid &&
tileGrid.classList.contains('animate-grid-width')) {
tileGrid.classList.remove('animate-grid-width');
if (this.onTileGridTransitionEndHandler_)
this.onTileGridTransitionEndHandler_();
}
},
};
/**
* Creates a new tile given a particular data. If there's no data, then
* a tile filler will be created.
* @param {TilePage} tilePage A TilePage.
* @param {Object=} opt_data The data that will be used to create the tile.
* @return {Tile} The new tile.
*/
function createTile(tilePage, opt_data) {
var tile;
if (opt_data) {
// If there's data, the new tile will be a real one (not a filler).
tile = new tilePage.TileClass(opt_data);
} else {
// Otherwise, it will be a fake filler tile.
tile = cr.doc.createElement('span');
tile.className = 'tile';
}
return tile;
}
/**
* Fades a tile.
* @param {Tile} tile A Tile.
* @param {boolean} isFadeIn Whether to fade-in the tile. If |isFadeIn| is
* false, then the tile is going to fade-out.
*/
function fadeTile(tile, isFadeIn) {
var className = 'animate-hide-tile';
tile.classList.add(className);
if (isFadeIn) {
// Forces a reflow to ensure that the fade-out animation will work.
tile.scrollTop;
tile.classList.remove(className);
}
}
return {
Tile: Tile,
TilePage: TilePage,
};
});