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