OLD | NEW |
| (Empty) |
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | |
2 // Use of this source code is governed by a BSD-style license that can be | |
3 // found in the LICENSE file. | |
4 | |
5 cr.define('ntp', function() { | |
6 'use strict'; | |
7 | |
8 /** | |
9 * The maximum gap from the edge of the scrolling area which will display | |
10 * the shadow with transparency. After this point the shadow will become | |
11 * 100% opaque. | |
12 * @type {number} | |
13 * @const | |
14 */ | |
15 var MAX_SCROLL_SHADOW_GAP = 16; | |
16 | |
17 /** | |
18 * @type {number} | |
19 * @const | |
20 */ | |
21 var SCROLL_BAR_WIDTH = 12; | |
22 | |
23 //---------------------------------------------------------------------------- | |
24 // Tile | |
25 //---------------------------------------------------------------------------- | |
26 | |
27 /** | |
28 * A virtual Tile class. Each TilePage subclass should have its own Tile | |
29 * subclass implemented too (e.g. MostVisitedPage contains MostVisited | |
30 * tiles, and MostVisited is a Tile subclass). | |
31 * @constructor | |
32 */ | |
33 function Tile() { | |
34 console.error('Tile is a virtual class and is not supposed to be ' + | |
35 'instantiated'); | |
36 } | |
37 | |
38 /** | |
39 * Creates a Tile subclass. We need to use this function to create a Tile | |
40 * subclass because a Tile must also subclass a HTMLElement (which can be | |
41 * any HTMLElement), so we need to individually add methods and getters here. | |
42 * @param {Object} Subclass The prototype object of the class we want to be | |
43 * a Tile subclass. | |
44 * @param {Object} The extended Subclass object. | |
45 */ | |
46 Tile.subclass = function(Subclass) { | |
47 var Base = Tile.prototype; | |
48 for (var name in Base) { | |
49 if (!Subclass.hasOwnProperty(name)) | |
50 Subclass[name] = Base[name]; | |
51 } | |
52 for (var name in TileGetters) { | |
53 if (!Subclass.hasOwnProperty(name)) | |
54 Subclass.__defineGetter__(name, TileGetters[name]); | |
55 } | |
56 return Subclass; | |
57 }; | |
58 | |
59 Tile.prototype = { | |
60 // Tile data object. | |
61 data_: null, | |
62 | |
63 /** | |
64 * Initializes a Tile. | |
65 */ | |
66 initialize: function() { | |
67 this.classList.add('tile'); | |
68 this.reset(); | |
69 }, | |
70 | |
71 /** | |
72 * Resets the tile DOM. | |
73 */ | |
74 reset: function() { | |
75 }, | |
76 | |
77 /** | |
78 * The data for this Tile. | |
79 * @param {Object} data A dictionary of relevant data for the page. | |
80 */ | |
81 set data(data) { | |
82 // TODO(pedrosimonetti): Remove data.filler usage everywhere. | |
83 if (!data || data.filler) { | |
84 if (this.data_) | |
85 this.reset(); | |
86 return; | |
87 } | |
88 | |
89 this.data_ = data; | |
90 }, | |
91 }; | |
92 | |
93 var TileGetters = { | |
94 /** | |
95 * The TileCell associated to this Tile. | |
96 * @type {TileCell} | |
97 */ | |
98 'tileCell': function() { | |
99 return findAncestorByClass(this, 'tile-cell'); | |
100 }, | |
101 | |
102 /** | |
103 * The index of the Tile. | |
104 * @type {number} | |
105 */ | |
106 'index': function() { | |
107 assert(this.tileCell); | |
108 return this.tileCell.index; | |
109 }, | |
110 }; | |
111 | |
112 //---------------------------------------------------------------------------- | |
113 // TileCell | |
114 //---------------------------------------------------------------------------- | |
115 | |
116 /** | |
117 * Creates a new TileCell object. A TileCell represents a cell in the | |
118 * TilePage's grid. A TilePage uses TileCells to position Tiles in the proper | |
119 * place and to animate them individually. Each TileCell is associated to | |
120 * one Tile at a time (or none if it is a filler object), and that association | |
121 * might change when the grid is resized. When that happens, the grid is | |
122 * updated and the Tiles are moved to the proper TileCell. We cannot move the | |
123 * the TileCell itself during the resize because this transition is animated | |
124 * with CSS and there's no way to stop CSS animations, and we really want to | |
125 * animate with CSS to take advantage of hardware acceleration. | |
126 * @constructor | |
127 * @extends {HTMLDivElement} | |
128 * @param {HTMLElement} tile Tile element that will be associated to the cell. | |
129 */ | |
130 function TileCell(tile) { | |
131 var tileCell = cr.doc.createElement('div'); | |
132 tileCell.__proto__ = TileCell.prototype; | |
133 tileCell.initialize(tile); | |
134 | |
135 return tileCell; | |
136 } | |
137 | |
138 TileCell.prototype = { | |
139 __proto__: HTMLDivElement.prototype, | |
140 | |
141 /** | |
142 * Initializes a TileCell. | |
143 * @param {Tile} tile The Tile that will be assigned to this TileCell. | |
144 */ | |
145 initialize: function(tile) { | |
146 this.className = 'tile-cell'; | |
147 this.assign(tile); | |
148 }, | |
149 | |
150 /** | |
151 * The index of the TileCell. | |
152 * @type {number} | |
153 */ | |
154 get index() { | |
155 return Array.prototype.indexOf.call(this.tilePage.tiles_, | |
156 this.tile); | |
157 }, | |
158 | |
159 /** | |
160 * The Tile associated to this TileCell. | |
161 * @type {Tile} | |
162 */ | |
163 get tile() { | |
164 return this.firstElementChild; | |
165 }, | |
166 | |
167 /** | |
168 * The TilePage associated to this TileCell. | |
169 * @type {TilePage} | |
170 */ | |
171 get tilePage() { | |
172 return findAncestorByClass(this, 'tile-page'); | |
173 }, | |
174 | |
175 /** | |
176 * Assigns a Tile to the this TileCell. | |
177 * @type {TilePage} | |
178 */ | |
179 assign: function(tile) { | |
180 if (this.tile) | |
181 this.replaceChild(tile, this.tile); | |
182 else | |
183 this.appendChild(tile); | |
184 }, | |
185 | |
186 /** | |
187 * Called when an app is removed from Chrome. Animates its disappearance. | |
188 * @param {boolean=} opt_animate Whether the animation should be animated. | |
189 */ | |
190 doRemove: function(opt_animate) { | |
191 this.tilePage.removeTile(this.tile, false); | |
192 }, | |
193 }; | |
194 | |
195 //---------------------------------------------------------------------------- | |
196 // TilePage | |
197 //---------------------------------------------------------------------------- | |
198 | |
199 /** | |
200 * Creates a new TilePage object. This object contains tiles and controls | |
201 * their layout. | |
202 * @constructor | |
203 * @extends {HTMLDivElement} | |
204 */ | |
205 function TilePage() { | |
206 var el = cr.doc.createElement('div'); | |
207 el.__proto__ = TilePage.prototype; | |
208 | |
209 return el; | |
210 } | |
211 | |
212 TilePage.prototype = { | |
213 __proto__: HTMLDivElement.prototype, | |
214 | |
215 /** | |
216 * Reference to the Tile subclass that will be used to create the tiles. | |
217 * @constructor | |
218 * @extends {Tile} | |
219 */ | |
220 TileClass: Tile, | |
221 | |
222 // The config object should be defined by a TilePage subclass if it | |
223 // wants the non-default behavior. | |
224 config: { | |
225 // The width of a cell. | |
226 cellWidth: 110, | |
227 // The start margin of a cell (left or right according to text direction). | |
228 cellMarginStart: 12, | |
229 // The maximum number of Tiles to be displayed. | |
230 maxTileCount: 6, | |
231 // Whether the TilePage content will be scrollable. | |
232 scrollable: false, | |
233 }, | |
234 | |
235 /** | |
236 * Initializes a TilePage. | |
237 */ | |
238 initialize: function() { | |
239 this.className = 'tile-page'; | |
240 | |
241 // The div that wraps the scrollable element. | |
242 this.frame_ = this.ownerDocument.createElement('div'); | |
243 this.frame_.className = 'tile-page-frame'; | |
244 this.appendChild(this.frame_); | |
245 | |
246 // The content/scrollable element. | |
247 this.content_ = this.ownerDocument.createElement('div'); | |
248 this.content_.className = 'tile-page-content'; | |
249 this.frame_.appendChild(this.content_); | |
250 | |
251 if (this.config.scrollable) { | |
252 this.content_.classList.add('scrollable'); | |
253 | |
254 // The scrollable shadow top. | |
255 this.shadowTop_ = this.ownerDocument.createElement('div'); | |
256 this.shadowTop_.className = 'shadow-top'; | |
257 this.content_.appendChild(this.shadowTop_); | |
258 | |
259 // The scrollable shadow bottom. | |
260 this.shadowBottom_ = this.ownerDocument.createElement('div'); | |
261 this.shadowBottom_.className = 'shadow-bottom'; | |
262 this.content_.appendChild(this.shadowBottom_); | |
263 } | |
264 | |
265 // The div that defines the tile grid viewport. | |
266 this.tileGrid_ = this.ownerDocument.createElement('div'); | |
267 this.tileGrid_.className = 'tile-grid'; | |
268 this.content_.appendChild(this.tileGrid_); | |
269 | |
270 // The tile grid contents, which can be scrolled. | |
271 this.tileGridContent_ = this.ownerDocument.createElement('div'); | |
272 this.tileGridContent_.className = 'tile-grid-content'; | |
273 this.tileGrid_.appendChild(this.tileGridContent_); | |
274 | |
275 // The list of Tile elements which is used to fill the TileGrid cells. | |
276 this.tiles_ = []; | |
277 | |
278 // TODO(pedrosimonetti): Check duplication of these methods. | |
279 this.addEventListener('cardselected', this.handleCardSelection_); | |
280 this.addEventListener('carddeselected', this.handleCardDeselection_); | |
281 | |
282 this.tileGrid_.addEventListener('webkitTransitionEnd', | |
283 this.onTileGridTransitionEnd_.bind(this)); | |
284 | |
285 this.content_.addEventListener('scroll', this.onScroll.bind(this)); | |
286 }, | |
287 | |
288 /** | |
289 * The list of Tile elements. | |
290 * @type {Array<Tile>} | |
291 */ | |
292 get tiles() { | |
293 return this.tiles_; | |
294 }, | |
295 | |
296 /** | |
297 * The number of Tiles in this TilePage. | |
298 * @type {number} | |
299 */ | |
300 get tileCount() { | |
301 return this.tiles_.length; | |
302 }, | |
303 | |
304 /** | |
305 * Whether or not this TilePage is selected. | |
306 * @type {boolean} | |
307 */ | |
308 get selected() { | |
309 return Array.prototype.indexOf.call(this.parentNode.children, this) == | |
310 ntp.getCardSlider().currentCard; | |
311 }, | |
312 | |
313 /** | |
314 * Removes the tilePage from the DOM and cleans up event handlers. | |
315 */ | |
316 remove: function() { | |
317 // This checks arguments.length as most remove functions have a boolean | |
318 // |opt_animate| argument, but that's not necesarilly applicable to | |
319 // removing a tilePage. Selecting a different card in an animated way and | |
320 // deleting the card afterward is probably a better choice. | |
321 assert(typeof arguments[0] != 'boolean', | |
322 'This function takes no |opt_animate| argument.'); | |
323 this.parentNode.removeChild(this); | |
324 }, | |
325 | |
326 /** | |
327 * Notify interested subscribers that a tile has been removed from this | |
328 * page. | |
329 * @param {Tile} tile The newly added tile. | |
330 * @param {number} index The index of the tile that was added. | |
331 * @param {boolean} wasAnimated Whether the removal was animated. | |
332 */ | |
333 fireAddedEvent: function(tile, index, wasAnimated) { | |
334 var e = document.createEvent('Event'); | |
335 e.initEvent('tilePage:tile_added', true, true); | |
336 e.addedIndex = index; | |
337 e.addedTile = tile; | |
338 e.wasAnimated = wasAnimated; | |
339 this.dispatchEvent(e); | |
340 }, | |
341 | |
342 /** | |
343 * Removes the given tile and animates the repositioning of the other tiles. | |
344 * @param {boolean=} opt_animate Whether the removal should be animated. | |
345 * @param {boolean=} opt_dontNotify Whether a page should be removed if the | |
346 * last tile is removed from it. | |
347 */ | |
348 removeTile: function(tile, opt_animate, opt_dontNotify) { | |
349 var tiles = this.tiles; | |
350 var index = tiles.indexOf(tile); | |
351 tile.parentNode.removeChild(tile); | |
352 tiles.splice(index, 1); | |
353 this.renderGrid(); | |
354 | |
355 if (!opt_dontNotify) | |
356 this.fireRemovedEvent(tile, index, !!opt_animate); | |
357 }, | |
358 | |
359 /** | |
360 * Notify interested subscribers that a tile has been removed from this | |
361 * page. | |
362 * @param {TileCell} tile The tile that was removed. | |
363 * @param {number} oldIndex Where the tile was positioned before removal. | |
364 * @param {boolean} wasAnimated Whether the removal was animated. | |
365 */ | |
366 fireRemovedEvent: function(tile, oldIndex, wasAnimated) { | |
367 var e = document.createEvent('Event'); | |
368 e.initEvent('tilePage:tile_removed', true, true); | |
369 e.removedIndex = oldIndex; | |
370 e.removedTile = tile; | |
371 e.wasAnimated = wasAnimated; | |
372 this.dispatchEvent(e); | |
373 }, | |
374 | |
375 /** | |
376 * Removes all tiles from the page. | |
377 */ | |
378 removeAllTiles: function() { | |
379 while (this.tiles.length > 0) { | |
380 this.removeTile(this.tiles[this.tiles.length - 1]); | |
381 } | |
382 }, | |
383 | |
384 /** | |
385 * Called when the page is selected (in the card selector). | |
386 * @param {Event} e A custom cardselected event. | |
387 * @private | |
388 */ | |
389 handleCardSelection_: function(e) { | |
390 ntp.layout(); | |
391 }, | |
392 | |
393 /** | |
394 * Called when the page loses selection (in the card selector). | |
395 * @param {Event} e A custom carddeselected event. | |
396 * @private | |
397 */ | |
398 handleCardDeselection_: function(e) { | |
399 }, | |
400 | |
401 // ######################################################################### | |
402 // Extended Chrome Instant | |
403 // ######################################################################### | |
404 | |
405 | |
406 // properties | |
407 // ------------------------------------------------------------------------- | |
408 | |
409 // The number of columns. | |
410 colCount_: 0, | |
411 // The number of rows. | |
412 rowCount_: 0, | |
413 // The number of visible rows. We initialize this value with zero so | |
414 // we can detect when the first time the page is rendered. | |
415 numOfVisibleRows_: 1, | |
416 // The number of the last column being animated. We initialize this value | |
417 // with zero so we can detect when the first time the page is rendered. | |
418 animatingColCount_: 0, | |
419 // The index of the topmost row visible. | |
420 pageOffset_: 0, | |
421 // Data object representing the tiles. | |
422 dataList_: null, | |
423 | |
424 /** | |
425 * Appends a tile to the end of the tile grid. | |
426 * @param {Tile} tile The tile to be added. | |
427 * @param {number} index The location in the tile grid to insert it at. | |
428 * @protected | |
429 */ | |
430 appendTile: function(tile) { | |
431 var index = this.tiles_.length; | |
432 this.addTileAt(tile, index); | |
433 }, | |
434 | |
435 /** | |
436 * Adds the given element to the tile grid. | |
437 * @param {Tile} tile The tile to be added. | |
438 * @param {number} index The location in the tile grid to insert it at. | |
439 * @protected | |
440 */ | |
441 addTileAt: function(tile, index) { | |
442 this.tiles_.splice(index, 0, tile); | |
443 this.fireAddedEvent(tile, index, false); | |
444 this.renderGrid(); | |
445 }, | |
446 | |
447 /** | |
448 * Create a blank tile. | |
449 * @protected | |
450 */ | |
451 createTile_: function() { | |
452 return new this.TileClass(); | |
453 }, | |
454 | |
455 /** | |
456 * Create blank tiles. | |
457 * @param {number} count The desired number of Tiles to be created. If this | |
458 * value the maximum value defined in |config.maxTileCount|, the maximum | |
459 * value will be used instead. | |
460 * @protected | |
461 */ | |
462 createTiles_: function(count) { | |
463 count = Math.min(count, this.config.maxTileCount); | |
464 for (var i = 0; i < count; i++) { | |
465 this.appendTile(this.createTile_()); | |
466 } | |
467 }, | |
468 | |
469 /** | |
470 * This method will create/remove necessary/unnecessary tiles, render the | |
471 * grid when the number of tiles has changed, and finally will call | |
472 * |updateTiles_| which will in turn render the individual tiles. | |
473 * @protected | |
474 */ | |
475 updateGrid: function() { | |
476 var dataListLength = this.dataList_.length; | |
477 var tileCount = this.tileCount; | |
478 // Create or remove tiles if necessary. | |
479 if (tileCount < dataListLength) { | |
480 this.createTiles_(dataListLength - tileCount); | |
481 } else if (tileCount > dataListLength) { | |
482 var tiles = this.tiles_; | |
483 while (tiles.length > dataListLength) { | |
484 var previousLength = tiles.length; | |
485 // It doesn't matter which tiles are being removed here because | |
486 // they're going to be reconstructed below when calling updateTiles_ | |
487 // method, so the first tiles are being removed here. | |
488 this.removeTile(tiles[0]); | |
489 assert(tiles.length < previousLength); | |
490 } | |
491 } | |
492 | |
493 this.updateTiles_(); | |
494 }, | |
495 | |
496 /** | |
497 * Update the tiles after a change to |dataList_|. | |
498 */ | |
499 updateTiles_: function() { | |
500 var maxTileCount = this.config.maxTileCount; | |
501 var dataList = this.dataList_; | |
502 var tiles = this.tiles; | |
503 for (var i = 0; i < maxTileCount; i++) { | |
504 var data = dataList[i]; | |
505 var tile = tiles[i]; | |
506 | |
507 // TODO(pedrosimonetti): What do we do when there's no tile here? | |
508 if (!tile) | |
509 return; | |
510 | |
511 if (i >= dataList.length) | |
512 tile.reset(); | |
513 else | |
514 tile.data = data; | |
515 } | |
516 }, | |
517 | |
518 /** | |
519 * Sets the dataList that will be used to create Tiles. | |
520 * TODO(pedrosimonetti): Use setters and getters instead. | |
521 */ | |
522 setDataList: function(dataList) { | |
523 this.dataList_ = dataList.slice(0, this.config.maxTileCount); | |
524 }, | |
525 | |
526 // internal helpers | |
527 // ------------------------------------------------------------------------- | |
528 | |
529 /** | |
530 * Gets the required width for a Tile. | |
531 * @private | |
532 */ | |
533 getTileRequiredWidth_: function() { | |
534 var config = this.config; | |
535 return config.cellWidth + config.cellMarginStart; | |
536 }, | |
537 | |
538 /** | |
539 * Gets the the maximum number of columns that can fit in a given width. | |
540 * @param {number} width The width in pixels. | |
541 * @private | |
542 */ | |
543 getColCountForWidth_: function(width) { | |
544 var scrollBarIsVisible = this.config.scrollable && | |
545 this.content_.scrollHeight > this.content_.clientHeight; | |
546 var scrollBarWidth = scrollBarIsVisible ? SCROLL_BAR_WIDTH : 0; | |
547 var availableWidth = width + this.config.cellMarginStart - scrollBarWidth; | |
548 | |
549 var requiredWidth = this.getTileRequiredWidth_(); | |
550 var colCount = Math.floor(availableWidth / requiredWidth); | |
551 return colCount; | |
552 }, | |
553 | |
554 /** | |
555 * Gets the width for a given number of columns. | |
556 * @param {number} colCount The number of columns. | |
557 * @private | |
558 */ | |
559 getWidthForColCount_: function(colCount) { | |
560 var requiredWidth = this.getTileRequiredWidth_(); | |
561 var width = colCount * requiredWidth - this.config.cellMarginStart; | |
562 return width; | |
563 }, | |
564 | |
565 /** | |
566 * Returns the position of the tile at |index|. | |
567 * @param {number} index Tile index. | |
568 * @private | |
569 * @return {!{top: number, left: number}} Position. | |
570 */ | |
571 getTilePosition_: function(index) { | |
572 var colCount = this.colCount_; | |
573 var row = Math.floor(index / colCount); | |
574 var col = index % colCount; | |
575 if (isRTL()) | |
576 col = colCount - col - 1; | |
577 var config = this.config; | |
578 var top = ntp.TILE_ROW_HEIGHT * row; | |
579 var left = col * (config.cellWidth + config.cellMarginStart); | |
580 return {top: top, left: left}; | |
581 }, | |
582 | |
583 // rendering | |
584 // ------------------------------------------------------------------------- | |
585 | |
586 /** | |
587 * Renders the tile grid, and the individual tiles. Rendering the grid | |
588 * consists of adding/removing tile rows and tile cells according to the | |
589 * specified size (defined by the number of columns in the grid). While | |
590 * rendering the grid, the tiles are rendered in order in their respective | |
591 * cells and tile fillers are rendered when needed. This method sets the | |
592 * private properties colCount_ and rowCount_. | |
593 * | |
594 * This method should be called every time the contents of the grid changes, | |
595 * that is, when the number, contents or order of the tiles has changed. | |
596 * @param {number=} opt_colCount The number of columns. | |
597 * @param {number=} opt_tileCount Forces a particular number of tiles to | |
598 * be drawn. This is useful for cases like the restoration/insertion | |
599 * of tiles when you need to place a tile in a place of the grid that | |
600 * is not rendered at the moment. | |
601 * @protected | |
602 */ | |
603 renderGrid: function(opt_colCount, opt_tileCount) { | |
604 var colCount = opt_colCount || this.colCount_; | |
605 | |
606 var tileGridContent = this.tileGridContent_; | |
607 var tiles = this.tiles_; | |
608 var tileCount = opt_tileCount || tiles.length; | |
609 | |
610 var rowCount = Math.ceil(tileCount / colCount); | |
611 var tileRows = tileGridContent.getElementsByClassName('tile-row'); | |
612 | |
613 for (var tile = 0, row = 0; row < rowCount; row++) { | |
614 var tileRow = tileRows[row]; | |
615 | |
616 // Create tile row if there's no one yet. | |
617 if (!tileRow) { | |
618 tileRow = cr.doc.createElement('div'); | |
619 tileRow.className = 'tile-row'; | |
620 tileGridContent.appendChild(tileRow); | |
621 } | |
622 | |
623 // The tiles inside the current row. | |
624 var tileRowTiles = tileRow.childNodes; | |
625 | |
626 // Remove excessive columns from a particular tile row. | |
627 var maxColCount = Math.min(colCount, tileCount - tile); | |
628 maxColCount = Math.max(0, maxColCount); | |
629 while (tileRowTiles.length > maxColCount) { | |
630 tileRow.removeChild(tileRow.lastElementChild); | |
631 } | |
632 | |
633 // For each column in the current row. | |
634 for (var col = 0; col < colCount; col++, tile++) { | |
635 var tileCell; | |
636 var tileElement; | |
637 if (tileRowTiles[col]) { | |
638 tileCell = tileRowTiles[col]; | |
639 } else { | |
640 var span = cr.doc.createElement('span'); | |
641 tileCell = new TileCell(span); | |
642 } | |
643 | |
644 // Render Tiles. | |
645 tileElement = tiles[tile]; | |
646 if (tile < tileCount && tileElement) { | |
647 tileCell.classList.remove('filler'); | |
648 if (!tileCell.tile) | |
649 tileCell.appendChild(tileElement); | |
650 else if (tileElement != tileCell.tile) | |
651 tileCell.replaceChild(tileElement, tileCell.tile); | |
652 } else if (!tileCell.classList.contains('filler')) { | |
653 tileCell.classList.add('filler'); | |
654 tileElement = cr.doc.createElement('span'); | |
655 tileElement.className = 'tile'; | |
656 if (tileCell.tile) | |
657 tileCell.replaceChild(tileElement, tileCell.tile); | |
658 else | |
659 tileCell.appendChild(tileElement); | |
660 } | |
661 | |
662 if (!tileRowTiles[col]) | |
663 tileRow.appendChild(tileCell); | |
664 } | |
665 } | |
666 | |
667 // Remove excessive tile rows from the tile grid. | |
668 while (tileRows.length > rowCount) { | |
669 tileGridContent.removeChild(tileGridContent.lastElementChild); | |
670 } | |
671 | |
672 this.colCount_ = colCount; | |
673 this.rowCount_ = rowCount; | |
674 | |
675 // If we are manually changing the tile count (which can happen during | |
676 // the restoration/insertion animation) we should not fire the scroll | |
677 // event once some cells might contain dummy tiles which will cause | |
678 // an error. | |
679 if (!opt_tileCount) | |
680 this.onScroll(); | |
681 }, | |
682 | |
683 // layout | |
684 // ------------------------------------------------------------------------- | |
685 | |
686 /** | |
687 * Calculates the layout of the tile page according to the current Bottom | |
688 * Panel's size. This method will resize the containers of the tile page, | |
689 * and re-render the grid when its dimension changes (number of columns or | |
690 * visible rows changes). This method also sets the private properties | |
691 * |numOfVisibleRows_| and |animatingColCount_|. | |
692 * | |
693 * This method should be called every time the dimension of the grid changes | |
694 * or when you need to reinforce its dimension. | |
695 * @param {boolean=} opt_animate Whether the layout be animated. | |
696 */ | |
697 layout: function(opt_animate) { | |
698 var contentHeight = ntp.getContentHeight(); | |
699 this.content_.style.height = contentHeight + 'px'; | |
700 | |
701 var contentWidth = ntp.getContentWidth(); | |
702 var colCount = this.getColCountForWidth_(contentWidth); | |
703 var lastColCount = this.colCount_; | |
704 var animatingColCount = this.animatingColCount_; | |
705 if (colCount != animatingColCount) { | |
706 if (opt_animate) | |
707 this.tileGrid_.classList.add('animate-grid-width'); | |
708 | |
709 if (colCount > animatingColCount) { | |
710 // If the grid is expanding, it needs to be rendered first so the | |
711 // revealing tiles are visible as soon as the animation starts. | |
712 if (colCount != lastColCount) | |
713 this.renderGrid(colCount); | |
714 | |
715 // Hides affected columns and forces the reflow. | |
716 this.showTileCols_(animatingColCount, false); | |
717 // Trigger reflow, making the tiles completely hidden. | |
718 this.tileGrid_.offsetTop; | |
719 // Fades in the affected columns. | |
720 this.showTileCols_(animatingColCount, true); | |
721 } else { | |
722 // Fades out the affected columns. | |
723 this.showTileCols_(colCount, false); | |
724 } | |
725 | |
726 var newWidth = this.getWidthForColCount_(colCount); | |
727 this.tileGrid_.style.width = newWidth + 'px'; | |
728 | |
729 // TODO(pedrosimonetti): move to handler below. | |
730 var self = this; | |
731 this.onTileGridTransitionEndHandler_ = function() { | |
732 if (colCount < lastColCount) | |
733 self.renderGrid(colCount); | |
734 else | |
735 self.showTileCols_(0, true); | |
736 }; | |
737 } | |
738 | |
739 this.animatingColCount_ = colCount; | |
740 | |
741 this.frame_.style.width = contentWidth + 'px'; | |
742 | |
743 this.onScroll(); | |
744 }, | |
745 | |
746 // tile repositioning animation | |
747 // ------------------------------------------------------------------------- | |
748 | |
749 /** | |
750 * Tile repositioning state. | |
751 * @type {{index: number, isRemoving: number}} | |
752 */ | |
753 tileRepositioningState_: null, | |
754 | |
755 /** | |
756 * Gets the repositioning state. | |
757 * @return {{index: number, isRemoving: number}} The repositioning data. | |
758 */ | |
759 getTileRepositioningState: function() { | |
760 return this.tileRepositioningState_; | |
761 }, | |
762 | |
763 /** | |
764 * Sets the repositioning state that will be used to animate the tiles. | |
765 * @param {number} index The tile's index. | |
766 * @param {boolean} isRemoving Whether the tile is being removed. | |
767 */ | |
768 setTileRepositioningState: function(index, isRemoving) { | |
769 this.tileRepositioningState_ = { | |
770 index: index, | |
771 isRemoving: isRemoving | |
772 }; | |
773 }, | |
774 | |
775 /** | |
776 * Resets the repositioning state. | |
777 */ | |
778 resetTileRepositioningState: function() { | |
779 this.tileRepositioningState_ = null; | |
780 }, | |
781 | |
782 /** | |
783 * Animates a tile removal. | |
784 * @param {number} index The index of the tile to be removed. | |
785 * @param {Object} newDataList The new data list. | |
786 */ | |
787 animateTileRemoval: function(index, newDataList) { | |
788 var tiles = this.tiles_; | |
789 var tileCount = tiles.length; | |
790 assert(tileCount > 0); | |
791 | |
792 var tileCells = this.querySelectorAll('.tile-cell'); | |
793 var extraTileIndex = tileCount - 1; | |
794 var extraCell = tileCells[extraTileIndex]; | |
795 var extraTileData = newDataList[extraTileIndex]; | |
796 | |
797 var repositioningStartIndex = index + 1; | |
798 var repositioningEndIndex = tileCount; | |
799 | |
800 this.initializeRepositioningAnimation_(index, repositioningEndIndex, | |
801 true); | |
802 | |
803 var tileBeingRemoved = tiles[index]; | |
804 tileBeingRemoved.scrollTop; | |
805 | |
806 // The extra tile is the new one that will appear. It can be a normal | |
807 // tile (when there's extra data for it), or a filler tile. | |
808 var extraTile = createTile(this, extraTileData); | |
809 if (!extraTileData) | |
810 extraCell.classList.add('filler'); | |
811 // The extra tile is being assigned in order to put it in the right spot. | |
812 extraCell.assign(extraTile); | |
813 | |
814 this.executeRepositioningAnimation_(tileBeingRemoved, extraTile, | |
815 repositioningStartIndex, repositioningEndIndex, true); | |
816 | |
817 // Cleans up the animation. | |
818 var onPositioningTransitionEnd = function(e) { | |
819 var propertyName = e.propertyName; | |
820 if (!(propertyName == '-webkit-transform' || | |
821 propertyName == 'opacity')) { | |
822 return; | |
823 } | |
824 | |
825 lastAnimatingTile.removeEventListener('webkitTransitionEnd', | |
826 onPositioningTransitionEnd); | |
827 | |
828 this.finalizeRepositioningAnimation_(tileBeingRemoved, | |
829 repositioningStartIndex, repositioningEndIndex, true); | |
830 | |
831 this.removeTile(tileBeingRemoved); | |
832 | |
833 // If the extra tile is a real one (not a filler), then it needs to be | |
834 // added to the tile list. The tile has been placed in the right spot | |
835 // but the tile page still doesn't know about this new tile. | |
836 if (extraTileData) | |
837 this.appendTile(extraTile); | |
838 | |
839 }.bind(this); | |
840 | |
841 // Listens to the animation end. | |
842 var lastAnimatingTile = extraTile; | |
843 lastAnimatingTile.addEventListener('webkitTransitionEnd', | |
844 onPositioningTransitionEnd); | |
845 }, | |
846 | |
847 /** | |
848 * Animates a tile restoration. | |
849 * @param {number} index The index of the tile to be restored. | |
850 * @param {Object} newDataList The new data list. | |
851 */ | |
852 animateTileRestoration: function(index, newDataList) { | |
853 var tiles = this.tiles_; | |
854 var tileCount = tiles.length; | |
855 | |
856 var tileCells = this.getElementsByClassName('tile-cell'); | |
857 | |
858 // If the desired position is outside the grid, then the grid must be | |
859 // expanded so there will be a cell in the desired position. | |
860 if (index >= tileCells.length) | |
861 this.renderGrid(null, index + 1); | |
862 | |
863 var extraTileIndex = Math.min(tileCount, this.config.maxTileCount - 1); | |
864 var extraCell = tileCells[extraTileIndex]; | |
865 var extraTileData = newDataList[extraTileIndex + 1]; | |
866 | |
867 var repositioningStartIndex = index; | |
868 var repositioningEndIndex = tileCount - (extraTileData ? 1 : 0); | |
869 | |
870 this.initializeRepositioningAnimation_(index, repositioningEndIndex); | |
871 | |
872 var restoredData = newDataList[index]; | |
873 var tileBeingRestored = createTile(this, restoredData); | |
874 | |
875 // Temporarily assume the |index| cell so the tile can be animated in | |
876 // the right spot. | |
877 tileCells[index].appendChild(tileBeingRestored); | |
878 | |
879 if (this.config.scrollable) | |
880 this.content_.scrollTop = tileCells[index].offsetTop; | |
881 | |
882 var extraTile; | |
883 if (extraCell) | |
884 extraTile = extraCell.tile; | |
885 | |
886 this.executeRepositioningAnimation_(tileBeingRestored, extraTile, | |
887 repositioningStartIndex, repositioningEndIndex, false); | |
888 | |
889 // Cleans up the animation. | |
890 var onPositioningTransitionEnd = function(e) { | |
891 var propertyName = e.propertyName; | |
892 if (!(propertyName == '-webkit-transform' || | |
893 propertyName == 'opacity')) { | |
894 return; | |
895 } | |
896 | |
897 lastAnimatingTile.removeEventListener('webkitTransitionEnd', | |
898 onPositioningTransitionEnd); | |
899 | |
900 // When there's an extra data, it means the tile is a real one (not a | |
901 // filler), and therefore it needs to be removed from the tile list. | |
902 if (extraTileData) | |
903 this.removeTile(extraTile); | |
904 | |
905 this.finalizeRepositioningAnimation_(tileBeingRestored, | |
906 repositioningStartIndex, repositioningEndIndex, false); | |
907 | |
908 this.addTileAt(tileBeingRestored, index); | |
909 | |
910 }.bind(this); | |
911 | |
912 // Listens to the animation end. | |
913 var lastAnimatingTile = tileBeingRestored; | |
914 lastAnimatingTile.addEventListener('webkitTransitionEnd', | |
915 onPositioningTransitionEnd); | |
916 }, | |
917 | |
918 // animation helpers | |
919 // ------------------------------------------------------------------------- | |
920 | |
921 /** | |
922 * Moves a tile to a new position. | |
923 * @param {Tile} tile A tile. | |
924 * @param {number} left Left coordinate. | |
925 * @param {number} top Top coordinate. | |
926 * @private | |
927 */ | |
928 moveTileTo_: function(tile, left, top) { | |
929 tile.style.left = left + 'px'; | |
930 tile.style.top = top + 'px'; | |
931 }, | |
932 | |
933 /** | |
934 * Resets a tile's position. | |
935 * @param {Tile} tile A tile. | |
936 * @private | |
937 */ | |
938 resetTilePosition_: function(tile) { | |
939 tile.style.left = ''; | |
940 tile.style.top = ''; | |
941 }, | |
942 | |
943 /** | |
944 * Initializes the repositioning animation. | |
945 * @param {number} startIndex Index of the first tile to be repositioned. | |
946 * @param {number} endIndex Index of the last tile to be repositioned. | |
947 * @param {boolean} isRemoving Whether the tile is being removed. | |
948 * @private | |
949 */ | |
950 initializeRepositioningAnimation_: function(startIndex, endIndex, | |
951 isRemoving) { | |
952 // Move tiles from relative to absolute position. | |
953 var tiles = this.tiles_; | |
954 var tileGridContent = this.tileGridContent_; | |
955 for (var i = startIndex; i < endIndex; i++) { | |
956 var tile = tiles[i]; | |
957 var position = this.getTilePosition_(i); | |
958 this.moveTileTo_(tile, position.left, position.top); | |
959 tile.style.zIndex = endIndex - i; | |
960 tileGridContent.appendChild(tile); | |
961 } | |
962 | |
963 tileGridContent.classList.add('animate-tile-repositioning'); | |
964 | |
965 if (!isRemoving) | |
966 tileGridContent.classList.add('undo-removal'); | |
967 }, | |
968 | |
969 /** | |
970 * Executes the repositioning animation. | |
971 * @param {Tile} targetTile The tile that is being removed/restored. | |
972 * @param {Tile} extraTile The extra tile that is going to appear/disappear. | |
973 * @param {number} startIndex Index of the first tile to be repositioned. | |
974 * @param {number} endIndex Index of the last tile to be repositioned. | |
975 * @param {boolean} isRemoving Whether the tile is being removed. | |
976 * @private | |
977 */ | |
978 executeRepositioningAnimation_: function(targetTile, extraTile, startIndex, | |
979 endIndex, isRemoving) { | |
980 targetTile.classList.add('target-tile'); | |
981 | |
982 // Alternate the visualization of the target and extra tiles. | |
983 fadeTile(targetTile, !isRemoving); | |
984 if (extraTile) | |
985 fadeTile(extraTile, isRemoving); | |
986 | |
987 // Move tiles to the new position. | |
988 var tiles = this.tiles_; | |
989 var positionDiff = isRemoving ? -1 : 1; | |
990 for (var i = startIndex; i < endIndex; i++) { | |
991 var position = this.getTilePosition_(i + positionDiff); | |
992 this.moveTileTo_(tiles[i], position.left, position.top); | |
993 } | |
994 }, | |
995 | |
996 /** | |
997 * Finalizes the repositioning animation. | |
998 * @param {Tile} targetTile The tile that is being removed/restored. | |
999 * @param {number} startIndex Index of the first tile to be repositioned. | |
1000 * @param {number} endIndex Index of the last tile to be repositioned. | |
1001 * @param {boolean} isRemoving Whether the tile is being removed. | |
1002 * @private | |
1003 */ | |
1004 finalizeRepositioningAnimation_: function(targetTile, startIndex, endIndex, | |
1005 isRemoving) { | |
1006 // Remove temporary class names. | |
1007 var tileGridContent = this.tileGridContent_; | |
1008 tileGridContent.classList.remove('animate-tile-repositioning'); | |
1009 tileGridContent.classList.remove('undo-removal'); | |
1010 targetTile.classList.remove('target-tile'); | |
1011 | |
1012 // Move tiles back to relative position. | |
1013 var tiles = this.tiles_; | |
1014 var tileCells = this.querySelectorAll('.tile-cell'); | |
1015 var positionDiff = isRemoving ? -1 : 1; | |
1016 for (var i = startIndex; i < endIndex; i++) { | |
1017 var tile = tiles[i]; | |
1018 this.resetTilePosition_(tile); | |
1019 tile.style.zIndex = ''; | |
1020 var tileCell = tileCells[i + positionDiff]; | |
1021 if (tileCell) | |
1022 tileCell.assign(tile); | |
1023 } | |
1024 }, | |
1025 | |
1026 /** | |
1027 * Animates the display of columns. | |
1028 * @param {number} col The column number. | |
1029 * @param {boolean} show Whether or not to show the row. | |
1030 */ | |
1031 showTileCols_: function(col, show) { | |
1032 var prop = show ? 'remove' : 'add'; | |
1033 var max = 10; // TODO(pedrosimonetti): Add const? | |
1034 var tileGridContent = this.tileGridContent_; | |
1035 for (var i = col; i < max; i++) { | |
1036 tileGridContent.classList[prop]('hide-col-' + i); | |
1037 } | |
1038 }, | |
1039 | |
1040 // event handlers | |
1041 // ------------------------------------------------------------------------- | |
1042 | |
1043 /** | |
1044 * Handles the scroll event. | |
1045 * @protected | |
1046 */ | |
1047 onScroll: function() { | |
1048 // If the TilePage is scrollable, then the opacity of shadow top and | |
1049 // bottom must adjusted, indicating when there's an overflow content. | |
1050 if (this.config.scrollable) { | |
1051 var content = this.content_; | |
1052 var topGap = Math.min(MAX_SCROLL_SHADOW_GAP, content.scrollTop); | |
1053 var bottomGap = Math.min(MAX_SCROLL_SHADOW_GAP, content.scrollHeight - | |
1054 content.scrollTop - content.clientHeight); | |
1055 | |
1056 this.shadowTop_.style.opacity = topGap / MAX_SCROLL_SHADOW_GAP; | |
1057 this.shadowBottom_.style.opacity = bottomGap / MAX_SCROLL_SHADOW_GAP; | |
1058 } | |
1059 }, | |
1060 | |
1061 /** | |
1062 * Handles the end of the horizontal tile grid transition. | |
1063 * @param {Event} e The tile grid webkitTransitionEnd event. | |
1064 */ | |
1065 onTileGridTransitionEnd_: function(e) { | |
1066 if (!this.selected) | |
1067 return; | |
1068 | |
1069 // We should remove the classes that control transitions when the | |
1070 // transition ends so when the text is resized (Ctrl + '+'), no other | |
1071 // transition should happen except those defined in the specification. | |
1072 // For example, the tile has a transition for its 'width' property which | |
1073 // is used when the tile is being hidden. But when you resize the text, | |
1074 // and therefore the tile changes its 'width', this change should not be | |
1075 // animated. | |
1076 | |
1077 // When the tile grid width transition ends, we need to remove the class | |
1078 // 'animate-grid-width' which handles the tile grid width transition, and | |
1079 // individual tile transitions. TODO(pedrosimonetti): Investigate if we | |
1080 // can improve the performance here by using a more efficient selector. | |
1081 var tileGrid = this.tileGrid_; | |
1082 if (e.target == tileGrid && | |
1083 tileGrid.classList.contains('animate-grid-width')) { | |
1084 tileGrid.classList.remove('animate-grid-width'); | |
1085 | |
1086 if (this.onTileGridTransitionEndHandler_) | |
1087 this.onTileGridTransitionEndHandler_(); | |
1088 } | |
1089 }, | |
1090 }; | |
1091 | |
1092 /** | |
1093 * Creates a new tile given a particular data. If there's no data, then | |
1094 * a tile filler will be created. | |
1095 * @param {TilePage} tilePage A TilePage. | |
1096 * @param {Object=} opt_data The data that will be used to create the tile. | |
1097 * @return {Tile} The new tile. | |
1098 */ | |
1099 function createTile(tilePage, opt_data) { | |
1100 var tile; | |
1101 if (opt_data) { | |
1102 // If there's data, the new tile will be a real one (not a filler). | |
1103 tile = new tilePage.TileClass(opt_data); | |
1104 } else { | |
1105 // Otherwise, it will be a fake filler tile. | |
1106 tile = cr.doc.createElement('span'); | |
1107 tile.className = 'tile'; | |
1108 } | |
1109 return tile; | |
1110 } | |
1111 | |
1112 /** | |
1113 * Fades a tile. | |
1114 * @param {Tile} tile A Tile. | |
1115 * @param {boolean} isFadeIn Whether to fade-in the tile. If |isFadeIn| is | |
1116 * false, then the tile is going to fade-out. | |
1117 */ | |
1118 function fadeTile(tile, isFadeIn) { | |
1119 var className = 'animate-hide-tile'; | |
1120 tile.classList.add(className); | |
1121 if (isFadeIn) { | |
1122 // Forces a reflow to ensure that the fade-out animation will work. | |
1123 tile.scrollTop; | |
1124 tile.classList.remove(className); | |
1125 } | |
1126 } | |
1127 | |
1128 return { | |
1129 Tile: Tile, | |
1130 TilePage: TilePage, | |
1131 }; | |
1132 }); | |
OLD | NEW |