OLD | NEW |
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 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 | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 cr.define('ntp', function() { | 5 cr.define('ntp', function() { |
6 'use strict'; | 6 'use strict'; |
7 | 7 |
8 // We can't pass the currently dragging tile via dataTransfer because of | 8 //---------------------------------------------------------------------------- |
9 // http://crbug.com/31037 | 9 // Tile |
10 var currentlyDraggingTile = null; | 10 //---------------------------------------------------------------------------- |
11 function getCurrentlyDraggingTile() { | 11 |
12 return currentlyDraggingTile; | 12 /** |
13 } | 13 * A virtual Tile class. Each TilePage subclass should have its own Tile |
14 function setCurrentlyDraggingTile(tile) { | 14 * subclass implemented too (e.g. MostVisitedPage contains MostVisited |
15 currentlyDraggingTile = tile; | 15 * tiles, and MostVisited is a Tile subclass). |
16 if (tile) | 16 * @constructor |
17 ntp.enterRearrangeMode(); | 17 * @param {Object} config TilePage configuration object. |
18 else | 18 */ |
19 ntp.leaveRearrangeMode(); | 19 function Tile(config) { |
| 20 console.error('Tile is a virtual class and is not supposed to be ' + |
| 21 'instantiated'); |
20 } | 22 } |
21 | 23 |
22 /** | 24 /** |
23 * Changes the current dropEffect of a drag. This modifies the native cursor | 25 * Creates a Tile subclass. We need to use this function to create a Tile |
24 * and serves as an indicator of what we should do at the end of the drag as | 26 * subclass because a Tile must also subclass a HTMLElement (which can be |
25 * well as give indication to the user if a drop would succeed if they let go. | 27 * any HTMLElement), so we need to individually add methods and getters here. |
26 * @param {DataTransfer} dataTransfer A dataTransfer object from a drag event. | 28 * @param {Object} Subclass The prototype object of the class we want to be |
27 * @param {string} effect A drop effect to change to (i.e. copy, move, none). | 29 * a Tile subclass. |
| 30 * @param {Object} The extended Subclass object. |
28 */ | 31 */ |
29 function setCurrentDropEffect(dataTransfer, effect) { | 32 Tile.subclass = function(Subclass) { |
30 dataTransfer.dropEffect = effect; | 33 var Base = Tile.prototype; |
31 if (currentlyDraggingTile) | 34 for (var name in Base) { |
32 currentlyDraggingTile.lastDropEffect = dataTransfer.dropEffect; | 35 if (!Subclass.hasOwnProperty(name)) |
| 36 Subclass[name] = Base[name]; |
| 37 } |
| 38 for (var name in TileGetters) { |
| 39 if (!Subclass.hasOwnProperty(name)) |
| 40 Subclass.__defineGetter__(name, TileGetters[name]); |
| 41 } |
| 42 return Subclass; |
| 43 }; |
| 44 |
| 45 Tile.prototype = { |
| 46 /** |
| 47 * Initializes a Tile. |
| 48 * @param {Object} config TilePage configuration object. |
| 49 */ |
| 50 initialize: function(config) { |
| 51 this.className = 'tile'; |
| 52 } |
| 53 }; |
| 54 |
| 55 var TileGetters = { |
| 56 /** |
| 57 * The TileCell associated to this Tile. |
| 58 * @type {TileCell} |
| 59 */ |
| 60 'tileCell': function() { |
| 61 return findAncestorByClass(this, 'tile-cell'); |
| 62 }, |
| 63 |
| 64 /** |
| 65 * The index of the Tile. |
| 66 * @type {number} |
| 67 */ |
| 68 'index': function() { |
| 69 assert(this.tileCell); |
| 70 return this.tileCell.index; |
| 71 } |
| 72 }; |
| 73 |
| 74 //---------------------------------------------------------------------------- |
| 75 // TileCell |
| 76 //---------------------------------------------------------------------------- |
| 77 |
| 78 /** |
| 79 * Creates a new TileCell object. A TileCell represents a cell in the |
| 80 * TilePage's grid. A TilePage uses TileCells to position Tiles in the proper |
| 81 * place and to animate them individually. Each TileCell is associated to |
| 82 * one Tile at a time (or none if it is a filler object), and that association |
| 83 * might change when the grid is resized. When that happens, the grid is |
| 84 * updated and the Tiles are moved to the proper TileCell. We cannot move the |
| 85 * the TileCell itself during the resize because this transition is animated |
| 86 * with CSS and there's no way to stop CSS animations, and we really want to |
| 87 * animate with CSS to take advantage of hardware acceleration. |
| 88 * @constructor |
| 89 * @extends {HTMLDivElement} |
| 90 * @param {HTMLElement} tile Tile element that will be associated to the cell. |
| 91 * @param {Object} config TilePage configuration object. |
| 92 */ |
| 93 function TileCell(tile, config) { |
| 94 var tileCell = cr.doc.createElement('div'); |
| 95 tileCell.__proto__ = TileCell.prototype; |
| 96 tileCell.initialize(tile, config); |
| 97 |
| 98 return tileCell; |
33 } | 99 } |
34 | 100 |
35 /** | 101 TileCell.prototype = { |
36 * Creates a new Tile object. Tiles wrap content on a TilePage, providing | |
37 * some styling and drag functionality. | |
38 * @constructor | |
39 * @extends {HTMLDivElement} | |
40 */ | |
41 function Tile(contents) { | |
42 var tile = cr.doc.createElement('div'); | |
43 tile.__proto__ = Tile.prototype; | |
44 tile.initialize(contents); | |
45 | |
46 return tile; | |
47 } | |
48 | |
49 Tile.prototype = { | |
50 __proto__: HTMLDivElement.prototype, | 102 __proto__: HTMLDivElement.prototype, |
51 | 103 |
52 initialize: function(contents) { | 104 /** |
53 // 'real' as opposed to doppleganger. | 105 * Initializes a TileCell. |
54 this.className = 'tile real'; | 106 * @param {Tile} tile The Tile that will be assigned to this TileCell. |
55 this.appendChild(contents); | 107 * @param {Object} config TilePage configuration object. |
56 contents.tile = this; | 108 */ |
57 | 109 initialize: function(tile, config) { |
58 this.addEventListener('dragstart', this.onDragStart_); | 110 this.className = 'tile-cell'; |
59 this.addEventListener('drag', this.onDragMove_); | 111 this.assign_(tile); |
60 this.addEventListener('dragend', this.onDragEnd_); | |
61 | |
62 this.firstChild.addEventListener( | |
63 'webkitAnimationEnd', this.onContentsAnimationEnd_.bind(this)); | |
64 | |
65 this.eventTracker = new EventTracker(); | |
66 }, | 112 }, |
67 | 113 |
| 114 /** |
| 115 * The index of the TileCell. |
| 116 * @type {number} |
| 117 */ |
68 get index() { | 118 get index() { |
69 return Array.prototype.indexOf.call(this.tilePage.tileElements_, this); | 119 return Array.prototype.indexOf.call(this.tilePage.tiles_, |
| 120 this.firstChild); |
70 }, | 121 }, |
71 | 122 |
| 123 /** |
| 124 * The TilePage associated to this TileCell. |
| 125 * @type {TilePage} |
| 126 */ |
72 get tilePage() { | 127 get tilePage() { |
73 return findAncestorByClass(this, 'tile-page'); | 128 return findAncestorByClass(this, 'tile-page'); |
74 }, | 129 }, |
75 | 130 |
76 /** | 131 /** |
77 * Position the tile at |x, y|, and store this as the grid location, i.e. | 132 * Assigns a Tile to the this TileCell. |
78 * where the tile 'belongs' when it's not being dragged. | 133 * @type {TilePage} |
79 * @param {number} x The x coordinate, in pixels. | |
80 * @param {number} y The y coordinate, in pixels. | |
81 */ | 134 */ |
82 setGridPosition: function(x, y) { | 135 assign_: function(tile) { |
83 this.gridX = x; | 136 if (this.firstChild) |
84 this.gridY = y; | 137 this.replaceChild(tile, this.firstChild); |
85 this.moveTo(x, y); | 138 else |
| 139 this.appendChild(tile); |
86 }, | 140 }, |
87 | 141 |
88 /** | 142 /** |
89 * Position the tile at |x, y|. | |
90 * @param {number} x The x coordinate, in pixels. | |
91 * @param {number} y The y coordinate, in pixels. | |
92 */ | |
93 moveTo: function(x, y) { | |
94 // left overrides right in LTR, and right takes precedence in RTL. | |
95 this.style.left = toCssPx(x); | |
96 this.style.right = toCssPx(x); | |
97 this.style.top = toCssPx(y); | |
98 }, | |
99 | |
100 /** | |
101 * The handler for dragstart events fired on |this|. | |
102 * @param {Event} e The event for the drag. | |
103 * @private | |
104 */ | |
105 onDragStart_: function(e) { | |
106 // The user may start dragging again during a previous drag's finishing | |
107 // animation. | |
108 if (this.classList.contains('dragging')) | |
109 this.finalizeDrag_(); | |
110 | |
111 setCurrentlyDraggingTile(this); | |
112 | |
113 e.dataTransfer.effectAllowed = 'copyMove'; | |
114 this.firstChild.setDragData(e.dataTransfer); | |
115 | |
116 // The drag clone is the node we use as a representation during the drag. | |
117 // It's attached to the top level document element so that it floats above | |
118 // image masks. | |
119 this.dragClone = this.cloneNode(true); | |
120 this.dragClone.style.right = ''; | |
121 this.dragClone.classList.add('drag-representation'); | |
122 $('card-slider-frame').appendChild(this.dragClone); | |
123 this.eventTracker.add(this.dragClone, 'webkitTransitionEnd', | |
124 this.onDragCloneTransitionEnd_.bind(this)); | |
125 | |
126 this.classList.add('dragging'); | |
127 // offsetLeft is mirrored in RTL. Un-mirror it. | |
128 var offsetLeft = isRTL() ? | |
129 this.parentNode.clientWidth - this.offsetLeft : | |
130 this.offsetLeft; | |
131 this.dragOffsetX = e.x - offsetLeft - this.parentNode.offsetLeft; | |
132 this.dragOffsetY = e.y - this.offsetTop - | |
133 // Unlike offsetTop, this value takes scroll position into account. | |
134 this.parentNode.getBoundingClientRect().top; | |
135 | |
136 this.onDragMove_(e); | |
137 }, | |
138 | |
139 /** | |
140 * The handler for drag events fired on |this|. | |
141 * @param {Event} e The event for the drag. | |
142 * @private | |
143 */ | |
144 onDragMove_: function(e) { | |
145 if (e.view != window || (e.x == 0 && e.y == 0)) { | |
146 this.dragClone.hidden = true; | |
147 return; | |
148 } | |
149 | |
150 this.dragClone.hidden = false; | |
151 this.dragClone.style.left = toCssPx(e.x - this.dragOffsetX); | |
152 this.dragClone.style.top = toCssPx(e.y - this.dragOffsetY); | |
153 }, | |
154 | |
155 /** | |
156 * The handler for dragend events fired on |this|. | |
157 * @param {Event} e The event for the drag. | |
158 * @private | |
159 */ | |
160 onDragEnd_: function(e) { | |
161 this.dragClone.hidden = false; | |
162 this.dragClone.classList.add('placing'); | |
163 | |
164 setCurrentlyDraggingTile(null); | |
165 | |
166 // tilePage will be null if we've already been removed. | |
167 var tilePage = this.tilePage; | |
168 if (tilePage) | |
169 tilePage.positionTile_(this.index); | |
170 | |
171 // Take an appropriate action with the drag clone. | |
172 if (this.landedOnTrash) { | |
173 this.dragClone.classList.add('deleting'); | |
174 } else if (tilePage) { | |
175 // TODO(dbeam): Until we fix dropEffect to the correct behavior it will | |
176 // differ on windows - crbug.com/39399. That's why we use the custom | |
177 // this.lastDropEffect instead of e.dataTransfer.dropEffect. | |
178 if (tilePage.selected && this.lastDropEffect != 'copy') { | |
179 // The drag clone can still be hidden from the last drag move event. | |
180 this.dragClone.hidden = false; | |
181 // The tile's contents may have moved following the respositioning; | |
182 // adjust for that. | |
183 var contentDiffX = this.dragClone.firstChild.offsetLeft - | |
184 this.firstChild.offsetLeft; | |
185 var contentDiffY = this.dragClone.firstChild.offsetTop - | |
186 this.firstChild.offsetTop; | |
187 this.dragClone.style.left = | |
188 toCssPx(this.gridX + this.parentNode.offsetLeft - | |
189 contentDiffX); | |
190 this.dragClone.style.top = | |
191 toCssPx(this.gridY + | |
192 this.parentNode.getBoundingClientRect().top - | |
193 contentDiffY); | |
194 } else if (this.dragClone.hidden) { | |
195 this.finalizeDrag_(); | |
196 } else { | |
197 // The CSS3 transitions spec intentionally leaves it up to individual | |
198 // user agents to determine when styles should be applied. On some | |
199 // platforms (at the moment, Windows), when you apply both classes | |
200 // immediately a transition may not occur correctly. That's why we're | |
201 // using a setTimeout here to queue adding the class until the | |
202 // previous class (currently: .placing) sets up a transition. | |
203 // http://dev.w3.org/csswg/css3-transitions/#starting | |
204 window.setTimeout(function() { | |
205 if (this.dragClone) | |
206 this.dragClone.classList.add('dropped-on-other-page'); | |
207 }.bind(this), 0); | |
208 } | |
209 } | |
210 | |
211 delete this.lastDropEffect; | |
212 this.landedOnTrash = false; | |
213 }, | |
214 | |
215 /** | |
216 * Creates a clone of this node offset by the coordinates. Used for the | |
217 * dragging effect where a tile appears to float off one side of the grid | |
218 * and re-appear on the other. | |
219 * @param {number} x x-axis offset, in pixels. | |
220 * @param {number} y y-axis offset, in pixels. | |
221 */ | |
222 showDoppleganger: function(x, y) { | |
223 // We always have to clear the previous doppleganger to make sure we get | |
224 // style updates for the contents of this tile. | |
225 this.clearDoppleganger(); | |
226 | |
227 var clone = this.cloneNode(true); | |
228 clone.classList.remove('real'); | |
229 clone.classList.add('doppleganger'); | |
230 var clonelets = clone.querySelectorAll('.real'); | |
231 for (var i = 0; i < clonelets.length; i++) { | |
232 clonelets[i].classList.remove('real'); | |
233 } | |
234 | |
235 this.appendChild(clone); | |
236 this.doppleganger_ = clone; | |
237 | |
238 if (isRTL()) | |
239 x *= -1; | |
240 | |
241 this.doppleganger_.style.WebkitTransform = 'translate(' + x + 'px, ' + | |
242 y + 'px)'; | |
243 }, | |
244 | |
245 /** | |
246 * Destroys the current doppleganger. | |
247 */ | |
248 clearDoppleganger: function() { | |
249 if (this.doppleganger_) { | |
250 this.removeChild(this.doppleganger_); | |
251 this.doppleganger_ = null; | |
252 } | |
253 }, | |
254 | |
255 /** | |
256 * Returns status of doppleganger. | |
257 * @return {boolean} True if there is a doppleganger showing for |this|. | |
258 */ | |
259 hasDoppleganger: function() { | |
260 return !!this.doppleganger_; | |
261 }, | |
262 | |
263 /** | |
264 * Cleans up after the drag is over. This is either called when the | |
265 * drag representation finishes animating to the final position, or when | |
266 * the next drag starts (if the user starts a 2nd drag very quickly). | |
267 * @private | |
268 */ | |
269 finalizeDrag_: function() { | |
270 assert(this.classList.contains('dragging')); | |
271 | |
272 var clone = this.dragClone; | |
273 this.dragClone = null; | |
274 | |
275 clone.parentNode.removeChild(clone); | |
276 this.eventTracker.remove(clone, 'webkitTransitionEnd'); | |
277 this.classList.remove('dragging'); | |
278 if (this.firstChild.finalizeDrag) | |
279 this.firstChild.finalizeDrag(); | |
280 }, | |
281 | |
282 /** | |
283 * Called when the drag representation node is done migrating to its final | |
284 * resting spot. | |
285 * @param {Event} e The transition end event. | |
286 */ | |
287 onDragCloneTransitionEnd_: function(e) { | |
288 if (this.classList.contains('dragging') && | |
289 (e.propertyName == 'left' || e.propertyName == 'top' || | |
290 e.propertyName == '-webkit-transform')) { | |
291 this.finalizeDrag_(); | |
292 } | |
293 }, | |
294 | |
295 /** | |
296 * Called when an app is removed from Chrome. Animates its disappearance. | 143 * Called when an app is removed from Chrome. Animates its disappearance. |
297 * @param {boolean=} opt_animate Whether the animation should be animated. | 144 * @param {boolean=} opt_animate Whether the animation should be animated. |
298 */ | 145 */ |
299 doRemove: function(opt_animate) { | 146 doRemove: function(opt_animate) { |
300 if (opt_animate) | 147 if (opt_animate) |
301 this.firstChild.classList.add('removing-tile-contents'); | 148 this.firstChild.classList.add('removing-tile-contents'); |
302 else | 149 else |
303 this.tilePage.removeTile(this, false); | 150 this.tilePage.removeTile(this, false); |
304 }, | 151 } |
305 | |
306 /** | |
307 * Callback for the webkitAnimationEnd event on the tile's contents. | |
308 * @param {Event} e The event object. | |
309 */ | |
310 onContentsAnimationEnd_: function(e) { | |
311 if (this.firstChild.classList.contains('new-tile-contents')) | |
312 this.firstChild.classList.remove('new-tile-contents'); | |
313 if (this.firstChild.classList.contains('removing-tile-contents')) | |
314 this.tilePage.removeTile(this, true); | |
315 }, | |
316 }; | 152 }; |
317 | 153 |
318 /** | 154 //---------------------------------------------------------------------------- |
319 * Gives the proportion of the row width that is devoted to a single icon. | 155 // TilePage |
320 * @param {number} rowTileCount The number of tiles in a row. | 156 //---------------------------------------------------------------------------- |
321 * @param {number} tileSpacingFraction The proportion of the tile width which | |
322 * will be used as spacing between tiles. | |
323 * @return {number} The ratio between icon width and row width. | |
324 */ | |
325 function tileWidthFraction(rowTileCount, tileSpacingFraction) { | |
326 return rowTileCount + (rowTileCount - 1) * tileSpacingFraction; | |
327 } | |
328 | |
329 /** | |
330 * Calculates an assortment of tile-related values for a grid with the | |
331 * given dimensions. | |
332 * @param {number} width The pixel width of the grid. | |
333 * @param {number} numRowTiles The number of tiles in a row. | |
334 * @param {number} tileSpacingFraction The proportion of the tile width which | |
335 * will be used as spacing between tiles. | |
336 * @return {Object} A mapping of pixel values. | |
337 */ | |
338 function tileValuesForGrid(width, numRowTiles, tileSpacingFraction) { | |
339 var tileWidth = width / tileWidthFraction(numRowTiles, tileSpacingFraction); | |
340 var offsetX = tileWidth * (1 + tileSpacingFraction); | |
341 var interTileSpacing = offsetX - tileWidth; | |
342 | |
343 return { | |
344 tileWidth: tileWidth, | |
345 offsetX: offsetX, | |
346 interTileSpacing: interTileSpacing, | |
347 }; | |
348 } | |
349 | |
350 // The smallest amount of horizontal blank space to display on the sides when | |
351 // displaying a wide arrangement. There is an additional 26px of margin from | |
352 // the tile page padding. | |
353 var MIN_WIDE_MARGIN = 18; | |
354 | 157 |
355 /** | 158 /** |
356 * Creates a new TilePage object. This object contains tiles and controls | 159 * Creates a new TilePage object. This object contains tiles and controls |
357 * their layout. | 160 * their layout. |
358 * @param {Object} gridValues Pixel values that define the size and layout | |
359 * of the tile grid. | |
360 * @constructor | 161 * @constructor |
361 * @extends {HTMLDivElement} | 162 * @extends {HTMLDivElement} |
362 */ | 163 */ |
363 function TilePage(gridValues) { | 164 function TilePage() { |
364 var el = cr.doc.createElement('div'); | 165 var el = cr.doc.createElement('div'); |
365 el.gridValues_ = gridValues; | |
366 el.__proto__ = TilePage.prototype; | 166 el.__proto__ = TilePage.prototype; |
367 el.initialize(); | 167 el.initialize(); |
368 | 168 |
369 return el; | 169 return el; |
370 } | 170 } |
371 | 171 |
372 /** | |
373 * Takes a collection of grid layout pixel values and updates them with | |
374 * additional tiling values that are calculated from TilePage constants. | |
375 * @param {Object} grid The grid layout pixel values to update. | |
376 */ | |
377 TilePage.initGridValues = function(grid) { | |
378 // The amount of space we need to display a narrow grid (all narrow grids | |
379 // are this size). | |
380 grid.narrowWidth = | |
381 grid.minTileWidth * tileWidthFraction(grid.minColCount, | |
382 grid.tileSpacingFraction); | |
383 // The minimum amount of space we need to display a wide grid. | |
384 grid.minWideWidth = | |
385 grid.minTileWidth * tileWidthFraction(grid.maxColCount, | |
386 grid.tileSpacingFraction); | |
387 // The largest we will ever display a wide grid. | |
388 grid.maxWideWidth = | |
389 grid.maxTileWidth * tileWidthFraction(grid.maxColCount, | |
390 grid.tileSpacingFraction); | |
391 // Tile-related pixel values for the narrow display. | |
392 grid.narrowTileValues = tileValuesForGrid(grid.narrowWidth, | |
393 grid.minColCount, | |
394 grid.tileSpacingFraction); | |
395 // Tile-related pixel values for the minimum narrow display. | |
396 grid.wideTileValues = tileValuesForGrid(grid.minWideWidth, | |
397 grid.maxColCount, | |
398 grid.tileSpacingFraction); | |
399 }; | |
400 | |
401 TilePage.prototype = { | 172 TilePage.prototype = { |
402 __proto__: HTMLDivElement.prototype, | 173 __proto__: HTMLDivElement.prototype, |
403 | 174 |
| 175 // The config object should be defined by each TilePage subclass. |
| 176 config_: { |
| 177 // The width of a cell. |
| 178 cellWidth: 0, |
| 179 // The start margin of a cell (left or right according to text direction). |
| 180 cellMarginStart: 0, |
| 181 // The border panel horizontal margin. |
| 182 bottomPanelHorizontalMargin: 0, |
| 183 // The height of the tile row. |
| 184 rowHeight: 0, |
| 185 // The maximum number of Tiles to be displayed. |
| 186 maxTileCount: 0 |
| 187 }, |
| 188 |
| 189 /** |
| 190 * Initializes a TilePage. |
| 191 */ |
404 initialize: function() { | 192 initialize: function() { |
405 this.className = 'tile-page'; | 193 this.className = 'tile-page'; |
406 | 194 |
407 // Div that acts as a custom scrollbar. The scrollbar has to live | 195 // The content defines the actual space a page has to display tiles. |
408 // outside the content div so it doesn't flicker when scrolling (due to | |
409 // repainting after the scroll, then repainting again when moved in the | |
410 // onScroll handler). |scrollbar_| is only aesthetic, and it only | |
411 // represents the thumb. Actual events are still handled by the invisible | |
412 // native scrollbars. This div gives us more flexibility with the visuals. | |
413 this.scrollbar_ = this.ownerDocument.createElement('div'); | |
414 this.scrollbar_.className = 'tile-page-scrollbar'; | |
415 this.scrollbar_.hidden = true; | |
416 this.appendChild(this.scrollbar_); | |
417 | |
418 // This contains everything but the scrollbar. | |
419 this.content_ = this.ownerDocument.createElement('div'); | 196 this.content_ = this.ownerDocument.createElement('div'); |
420 this.content_.className = 'tile-page-content'; | 197 this.content_.className = 'tile-page-content'; |
421 this.appendChild(this.content_); | 198 this.appendChild(this.content_); |
422 | 199 |
423 // Div that sets the vertical position of the tile grid. | 200 // The div that defines the tile grid viewport. |
424 this.topMargin_ = this.ownerDocument.createElement('div'); | |
425 this.topMargin_.className = 'top-margin'; | |
426 this.content_.appendChild(this.topMargin_); | |
427 | |
428 // Div that holds the tiles. | |
429 this.tileGrid_ = this.ownerDocument.createElement('div'); | 201 this.tileGrid_ = this.ownerDocument.createElement('div'); |
430 this.tileGrid_.className = 'tile-grid'; | 202 this.tileGrid_.className = 'tile-grid'; |
431 this.tileGrid_.style.minWidth = this.gridValues_.narrowWidth + 'px'; | |
432 this.content_.appendChild(this.tileGrid_); | 203 this.content_.appendChild(this.tileGrid_); |
433 | 204 |
434 // Ordered list of our tiles. | 205 // The tile grid contents, which can be scrolled. |
435 this.tileElements_ = this.tileGrid_.getElementsByClassName('tile real'); | 206 this.tileGridContent_ = this.ownerDocument.createElement('div'); |
436 // Ordered list of the elements which want to accept keyboard focus. These | 207 this.tileGridContent_.className = 'tile-grid-content'; |
437 // elements will not be a part of the normal tab order; the tile grid | 208 this.tileGrid_.appendChild(this.tileGridContent_); |
438 // initially gets focused and then these elements can be focused via the | |
439 // arrow keys. | |
440 this.focusableElements_ = | |
441 this.tileGrid_.getElementsByClassName('focusable'); | |
442 | 209 |
443 // These are properties used in updateTopMargin. | 210 // The list of Tile elements which is used to fill the TileGrid cells. |
444 this.animatedTopMarginPx_ = 0; | 211 this.tiles_ = []; |
445 this.topMarginPx_ = 0; | 212 |
| 213 // Event handlers. |
| 214 this.tileGrid_.addEventListener('webkitTransitionEnd', |
| 215 this.onTileGridAnimationEnd_.bind(this)); |
446 | 216 |
447 this.eventTracker = new EventTracker(); | 217 this.eventTracker = new EventTracker(); |
448 this.eventTracker.add(window, 'resize', this.onResize_.bind(this)); | 218 this.eventTracker.add(window, 'resize', this.onResize_.bind(this)); |
449 | 219 this.eventTracker.add(window, 'keyup', this.onKeyUp_.bind(this)); |
450 this.addEventListener('DOMNodeInsertedIntoDocument', | |
451 this.onNodeInsertedIntoDocument_); | |
452 | |
453 this.content_.addEventListener('scroll', this.onScroll_.bind(this)); | |
454 | |
455 this.dragWrapper_ = new cr.ui.DragWrapper(this.tileGrid_, this); | |
456 | 220 |
457 this.addEventListener('cardselected', this.handleCardSelection_); | 221 this.addEventListener('cardselected', this.handleCardSelection_); |
458 this.addEventListener('carddeselected', this.handleCardDeselection_); | 222 this.addEventListener('carddeselected', this.handleCardDeselection_); |
459 this.addEventListener('focus', this.handleFocus_); | |
460 this.addEventListener('keydown', this.handleKeyDown_); | |
461 this.addEventListener('mousedown', this.handleMouseDown_); | |
462 | |
463 this.focusElementIndex_ = -1; | |
464 }, | 223 }, |
465 | 224 |
| 225 /** |
| 226 * The list of Tile elements. |
| 227 * @type {Array<Tile>} |
| 228 */ |
466 get tiles() { | 229 get tiles() { |
467 return this.tileElements_; | 230 return this.tiles_; |
468 }, | 231 }, |
469 | 232 |
| 233 /** |
| 234 * The number of Tiles in this TilePage. |
| 235 * @type {number} |
| 236 */ |
470 get tileCount() { | 237 get tileCount() { |
471 return this.tileElements_.length; | 238 return this.tiles_.length; |
472 }, | 239 }, |
473 | 240 |
| 241 /** |
| 242 * Whether or not this TilePage is selected. |
| 243 * @type {boolean} |
| 244 */ |
474 get selected() { | 245 get selected() { |
475 return Array.prototype.indexOf.call(this.parentNode.children, this) == | 246 return Array.prototype.indexOf.call(this.parentNode.children, this) == |
476 ntp.getCardSlider().currentCard; | 247 ntp.getCardSlider().currentCard; |
477 }, | 248 }, |
478 | 249 |
479 /** | 250 /** |
480 * The size of the margin (unused space) on the sides of the tile grid, in | |
481 * pixels. | |
482 * @type {number} | |
483 */ | |
484 get sideMargin() { | |
485 return this.layoutValues_.leftMargin; | |
486 }, | |
487 | |
488 /** | |
489 * Returns the width of the scrollbar, in pixels, if it is active, or 0 | |
490 * otherwise. | |
491 * @type {number} | |
492 */ | |
493 get scrollbarWidth() { | |
494 return this.scrollbar_.hidden ? 0 : 13; | |
495 }, | |
496 | |
497 /** | |
498 * Returns any extra padding to insert to the bottom of a tile page. By | |
499 * default there is none, but subclasses can override. | |
500 * @type {number} | |
501 */ | |
502 get extraBottomPadding() { | |
503 return 0; | |
504 }, | |
505 | |
506 /** | |
507 * The notification content of this tile (if any, otherwise null). | 251 * The notification content of this tile (if any, otherwise null). |
508 * @type {!HTMLElement} | 252 * @type {!HTMLElement} |
509 */ | 253 */ |
510 get notification() { | 254 get notification() { |
511 return this.topMargin_.nextElementSibling.id == 'notification-container' ? | 255 return this.content_.firstChild.id == 'notification-container' ? |
512 this.topMargin_.nextElementSibling : null; | 256 this.content_.firstChild : null; |
513 }, | 257 }, |
514 /** | 258 /** |
515 * The notification content of this tile (if any, otherwise null). | 259 * The notification content of this tile (if any, otherwise null). |
516 * @type {!HTMLElement} | 260 * @type {!HTMLElement} |
517 */ | 261 */ |
518 set notification(node) { | 262 set notification(node) { |
519 assert(node instanceof HTMLElement, '|node| isn\'t an HTMLElement!'); | 263 assert(node instanceof HTMLElement, '|node| isn\'t an HTMLElement!'); |
520 // NOTE: Implicitly removes from DOM if |node| is inside it. | 264 // NOTE: Implicitly removes from DOM if |node| is inside it. |
521 this.content_.insertBefore(node, this.topMargin_.nextElementSibling); | 265 this.content_.insertBefore(node, this.content_.firstChild); |
522 this.positionNotification_(); | 266 this.positionNotification_(); |
523 }, | 267 }, |
524 | 268 |
525 /** | 269 /** |
526 * Fetches the size, in pixels, of the padding-top of the tile contents. | |
527 * @type {number} | |
528 */ | |
529 get contentPadding() { | |
530 if (typeof this.contentPadding_ == 'undefined') { | |
531 this.contentPadding_ = | |
532 parseInt(getComputedStyle(this.content_).paddingTop, 10); | |
533 } | |
534 return this.contentPadding_; | |
535 }, | |
536 | |
537 /** | |
538 * Removes the tilePage from the DOM and cleans up event handlers. | 270 * Removes the tilePage from the DOM and cleans up event handlers. |
539 */ | 271 */ |
540 remove: function() { | 272 remove: function() { |
541 // This checks arguments.length as most remove functions have a boolean | 273 // This checks arguments.length as most remove functions have a boolean |
542 // |opt_animate| argument, but that's not necesarilly applicable to | 274 // |opt_animate| argument, but that's not necesarilly applicable to |
543 // removing a tilePage. Selecting a different card in an animated way and | 275 // removing a tilePage. Selecting a different card in an animated way and |
544 // deleting the card afterward is probably a better choice. | 276 // deleting the card afterward is probably a better choice. |
545 assert(typeof arguments[0] != 'boolean', | 277 assert(typeof arguments[0] != 'boolean', |
546 'This function takes no |opt_animate| argument.'); | 278 'This function takes no |opt_animate| argument.'); |
547 this.tearDown_(); | 279 this.tearDown_(); |
548 this.parentNode.removeChild(this); | 280 this.parentNode.removeChild(this); |
549 }, | 281 }, |
550 | 282 |
551 /** | 283 /** |
552 * Cleans up resources that are no longer needed after this TilePage | 284 * Cleans up resources that are no longer needed after this TilePage |
553 * instance is removed from the DOM. | 285 * instance is removed from the DOM. |
554 * @private | 286 * @private |
555 */ | 287 */ |
556 tearDown_: function() { | 288 tearDown_: function() { |
557 this.eventTracker.removeAll(); | 289 this.eventTracker.removeAll(); |
558 }, | 290 }, |
559 | 291 |
560 /** | 292 /** |
561 * Appends a tile to the end of the tile grid. | |
562 * @param {HTMLElement} tileElement The contents of the tile. | |
563 * @param {boolean} animate If true, the append will be animated. | |
564 * @protected | |
565 */ | |
566 appendTile: function(tileElement, animate) { | |
567 this.addTileAt(tileElement, this.tileElements_.length, animate); | |
568 }, | |
569 | |
570 /** | |
571 * Adds the given element to the tile grid. | |
572 * @param {Node} tileElement The tile object/node to insert. | |
573 * @param {number} index The location in the tile grid to insert it at. | |
574 * @param {boolean} animate If true, the tile in question will be | |
575 * animated (other tiles, if they must reposition, do not animate). | |
576 * @protected | |
577 */ | |
578 addTileAt: function(tileElement, index, animate) { | |
579 this.classList.remove('animating-tile-page'); | |
580 if (animate) | |
581 tileElement.classList.add('new-tile-contents'); | |
582 | |
583 // Make sure the index is positive and either in the the bounds of | |
584 // this.tileElements_ or at the end (meaning append). | |
585 assert(index >= 0 && index <= this.tileElements_.length); | |
586 | |
587 var wrapperDiv = new Tile(tileElement); | |
588 // If is out of the bounds of the tile element list, .insertBefore() will | |
589 // act just like appendChild(). | |
590 this.tileGrid_.insertBefore(wrapperDiv, this.tileElements_[index]); | |
591 this.calculateLayoutValues_(); | |
592 this.heightChanged_(); | |
593 | |
594 this.repositionTiles_(); | |
595 this.fireAddedEvent(wrapperDiv, index, animate); | |
596 }, | |
597 | |
598 /** | |
599 * Notify interested subscribers that a tile has been removed from this | 293 * Notify interested subscribers that a tile has been removed from this |
600 * page. | 294 * page. TODO(pedrosimonetti): Do we really need to fire this event? |
601 * @param {Tile} tile The newly added tile. | 295 * @param {TileCell} tile The newly added tile. |
602 * @param {number} index The index of the tile that was added. | 296 * @param {number} index The index of the tile that was added. |
603 * @param {boolean} wasAnimated Whether the removal was animated. | 297 * @param {boolean} wasAnimated Whether the removal was animated. |
604 */ | 298 */ |
605 fireAddedEvent: function(tile, index, wasAnimated) { | 299 fireAddedEvent: function(tile, index, wasAnimated) { |
606 var e = document.createEvent('Event'); | 300 var e = document.createEvent('Event'); |
607 e.initEvent('tilePage:tile_added', true, true); | 301 e.initEvent('tilePage:tile_added', true, true); |
608 e.addedIndex = index; | 302 e.addedIndex = index; |
609 e.addedTile = tile; | 303 e.addedTile = tile; |
610 e.wasAnimated = wasAnimated; | 304 e.wasAnimated = wasAnimated; |
611 this.dispatchEvent(e); | 305 this.dispatchEvent(e); |
(...skipping 13 matching lines...) Expand all Loading... |
625 this.calculateLayoutValues_(); | 319 this.calculateLayoutValues_(); |
626 this.cleanupDrag(); | 320 this.cleanupDrag(); |
627 | 321 |
628 if (!opt_dontNotify) | 322 if (!opt_dontNotify) |
629 this.fireRemovedEvent(tile, index, !!opt_animate); | 323 this.fireRemovedEvent(tile, index, !!opt_animate); |
630 }, | 324 }, |
631 | 325 |
632 /** | 326 /** |
633 * Notify interested subscribers that a tile has been removed from this | 327 * Notify interested subscribers that a tile has been removed from this |
634 * page. | 328 * page. |
635 * @param {Tile} tile The tile that was removed. | 329 * @param {TileCell} tile The tile that was removed. |
636 * @param {number} oldIndex Where the tile was positioned before removal. | 330 * @param {number} oldIndex Where the tile was positioned before removal. |
637 * @param {boolean} wasAnimated Whether the removal was animated. | 331 * @param {boolean} wasAnimated Whether the removal was animated. |
638 */ | 332 */ |
639 fireRemovedEvent: function(tile, oldIndex, wasAnimated) { | 333 fireRemovedEvent: function(tile, oldIndex, wasAnimated) { |
640 var e = document.createEvent('Event'); | 334 var e = document.createEvent('Event'); |
641 e.initEvent('tilePage:tile_removed', true, true); | 335 e.initEvent('tilePage:tile_removed', true, true); |
642 e.removedIndex = oldIndex; | 336 e.removedIndex = oldIndex; |
643 e.removedTile = tile; | 337 e.removedTile = tile; |
644 e.wasAnimated = wasAnimated; | 338 e.wasAnimated = wasAnimated; |
645 this.dispatchEvent(e); | 339 this.dispatchEvent(e); |
646 }, | 340 }, |
647 | 341 |
648 /** | 342 /** |
649 * Removes all tiles from the page. | 343 * Removes all tiles from the page. |
650 */ | 344 */ |
651 removeAllTiles: function() { | 345 removeAllTiles: function() { |
| 346 // TODO(pedrosimonetti): Dispatch individual tearDown functions. |
652 this.tileGrid_.innerHTML = ''; | 347 this.tileGrid_.innerHTML = ''; |
653 }, | 348 }, |
654 | 349 |
655 /** | 350 /** |
656 * Called when the page is selected (in the card selector). | 351 * Called when the page is selected (in the card selector). |
657 * @param {Event} e A custom cardselected event. | 352 * @param {Event} e A custom cardselected event. |
658 * @private | 353 * @private |
659 */ | 354 */ |
660 handleCardSelection_: function(e) { | 355 handleCardSelection_: function(e) { |
661 this.tabIndex = 1; | |
662 | |
663 // When we are selected, we re-calculate the layout values. (See comment | 356 // When we are selected, we re-calculate the layout values. (See comment |
664 // in doDrop.) | 357 // in doDrop.) |
665 this.calculateLayoutValues_(); | 358 this.calculateLayoutValues_(); |
666 }, | 359 }, |
667 | 360 |
668 /** | 361 /** |
669 * Called when the page loses selection (in the card selector). | 362 * Called when the page loses selection (in the card selector). |
670 * @param {Event} e A custom carddeselected event. | 363 * @param {Event} e A custom carddeselected event. |
671 * @private | 364 * @private |
672 */ | 365 */ |
673 handleCardDeselection_: function(e) { | 366 handleCardDeselection_: function(e) { |
674 this.tabIndex = -1; | |
675 if (this.currentFocusElement_) | |
676 this.currentFocusElement_.tabIndex = -1; | |
677 }, | 367 }, |
678 | 368 |
679 /** | 369 /** |
680 * When we get focus, pass it on to the focus element. | |
681 * @param {Event} e The focus event. | |
682 * @private | |
683 */ | |
684 handleFocus_: function(e) { | |
685 if (this.focusableElements_.length == 0) | |
686 return; | |
687 | |
688 this.updateFocusElement_(); | |
689 }, | |
690 | |
691 /** | |
692 * Since we are doing custom focus handling, we have to manually | |
693 * set focusability on click (as well as keyboard nav above). | |
694 * @param {Event} e The focus event. | |
695 * @private | |
696 */ | |
697 handleMouseDown_: function(e) { | |
698 var focusable = findAncestorByClass(e.target, 'focusable'); | |
699 if (focusable) { | |
700 this.focusElementIndex_ = | |
701 Array.prototype.indexOf.call(this.focusableElements_, | |
702 focusable); | |
703 this.updateFocusElement_(); | |
704 } else { | |
705 // This prevents the tile page from getting focus when the user clicks | |
706 // inside the grid but outside of any tile. | |
707 e.preventDefault(); | |
708 } | |
709 }, | |
710 | |
711 /** | |
712 * Handle arrow key focus nav. | |
713 * @param {Event} e The focus event. | |
714 * @private | |
715 */ | |
716 handleKeyDown_: function(e) { | |
717 // We only handle up, down, left, right without control keys. | |
718 if (e.metaKey || e.shiftKey || e.altKey || e.ctrlKey) | |
719 return; | |
720 | |
721 // Wrap the given index to |this.focusableElements_|. | |
722 var wrap = function(idx) { | |
723 return (idx + this.focusableElements_.length) % | |
724 this.focusableElements_.length; | |
725 }.bind(this); | |
726 | |
727 switch (e.keyIdentifier) { | |
728 case 'Right': | |
729 case 'Left': | |
730 var direction = e.keyIdentifier == 'Right' ? 1 : -1; | |
731 this.focusElementIndex_ = wrap(this.focusElementIndex_ + direction); | |
732 break; | |
733 case 'Up': | |
734 case 'Down': | |
735 // Look through all focusable elements. Find the first one that is | |
736 // in the same column. | |
737 var direction = e.keyIdentifier == 'Up' ? -1 : 1; | |
738 var currentIndex = | |
739 Array.prototype.indexOf.call(this.focusableElements_, | |
740 this.currentFocusElement_); | |
741 var newFocusIdx = wrap(currentIndex + direction); | |
742 var tile = this.currentFocusElement_.parentNode; | |
743 for (;; newFocusIdx = wrap(newFocusIdx + direction)) { | |
744 var newTile = this.focusableElements_[newFocusIdx].parentNode; | |
745 var rowTiles = this.layoutValues_.numRowTiles; | |
746 if ((newTile.index - tile.index) % rowTiles == 0) | |
747 break; | |
748 } | |
749 | |
750 this.focusElementIndex_ = newFocusIdx; | |
751 break; | |
752 | |
753 default: | |
754 return; | |
755 } | |
756 | |
757 this.updateFocusElement_(); | |
758 | |
759 e.preventDefault(); | |
760 e.stopPropagation(); | |
761 }, | |
762 | |
763 /** | |
764 * Focuses the element for |this.focusElementIndex_|. Makes the current | |
765 * focus element, if any, no longer eligible for focus. | |
766 * @private | |
767 */ | |
768 updateFocusElement_: function() { | |
769 this.focusElementIndex_ = Math.min(this.focusableElements_.length - 1, | |
770 this.focusElementIndex_); | |
771 this.focusElementIndex_ = Math.max(0, this.focusElementIndex_); | |
772 | |
773 var newFocusElement = this.focusableElements_[this.focusElementIndex_]; | |
774 var lastFocusElement = this.currentFocusElement_; | |
775 if (lastFocusElement && lastFocusElement != newFocusElement) | |
776 lastFocusElement.tabIndex = -1; | |
777 | |
778 newFocusElement.tabIndex = 1; | |
779 newFocusElement.focus(); | |
780 this.tabIndex = -1; | |
781 }, | |
782 | |
783 /** | |
784 * The current focus element is that element which is eligible for focus. | |
785 * @type {HTMLElement} The node. | |
786 * @private | |
787 */ | |
788 get currentFocusElement_() { | |
789 return this.querySelector('.focusable[tabindex="1"]'); | |
790 }, | |
791 | |
792 /** | |
793 * Makes some calculations for tile layout. These change depending on | 370 * Makes some calculations for tile layout. These change depending on |
794 * height, width, and the number of tiles. | 371 * height, width, and the number of tiles. |
795 * TODO(estade): optimize calls to this function. Do nothing if the page is | 372 * TODO(estade): optimize calls to this function. Do nothing if the page is |
796 * hidden, but call before being shown. | 373 * hidden, but call before being shown. |
797 * @private | 374 * @private |
798 */ | 375 */ |
799 calculateLayoutValues_: function() { | 376 calculateLayoutValues_: function() { |
800 var grid = this.gridValues_; | 377 this.layout_(); |
801 var availableSpace = this.tileGrid_.clientWidth - 2 * MIN_WIDE_MARGIN; | |
802 var wide = availableSpace >= grid.minWideWidth; | |
803 var numRowTiles = wide ? grid.maxColCount : grid.minColCount; | |
804 | 378 |
805 var effectiveGridWidth = wide ? | 379 // TODO(pedrosimonetti): When do we really need to send this message? |
806 Math.min(Math.max(availableSpace, grid.minWideWidth), | |
807 grid.maxWideWidth) : | |
808 grid.narrowWidth; | |
809 var realTileValues = tileValuesForGrid(effectiveGridWidth, numRowTiles, | |
810 grid.tileSpacingFraction); | |
811 | |
812 // leftMargin centers the grid within the avaiable space. | |
813 var minMargin = wide ? MIN_WIDE_MARGIN : 0; | |
814 var leftMargin = | |
815 Math.max(minMargin, | |
816 (this.tileGrid_.clientWidth - effectiveGridWidth) / 2); | |
817 | |
818 var rowHeight = this.heightForWidth(realTileValues.tileWidth) + | |
819 realTileValues.interTileSpacing; | |
820 | |
821 this.layoutValues_ = { | |
822 colWidth: realTileValues.offsetX, | |
823 gridWidth: effectiveGridWidth, | |
824 leftMargin: leftMargin, | |
825 numRowTiles: numRowTiles, | |
826 rowHeight: rowHeight, | |
827 tileWidth: realTileValues.tileWidth, | |
828 wide: wide, | |
829 }; | |
830 | |
831 // We need to update the top margin as well. | |
832 this.updateTopMargin_(); | |
833 | |
834 this.firePageLayoutEvent_(); | 380 this.firePageLayoutEvent_(); |
835 }, | 381 }, |
836 | 382 |
837 /** | 383 /** |
838 * Dispatches the custom pagelayout event. | 384 * Dispatches the custom pagelayout event. |
839 * @private | 385 * @private |
840 */ | 386 */ |
841 firePageLayoutEvent_: function() { | 387 firePageLayoutEvent_: function() { |
842 cr.dispatchSimpleEvent(this, 'pagelayout', true, true); | 388 cr.dispatchSimpleEvent(this, 'pagelayout', true, true); |
843 }, | 389 }, |
844 | 390 |
845 /** | 391 /** |
846 * @return {number} The amount of margin that should be animated (in pixels) | |
847 * for the current grid layout. | |
848 */ | |
849 getAnimatedLeftMargin_: function() { | |
850 if (this.layoutValues_.wide) | |
851 return 0; | |
852 | |
853 var grid = this.gridValues_; | |
854 return (grid.minWideWidth - MIN_WIDE_MARGIN - grid.narrowWidth) / 2; | |
855 }, | |
856 | |
857 /** | |
858 * Calculates the x/y coordinates for an element and moves it there. | |
859 * @param {number} index The index of the element to be positioned. | |
860 * @param {number} indexOffset If provided, this is added to |index| when | |
861 * positioning the tile. The effect is that the tile will be positioned | |
862 * in a non-default location. | |
863 * @private | |
864 */ | |
865 positionTile_: function(index, indexOffset) { | |
866 var grid = this.gridValues_; | |
867 var layout = this.layoutValues_; | |
868 | |
869 indexOffset = typeof indexOffset != 'undefined' ? indexOffset : 0; | |
870 // Add the offset _after_ the modulus division. We might want to show the | |
871 // tile off the side of the grid. | |
872 var col = index % layout.numRowTiles + indexOffset; | |
873 var row = Math.floor(index / layout.numRowTiles); | |
874 // Calculate the final on-screen position for the tile. | |
875 var realX = col * layout.colWidth + layout.leftMargin; | |
876 var realY = row * layout.rowHeight; | |
877 | |
878 // Calculate the portion of the tile's position that should be animated. | |
879 var animatedTileValues = layout.wide ? | |
880 grid.wideTileValues : grid.narrowTileValues; | |
881 // Animate the difference between three-wide and six-wide. | |
882 var animatedLeftMargin = this.getAnimatedLeftMargin_(); | |
883 var animatedX = col * animatedTileValues.offsetX + animatedLeftMargin; | |
884 var animatedY = row * (this.heightForWidth(animatedTileValues.tileWidth) + | |
885 animatedTileValues.interTileSpacing); | |
886 | |
887 var tile = this.tileElements_[index]; | |
888 tile.setGridPosition(animatedX, animatedY); | |
889 tile.firstChild.setBounds(layout.tileWidth, | |
890 realX - animatedX, | |
891 realY - animatedY); | |
892 | |
893 // This code calculates whether the tile needs to show a clone of itself | |
894 // wrapped around the other side of the tile grid. | |
895 var offTheRight = col == layout.numRowTiles || | |
896 (col == layout.numRowTiles - 1 && tile.hasDoppleganger()); | |
897 var offTheLeft = col == -1 || (col == 0 && tile.hasDoppleganger()); | |
898 if (this.isCurrentDragTarget && (offTheRight || offTheLeft)) { | |
899 var sign = offTheRight ? 1 : -1; | |
900 tile.showDoppleganger(-layout.numRowTiles * layout.colWidth * sign, | |
901 layout.rowHeight * sign); | |
902 } else { | |
903 tile.clearDoppleganger(); | |
904 } | |
905 | |
906 if (index == this.tileElements_.length - 1) { | |
907 this.tileGrid_.style.height = (realY + layout.rowHeight) + 'px'; | |
908 this.queueUpdateScrollbars_(); | |
909 } | |
910 }, | |
911 | |
912 /** | |
913 * Gets the index of the tile that should occupy coordinate (x, y). Note | |
914 * that this function doesn't care where the tiles actually are, and will | |
915 * return an index even for the space between two tiles. This function is | |
916 * effectively the inverse of |positionTile_|. | |
917 * @param {number} x The x coordinate, in pixels, relative to the left of | |
918 * |this|. | |
919 * @param {number} y The y coordinate, in pixels, relative to the top of | |
920 * |this|. | |
921 * @private | |
922 */ | |
923 getWouldBeIndexForPoint_: function(x, y) { | |
924 var grid = this.gridValues_; | |
925 var layout = this.layoutValues_; | |
926 | |
927 var gridClientRect = this.tileGrid_.getBoundingClientRect(); | |
928 var col = Math.floor((x - gridClientRect.left - layout.leftMargin) / | |
929 layout.colWidth); | |
930 if (col < 0 || col >= layout.numRowTiles) | |
931 return -1; | |
932 | |
933 if (isRTL()) | |
934 col = layout.numRowTiles - 1 - col; | |
935 | |
936 var row = Math.floor((y - gridClientRect.top) / layout.rowHeight); | |
937 return row * layout.numRowTiles + col; | |
938 }, | |
939 | |
940 /** | |
941 * Window resize event handler. Window resizes may trigger re-layouts. | |
942 * @param {Object} e The resize event. | |
943 */ | |
944 onResize_: function(e) { | |
945 if (this.lastWidth_ == this.clientWidth && | |
946 this.lastHeight_ == this.clientHeight) { | |
947 return; | |
948 } | |
949 | |
950 this.calculateLayoutValues_(); | |
951 | |
952 this.lastWidth_ = this.clientWidth; | |
953 this.lastHeight_ = this.clientHeight; | |
954 this.classList.add('animating-tile-page'); | |
955 this.heightChanged_(); | |
956 | |
957 this.positionNotification_(); | |
958 this.repositionTiles_(); | |
959 }, | |
960 | |
961 /** | |
962 * The tile grid has an image mask which fades at the edges. We only show | |
963 * the mask when there is an active drag; it obscures doppleganger tiles | |
964 * as they enter or exit the grid. | |
965 * @private | |
966 */ | |
967 updateMask_: function() { | |
968 if (!this.isCurrentDragTarget) { | |
969 this.tileGrid_.style.WebkitMaskBoxImage = ''; | |
970 return; | |
971 } | |
972 | |
973 var leftMargin = this.layoutValues_.leftMargin; | |
974 // The fade distance is the space between tiles. | |
975 var fadeDistance = (this.gridValues_.tileSpacingFraction * | |
976 this.layoutValues_.tileWidth); | |
977 fadeDistance = Math.min(leftMargin, fadeDistance); | |
978 // On Skia we don't use any fade because it works very poorly. See | |
979 // http://crbug.com/99373 | |
980 if (!cr.isMac) | |
981 fadeDistance = 1; | |
982 var gradient = | |
983 '-webkit-linear-gradient(left,' + | |
984 'transparent, ' + | |
985 'transparent ' + (leftMargin - fadeDistance) + 'px, ' + | |
986 'black ' + leftMargin + 'px, ' + | |
987 'black ' + (this.tileGrid_.clientWidth - leftMargin) + 'px, ' + | |
988 'transparent ' + (this.tileGrid_.clientWidth - leftMargin + | |
989 fadeDistance) + 'px, ' + | |
990 'transparent)'; | |
991 this.tileGrid_.style.WebkitMaskBoxImage = gradient; | |
992 }, | |
993 | |
994 updateTopMargin_: function() { | |
995 var layout = this.layoutValues_; | |
996 | |
997 // The top margin is set so that the vertical midpoint of the grid will | |
998 // be 1/3 down the page. | |
999 var numTiles = this.tileCount + | |
1000 (this.isCurrentDragTarget && !this.withinPageDrag_ ? 1 : 0); | |
1001 var numRows = Math.max(1, Math.ceil(numTiles / layout.numRowTiles)); | |
1002 var usedHeight = layout.rowHeight * numRows; | |
1003 var newMargin = document.documentElement.clientHeight / 3 - | |
1004 usedHeight / 3 - this.contentPadding; | |
1005 // The 'height' style attribute of topMargin is non-zero to work around | |
1006 // webkit's collapsing margin behavior, so we have to factor that into | |
1007 // our calculations here. | |
1008 newMargin = Math.max(newMargin, 0) - this.topMargin_.offsetHeight; | |
1009 | |
1010 // |newMargin| is the final margin we actually want to show. However, | |
1011 // part of that should be animated and part should not (for the same | |
1012 // reason as with leftMargin). The approach is to consider differences | |
1013 // when the layout changes from wide to narrow or vice versa as | |
1014 // 'animatable'. These differences accumulate in animatedTopMarginPx_, | |
1015 // while topMarginPx_ caches the real (total) margin. Either of these | |
1016 // calculations may come out to be negative, so we use margins as the | |
1017 // css property. | |
1018 | |
1019 if (typeof this.topMarginIsForWide_ == 'undefined') | |
1020 this.topMarginIsForWide_ = layout.wide; | |
1021 if (this.topMarginIsForWide_ != layout.wide) { | |
1022 this.animatedTopMarginPx_ += newMargin - this.topMarginPx_; | |
1023 this.topMargin_.style.marginBottom = toCssPx(this.animatedTopMarginPx_); | |
1024 } | |
1025 | |
1026 this.topMarginIsForWide_ = layout.wide; | |
1027 this.topMarginPx_ = newMargin; | |
1028 this.topMargin_.style.marginTop = | |
1029 toCssPx(this.topMarginPx_ - this.animatedTopMarginPx_); | |
1030 }, | |
1031 | |
1032 /** | |
1033 * Position the notification if there's one showing. | 392 * Position the notification if there's one showing. |
| 393 * TODO(pedrosimonetti): Fix the position of the notification. |
1034 */ | 394 */ |
1035 positionNotification_: function() { | 395 positionNotification_: function() { |
1036 var notification = this.notification; | |
1037 if (!notification || notification.hidden) | |
1038 return; | |
1039 | |
1040 // Update the horizontal position. | |
1041 var animatedLeftMargin = this.getAnimatedLeftMargin_(); | |
1042 notification.style.WebkitMarginStart = animatedLeftMargin + 'px'; | |
1043 var leftOffset = (this.layoutValues_.leftMargin - animatedLeftMargin) * | |
1044 (isRTL() ? -1 : 1); | |
1045 notification.style.WebkitTransform = 'translateX(' + leftOffset + 'px)'; | |
1046 | |
1047 // Update the allowable widths of the text. | |
1048 var buttonWidth = notification.querySelector('button').offsetWidth + 8; | |
1049 notification.querySelector('span').style.maxWidth = | |
1050 this.layoutValues_.gridWidth - buttonWidth + 'px'; | |
1051 | |
1052 // This makes sure the text doesn't condense smaller than the narrow size | |
1053 // of the grid (e.g. when a user makes the window really small). | |
1054 notification.style.minWidth = | |
1055 this.gridValues_.narrowWidth - buttonWidth + 'px'; | |
1056 | |
1057 // Update the top position. | |
1058 notification.style.marginTop = -notification.offsetHeight + 'px'; | |
1059 }, | |
1060 | |
1061 /** | |
1062 * Handles final setup that can only happen after |this| is inserted into | |
1063 * the page. | |
1064 * @private | |
1065 */ | |
1066 onNodeInsertedIntoDocument_: function(e) { | |
1067 this.calculateLayoutValues_(); | |
1068 this.heightChanged_(); | |
1069 }, | |
1070 | |
1071 /** | |
1072 * Called when the height of |this| has changed: update the size of | |
1073 * tileGrid. | |
1074 * @private | |
1075 */ | |
1076 heightChanged_: function() { | |
1077 // The tile grid will expand to the bottom footer, or enough to hold all | |
1078 // the tiles, whichever is greater. It would be nicer if tilePage were | |
1079 // a flex box, and the tile grid could be box-flex: 1, but this exposes a | |
1080 // bug where repositioning tiles will cause the scroll position to reset. | |
1081 this.tileGrid_.style.minHeight = (this.clientHeight - | |
1082 this.tileGrid_.offsetTop - this.content_.offsetTop - | |
1083 this.extraBottomPadding - | |
1084 (this.footerNode_ ? this.footerNode_.clientHeight : 0)) + 'px'; | |
1085 }, | 396 }, |
1086 | 397 |
1087 /** | 398 /** |
1088 * Places an element at the bottom of the content div. Used in bare-minimum | 399 * Places an element at the bottom of the content div. Used in bare-minimum |
1089 * mode to hold #footer. | 400 * mode to hold #footer. |
| 401 * TODO(pedrosimonetti): Delete & make Footer non-required(shared/js/util). |
1090 * @param {HTMLElement} footerNode The node to append to content. | 402 * @param {HTMLElement} footerNode The node to append to content. |
1091 */ | 403 */ |
1092 appendFooter: function(footerNode) { | 404 appendFooter: function(footerNode) { |
1093 this.footerNode_ = footerNode; | 405 this.footerNode_ = footerNode; |
1094 this.content_.appendChild(footerNode); | 406 this.content_.appendChild(footerNode); |
1095 }, | 407 }, |
1096 | 408 |
1097 /** | 409 /** |
1098 * Scrolls the page in response to an mousewheel event, although the event | 410 * Scrolls the page in response to an mousewheel event, although the event |
1099 * may have been triggered on a different element. Return true if the | 411 * may have been triggered on a different element. Return true if the |
1100 * event triggered scrolling, and false otherwise. | 412 * event triggered scrolling, and false otherwise. |
1101 * This is called explicitly, which allows a consistent experience whether | 413 * This is called explicitly, which allows a consistent experience whether |
1102 * the user scrolls on the page or on the page switcher, because this | 414 * the user scrolls on the page or on the page switcher, because this |
1103 * function provides a common conversion factor between wheel delta and | 415 * function provides a common conversion factor between wheel delta and |
1104 * scroll delta. | 416 * scroll delta. |
1105 * @param {Event} e The mousewheel event. | 417 * @param {Event} e The mousewheel event. |
1106 */ | 418 */ |
1107 handleMouseWheel: function(e) { | 419 handleMouseWheel: function(e) { |
1108 if (e.wheelDeltaY == 0) | 420 if (e.wheelDeltaY == 0) |
1109 return false; | 421 return false; |
1110 | 422 |
1111 this.content_.scrollTop -= e.wheelDeltaY / 3; | 423 this.content_.scrollTop -= e.wheelDeltaY / 3; |
1112 return true; | 424 return true; |
1113 }, | 425 }, |
1114 | 426 |
1115 /** | 427 // ######################################################################### |
1116 * Handler for the 'scroll' event on |content_|. | 428 // Extended Chrome Instant |
1117 * @param {Event} e The scroll event. | 429 // ######################################################################### |
1118 * @private | 430 |
1119 */ | 431 |
1120 onScroll_: function(e) { | 432 // properties |
1121 this.queueUpdateScrollbars_(); | 433 // ------------------------------------------------------------------------- |
1122 }, | 434 |
1123 | 435 // The number of columns. |
1124 /** | 436 colCount_: 5, |
1125 * ID of scrollbar update timer. If 0, there's no scrollbar re-calc queued. | 437 // The number of rows. |
1126 * @private | 438 rowCount_: 2, |
1127 */ | 439 // The number of visible rows. |
1128 scrollbarUpdate_: 0, | 440 numOfVisibleRows_: 0, |
1129 | 441 // The number of the last column being animated. |
1130 /** | 442 // TODO(pedrosimonetti): How to handle initialization of this value? |
1131 * Queues an update on the custom scrollbar. Used for two reasons: first, | 443 animatingColCount_: 5, |
1132 * coalescing of multiple updates, and second, because action like | 444 // The index of the topmost row visible. |
1133 * repositioning a tile can require a delay before they affect values | 445 // TODO(pedrosimonetti): Move to config_? |
1134 * like clientHeight. | 446 pageOffset_: 0, |
1135 * @private | 447 |
1136 */ | 448 /** |
1137 queueUpdateScrollbars_: function() { | 449 * Appends a tile to the end of the tile grid. |
1138 if (this.scrollbarUpdate_) | 450 * @param {Tile} tile The tile to be added. |
1139 return; | 451 * @param {number} index The location in the tile grid to insert it at. |
1140 | 452 * @protected |
1141 this.scrollbarUpdate_ = window.setTimeout( | 453 */ |
1142 this.doUpdateScrollbars_.bind(this), 0); | 454 appendTile: function(tile) { |
1143 }, | 455 var index = this.tiles_.length; |
1144 | 456 this.tiles_.push(tile); |
1145 /** | 457 this.renderGrid_(); |
1146 * Does the work of calculating the visibility, height and position of the | 458 this.fireAddedEvent(tile, index); |
1147 * scrollbar thumb (there is no track or buttons). | 459 }, |
1148 * @private | 460 |
1149 */ | 461 /** |
1150 doUpdateScrollbars_: function() { | 462 * Adds the given element to the tile grid. |
1151 this.scrollbarUpdate_ = 0; | 463 * TODO(pedrosimonetti): If this is not being used, delete. |
1152 | 464 * @param {Tile} tile The tile to be added. |
1153 var content = this.content_; | 465 * @param {number} index The location in the tile grid to insert it at. |
1154 | 466 * @protected |
1155 // Adjust scroll-height to account for possible header-bar. | 467 */ |
1156 var adjustedScrollHeight = content.scrollHeight - content.offsetTop; | 468 addTileAt: function(tile, index) { |
1157 | 469 this.tiles_.splice(index, 0, tile); |
1158 if (adjustedScrollHeight <= content.clientHeight) { | 470 this.renderGrid_(); |
1159 this.scrollbar_.hidden = true; | 471 this.fireAddedEvent(tile, index); |
1160 return; | 472 }, |
1161 } else { | 473 |
1162 this.scrollbar_.hidden = false; | 474 // internal helpers |
1163 } | 475 // ------------------------------------------------------------------------- |
1164 | 476 |
1165 var thumbTop = content.offsetTop + | 477 /** |
1166 content.scrollTop / adjustedScrollHeight * content.clientHeight; | 478 * Gets the required width for a Tile. |
1167 var thumbHeight = content.clientHeight / adjustedScrollHeight * | 479 * @private |
1168 this.clientHeight; | 480 */ |
1169 | 481 getTileRequiredWidth_: function() { |
1170 this.scrollbar_.style.top = thumbTop + 'px'; | 482 var conf = this.config_; |
1171 this.scrollbar_.style.height = thumbHeight + 'px'; | 483 return conf.cellWidth + conf.cellMarginStart; |
1172 this.firePageLayoutEvent_(); | 484 }, |
1173 }, | 485 |
1174 | 486 /** |
1175 /** | 487 * Gets the the maximum number of columns that can fit in a given width. |
1176 * Get the height for a tile of a certain width. Override this function to | 488 * @param {number} width The width in pixels. |
1177 * get non-square tiles. | 489 * @private |
1178 * @param {number} width The pixel width of a tile. | 490 */ |
1179 * @return {number} The height for |width|. | 491 getColCountForWidth_: function(width) { |
1180 */ | 492 var availableWidth = width + this.config_.cellMarginStart; |
1181 heightForWidth: function(width) { | 493 var requiredWidth = this.getTileRequiredWidth_(); |
| 494 var colCount = Math.floor(availableWidth / requiredWidth); |
| 495 return colCount; |
| 496 }, |
| 497 |
| 498 /** |
| 499 * Gets the width for a given number of columns. |
| 500 * @param {number} colCount The number of columns. |
| 501 * @private |
| 502 */ |
| 503 getWidthForColCount_: function(colCount) { |
| 504 var requiredWidth = this.getTileRequiredWidth_(); |
| 505 var width = colCount * requiredWidth - this.config_.cellMarginStart; |
1182 return width; | 506 return width; |
1183 }, | 507 }, |
1184 | 508 |
1185 /** Dragging **/ | 509 /** |
1186 | 510 * Gets the bottom panel width. |
1187 get isCurrentDragTarget() { | 511 * @private |
1188 return this.dragWrapper_.isCurrentDragTarget; | 512 */ |
1189 }, | 513 getBottomPanelWidth_: function() { |
1190 | 514 var windowWidth = cr.doc.documentElement.clientWidth; |
1191 /** | 515 var width; |
1192 * Thunk for dragleave events fired on |tileGrid_|. | 516 // TODO(pedrosimonetti): Add constants? |
1193 * @param {Event} e A MouseEvent for the drag. | 517 if (windowWidth >= 948) |
1194 */ | 518 width = 748; |
1195 doDragLeave: function(e) { | 519 else if (windowWidth >= 500) |
1196 this.cleanupDrag(); | 520 width = windowWidth - 2 * this.config_.bottomPanelHorizontalMargin; |
1197 }, | 521 else if (windowWidth >= 300) |
1198 | 522 // TODO(pedrosimonetti): Check specification. |
1199 /** | 523 width = Math.floor(((windowWidth - 300) / 200) * 100 + 200); |
1200 * Performs all actions necessary when a drag enters the tile page. | 524 else |
1201 * @param {Event} e A mouseover event for the drag enter. | 525 width = 200; |
1202 */ | 526 |
1203 doDragEnter: function(e) { | 527 return width; |
1204 // Applies the mask so doppleganger tiles disappear into the fog. | 528 }, |
1205 this.updateMask_(); | 529 |
1206 | 530 /** |
1207 this.classList.add('animating-tile-page'); | 531 * Gets the number of available columns. |
1208 this.withinPageDrag_ = this.contains(currentlyDraggingTile); | 532 * @private |
1209 this.dragItemIndex_ = this.withinPageDrag_ ? | 533 */ |
1210 currentlyDraggingTile.index : this.tileElements_.length; | 534 getAvailableColCount_: function() { |
1211 this.currentDropIndex_ = this.dragItemIndex_; | 535 return this.getColCountForWidth_(this.getBottomPanelWidth_()); |
1212 | 536 }, |
1213 // The new tile may change the number of rows, hence the top margin | 537 |
1214 // will change. | 538 // rendering |
1215 if (!this.withinPageDrag_) | 539 // ------------------------------------------------------------------------- |
1216 this.updateTopMargin_(); | 540 |
1217 | 541 /** |
1218 this.doDragOver(e); | 542 * Renders the grid. |
1219 }, | 543 * @param {number} colCount The number of columns. |
1220 | 544 * @private |
1221 /** | 545 */ |
1222 * Performs all actions necessary when the user moves the cursor during | 546 renderGrid_: function(colCount) { |
1223 * a drag over the tile page. | 547 colCount = colCount || this.colCount_; |
1224 * @param {Event} e A mouseover event for the drag over. | 548 |
1225 */ | 549 var tileGridContent = this.tileGridContent_; |
1226 doDragOver: function(e) { | 550 var tiles = this.tiles_; |
1227 e.preventDefault(); | 551 var tileCount = tiles.length; |
1228 | 552 |
1229 this.setDropEffect(e.dataTransfer); | 553 var numOfVisibleRows = this.numOfVisibleRows_; |
1230 var newDragIndex = this.getWouldBeIndexForPoint_(e.pageX, e.pageY); | 554 var rowCount = Math.ceil(tileCount / colCount); |
1231 if (newDragIndex < 0 || newDragIndex >= this.tileElements_.length) | 555 rowCount = Math.max(rowCount, numOfVisibleRows); |
1232 newDragIndex = this.dragItemIndex_; | 556 var tileRows = tileGridContent.getElementsByClassName('tile-row'); |
1233 this.updateDropIndicator_(newDragIndex); | 557 |
1234 }, | 558 var pageOffset = this.pageOffset_; |
1235 | 559 |
1236 /** | 560 for (var tile = 0, row = 0; row < rowCount; row++) { |
1237 * Performs all actions necessary when the user completes a drop. | 561 var tileRow = tileRows[row]; |
1238 * @param {Event} e A mouseover event for the drag drop. | 562 |
1239 */ | 563 // Create tile row if there's no one yet. |
1240 doDrop: function(e) { | 564 if (!tileRow) { |
1241 e.stopPropagation(); | 565 tileRow = cr.doc.createElement('div'); |
1242 e.preventDefault(); | 566 tileRow.className = 'tile-row'; |
1243 | 567 tileGridContent.appendChild(tileRow); |
1244 var index = this.currentDropIndex_; | 568 } |
1245 // Only change data if this was not a 'null drag'. | 569 |
1246 if (!((index == this.dragItemIndex_) && this.withinPageDrag_)) { | 570 // Adjust row visibility. |
1247 var adjustedIndex = this.currentDropIndex_ + | 571 var rowVisible = row >= pageOffset && |
1248 (index > this.dragItemIndex_ ? 1 : 0); | 572 row <= (pageOffset + numOfVisibleRows - 1); |
1249 if (this.withinPageDrag_) { | 573 this.showTileRow_(tileRow, rowVisible); |
1250 this.tileGrid_.insertBefore( | 574 |
1251 currentlyDraggingTile, | 575 // The tiles inside the current row. |
1252 this.tileElements_[adjustedIndex]); | 576 var tileRowTiles = tileRow.childNodes; |
1253 this.tileMoved(currentlyDraggingTile, this.dragItemIndex_); | 577 |
| 578 // Remove excessive columns from a particular tile row. |
| 579 var maxColCount = Math.min(colCount, tileCount - tile); |
| 580 maxColCount = Math.max(0, maxColCount); |
| 581 while (tileRowTiles.length > maxColCount) { |
| 582 tileRow.removeChild(tileRow.lastElementChild); |
| 583 } |
| 584 |
| 585 // For each column in the current row. |
| 586 for (var col = 0; col < colCount; col++, tile++) { |
| 587 var tileCell; |
| 588 var tileElement; |
| 589 if (tileRowTiles[col]) { |
| 590 tileCell = tileRowTiles[col]; |
| 591 } else { |
| 592 var span = cr.doc.createElement('span'); |
| 593 tileCell = new TileCell(span, this.config_); |
| 594 } |
| 595 |
| 596 // Render Tiles. |
| 597 if (tile < tileCount) { |
| 598 tileCell.classList.remove('filler'); |
| 599 tileElement = tiles[tile]; |
| 600 if (!tileCell.firstChild) |
| 601 tileCell.appendChild(tileElement); |
| 602 else if (tileElement != tileCell.firstChild) |
| 603 tileCell.replaceChild(tileElement, tileCell.firstChild); |
| 604 } else if (!tileCell.classList.contains('filler')) { |
| 605 tileCell.classList.add('filler'); |
| 606 tileElement = cr.doc.createElement('span'); |
| 607 tileElement.className = 'tile'; |
| 608 if (tileCell.firstChild) |
| 609 tileCell.replaceChild(tileElement, tileCell.firstChild); |
| 610 else |
| 611 tileCell.appendChild(tileElement); |
| 612 } |
| 613 |
| 614 if (!tileRowTiles[col]) |
| 615 tileRow.appendChild(tileCell); |
| 616 } |
| 617 } |
| 618 |
| 619 // Remove excessive tile rows from the tile grid. |
| 620 while (tileRows.length > rowCount) { |
| 621 tileGridContent.removeChild(tileGridContent.lastElementChild); |
| 622 } |
| 623 |
| 624 this.colCount_ = colCount; |
| 625 this.rowCount_ = rowCount; |
| 626 }, |
| 627 |
| 628 /** |
| 629 * Adjusts the layout of the tile page according to the current window size. |
| 630 * @param {boolean} force Forces the layout. |
| 631 * @private |
| 632 */ |
| 633 layout_: function(force) { |
| 634 if (force) { |
| 635 this.numOfVisibleRows_ = 0; |
| 636 this.animatingColCount_ = 0; |
| 637 } |
| 638 |
| 639 var bottomPanelWidth = this.getBottomPanelWidth_(); |
| 640 var colCount = this.getColCountForWidth_(bottomPanelWidth); |
| 641 var lastColCount = this.colCount_; |
| 642 var animatingColCount = this.animatingColCount_; |
| 643 |
| 644 var windowHeight = cr.doc.documentElement.clientHeight; |
| 645 |
| 646 this.tileGridContent_.classList.add('animate-tile'); |
| 647 |
| 648 // TODO(pedrosimonetti): Better handling of height state. |
| 649 // TODO(pedrosimonetti): Add constants? |
| 650 var numOfVisibleRows = windowHeight > 500 ? 2 : 1; |
| 651 if (numOfVisibleRows != this.numOfVisibleRows_) { |
| 652 this.numOfVisibleRows_ = numOfVisibleRows; |
| 653 this.paginate_(null, true); |
| 654 $('page-list').style.height = |
| 655 (this.config_.rowHeight * numOfVisibleRows) + 'px'; |
| 656 } |
| 657 |
| 658 // changeVisibleCols |
| 659 if (colCount != animatingColCount) { |
| 660 var newWidth = this.getWidthForColCount_(colCount); |
| 661 if (colCount > animatingColCount) { |
| 662 // TODO(pedrosimonetti): Do an actual size check. |
| 663 if (colCount != lastColCount) |
| 664 this.renderGrid_(colCount); |
| 665 |
| 666 this.showTileCols_(animatingColCount, false); |
| 667 |
| 668 var self = this; |
| 669 // We need to save the animatingColCount value in a closure otherwise |
| 670 // the animation of tiles won't work when expanding horizontally. |
| 671 // The problem happens because the layout_ method is called several |
| 672 // times when resizing the window, and the animatingColCount is saved |
| 673 // and restored each time a new column has to be animated. So, if we |
| 674 // don't save the value, by the time the showTileCols_ method is |
| 675 // called the animatingColCount is holding a new value, breaking |
| 676 // the animation. |
| 677 setTimeout((function(animatingColCount) { |
| 678 return function() { |
| 679 self.showTileCols_(animatingColCount, true); |
| 680 } |
| 681 })(animatingColCount), 0); |
1254 } else { | 682 } else { |
1255 var originalPage = currentlyDraggingTile ? | 683 this.showTileCols_(colCount, false); |
1256 currentlyDraggingTile.tilePage : null; | 684 } |
1257 this.addDragData(e.dataTransfer, adjustedIndex); | 685 |
1258 if (originalPage) | 686 this.tileGrid_.style.width = newWidth + 'px'; |
1259 originalPage.cleanupDrag(); | 687 $('page-list-menu').style.width = newWidth + 'px'; |
1260 } | 688 |
1261 | 689 var self = this; |
1262 // Dropping the icon may cause topMargin to change, but changing it | 690 this.onTileGridAnimationEndHandler_ = function() { |
1263 // now would cause everything to move (annoying), so we leave it | 691 if (colCount < lastColCount) |
1264 // alone. The top margin will be re-calculated next time the window is | 692 self.renderGrid_(colCount); |
1265 // resized or the page is selected. | 693 else |
1266 } | 694 self.showTileCols_(0, true); |
1267 | 695 }; |
1268 this.classList.remove('animating-tile-page'); | 696 |
1269 this.cleanupDrag(); | 697 this.paginate_(); |
1270 }, | 698 } |
1271 | 699 |
1272 /** | 700 this.content_.style.width = bottomPanelWidth + 'px'; |
1273 * Appends the currently dragged tile to the end of the page. Called | 701 |
1274 * from outside the page, e.g. when dropping on a nav dot. | 702 this.animatingColCount_ = colCount; |
1275 */ | 703 }, |
1276 appendDraggingTile: function() { | 704 |
1277 var originalPage = currentlyDraggingTile.tilePage; | 705 // animation helpers |
1278 if (originalPage == this) | 706 // ------------------------------------------------------------------------- |
1279 return; | 707 |
1280 | 708 /** |
1281 this.addDragData(null, this.tileElements_.length); | 709 * Animates the display of a row. TODO(pedrosimonetti): Make it local? |
1282 if (originalPage) | 710 * @param {HTMLElement} row The row element. |
1283 originalPage.cleanupDrag(); | 711 * @param {boolean} show Whether or not to show the row. |
1284 }, | 712 */ |
1285 | 713 showTileRow_: function(row, show) { |
1286 /** | 714 row.classList[show ? 'remove' : 'add']('hide-row'); |
1287 * Makes sure all the tiles are in the right place after a drag is over. | 715 }, |
1288 */ | 716 |
1289 cleanupDrag: function() { | 717 /** |
1290 this.repositionTiles_(currentlyDraggingTile); | 718 * Animates the display of columns. TODO(pedrosimonetti): Make it local? |
1291 // Remove the drag mask. | 719 * @param {number} col The column number. |
1292 this.updateMask_(); | 720 * @param {boolean} show Whether or not to show the row. |
1293 }, | 721 */ |
1294 | 722 showTileCols_: function(col, show) { |
1295 /** | 723 var prop = show ? 'remove' : 'add'; |
1296 * Reposition all the tiles (possibly ignoring one). | 724 var max = 10; // TODO(pedrosimonetti): Add const? |
1297 * @param {?Node} ignoreNode An optional node to ignore. | 725 var tileGridContent = this.tileGridContent_; |
1298 * @private | 726 for (var i = col; i < max; i++) { |
1299 */ | 727 tileGridContent.classList[prop]('hide-col-' + i); |
1300 repositionTiles_: function(ignoreNode) { | 728 } |
1301 for (var i = 0; i < this.tileElements_.length; i++) { | 729 }, |
1302 if (!ignoreNode || ignoreNode !== this.tileElements_[i]) | 730 |
1303 this.positionTile_(i); | 731 // pagination |
1304 } | 732 // ------------------------------------------------------------------------- |
1305 }, | 733 |
1306 | 734 /** |
1307 /** | 735 * Resets the display of columns. |
1308 * Updates the visual indicator for the drop location for the active drag. | 736 * @param {number} pageOffset The index of the topmost row visible. |
1309 * @param {Event} e A MouseEvent for the drag. | 737 * @param {boolean} force Forces the pagination. |
1310 * @private | 738 */ |
1311 */ | 739 paginate_: function(pageOffset, force) { |
1312 updateDropIndicator_: function(newDragIndex) { | 740 var numOfVisibleRows = this.numOfVisibleRows_; |
1313 var oldDragIndex = this.currentDropIndex_; | 741 var pageOffset = typeof pageOffset == 'number' ? |
1314 if (newDragIndex == oldDragIndex) | 742 pageOffset : this.pageOffset_; |
1315 return; | 743 |
1316 | 744 pageOffset = Math.min(this.rowCount_ - numOfVisibleRows, pageOffset); |
1317 var repositionStart = Math.min(newDragIndex, oldDragIndex); | 745 pageOffset = Math.max(0, pageOffset); |
1318 var repositionEnd = Math.max(newDragIndex, oldDragIndex); | 746 |
1319 | 747 if (pageOffset != this.pageOffset || force) { |
1320 for (var i = repositionStart; i <= repositionEnd; i++) { | 748 var rows = this.tileGridContent_.getElementsByClassName('tile-row'); |
1321 if (i == this.dragItemIndex_) | 749 for (var i = 0, length = rows.length; i < length; i++) { |
1322 continue; | 750 var row = rows[i]; |
1323 else if (i > this.dragItemIndex_) | 751 var isRowVisible = i >= pageOffset && |
1324 var adjustment = i <= newDragIndex ? -1 : 0; | 752 i <= (pageOffset + numOfVisibleRows - 1); |
1325 else | 753 this.showTileRow_(row, isRowVisible); |
1326 var adjustment = i >= newDragIndex ? 1 : 0; | 754 } |
1327 | 755 |
1328 this.positionTile_(i, adjustment); | 756 this.pageOffset_ = pageOffset; |
1329 } | 757 this.tileGridContent_.style.webkitTransform = |
1330 this.currentDropIndex_ = newDragIndex; | 758 'translate3d(0,' + (-pageOffset * this.config_.rowHeight) + 'px,0)'; |
1331 }, | 759 } |
1332 | 760 }, |
1333 /** | 761 |
1334 * Checks if a page can accept a drag with the given data. | 762 // event handlers |
1335 * @param {Event} e The drag event if the drag object. Implementations will | 763 // ------------------------------------------------------------------------- |
1336 * likely want to check |e.dataTransfer|. | 764 |
1337 * @return {boolean} True if this page can handle the drag. | 765 /** |
1338 */ | 766 * Handles the window resize event. |
1339 shouldAcceptDrag: function(e) { | 767 */ |
1340 return false; | 768 onResize_: function() { |
1341 }, | 769 this.layout_(); |
1342 | 770 }, |
1343 /** | 771 |
1344 * Called to accept a drag drop. Will not be called for in-page drops. | 772 /** |
1345 * @param {Object} dataTransfer The data transfer object that holds the drop | 773 * Handles the end of the tile grid animation. |
1346 * data. This should only be used if currentlyDraggingTile is null. | 774 */ |
1347 * @param {number} index The tile index at which the drop occurred. | 775 onTileGridAnimationEnd_: function() { |
1348 */ | 776 // TODO(pedrosimonetti): Figure out how to cleanup each kind of |
1349 addDragData: function(dataTransfer, index) { | 777 // animation properly. |
1350 assert(false); | 778 if (event.target == this.tileGrid_ && |
1351 }, | 779 this.onTileGridAnimationEndHandler_ && |
1352 | 780 this.tileGridContent_.classList.contains('animate-tile')) { |
1353 /** | 781 this.onTileGridAnimationEndHandler_(); |
1354 * Called when a tile has been moved (via dragging). Override this to make | 782 } |
1355 * backend updates. | 783 }, |
1356 * @param {Node} draggedTile The tile that was dropped. | 784 |
1357 * @param {number} prevIndex The previous index of the tile. | 785 /** |
1358 */ | 786 * Handles the window keyup event. |
1359 tileMoved: function(draggedTile, prevIndex) { | 787 * @param {Event} e The keyboard event. |
1360 }, | 788 */ |
1361 | 789 onKeyUp_: function(e) { |
1362 /** | 790 var pageOffset = this.pageOffset_; |
1363 * Sets the drop effect on |dataTransfer| to the desired value (e.g. | 791 |
1364 * 'copy'). | 792 var keyCode = e.keyCode; |
1365 * @param {Object} dataTransfer The drag event dataTransfer object. | 793 if (keyCode == 40 /* down */) |
1366 */ | 794 pageOffset++; |
1367 setDropEffect: function(dataTransfer) { | 795 else if (keyCode == 38 /* up */) |
1368 assert(false); | 796 pageOffset--; |
1369 }, | 797 |
| 798 // Changes the pagination according to which arrow key was pressed. |
| 799 if (pageOffset != this.pageOffset_) |
| 800 this.paginate_(pageOffset); |
| 801 } |
1370 }; | 802 }; |
1371 | 803 |
| 804 /** |
| 805 * Shows a deprecate error. |
| 806 */ |
| 807 function deprecate() { |
| 808 console.error('This function is deprecated!'); |
| 809 } |
| 810 |
1372 return { | 811 return { |
1373 getCurrentlyDraggingTile: getCurrentlyDraggingTile, | 812 // TODO(pedrosimonetti): Drag. Delete after porting the rest of the code. |
1374 setCurrentDropEffect: setCurrentDropEffect, | 813 getCurrentlyDraggingTile2: deprecate, |
1375 TilePage: TilePage, | 814 setCurrentDropEffect2: deprecate, |
| 815 Tile2: Tile, |
| 816 TilePage2: TilePage |
1376 }; | 817 }; |
1377 }); | 818 }); |
OLD | NEW |