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 5cr.define('ntp', function() { 6 'use strict'; 7 8 // We can't pass the currently dragging tile via dataTransfer because of 9 // http://crbug.com/31037 10 var currentlyDraggingTile = null; 11 function getCurrentlyDraggingTile() { 12 return currentlyDraggingTile; 13 } 14 function setCurrentlyDraggingTile(tile) { 15 currentlyDraggingTile = tile; 16 if (tile) 17 ntp.enterRearrangeMode(); 18 else 19 ntp.leaveRearrangeMode(); 20 } 21 22 /** 23 * Changes the current dropEffect of a drag. This modifies the native cursor 24 * and serves as an indicator of what we should do at the end of the drag as 25 * well as give indication to the user if a drop would succeed if they let go. 26 * @param {DataTransfer} dataTransfer A dataTransfer object from a drag event. 27 * @param {string} effect A drop effect to change to (i.e. copy, move, none). 28 */ 29 function setCurrentDropEffect(dataTransfer, effect) { 30 dataTransfer.dropEffect = effect; 31 if (currentlyDraggingTile) 32 currentlyDraggingTile.lastDropEffect = dataTransfer.dropEffect; 33 } 34 35 /** 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, 51 52 initialize: function(contents) { 53 // 'real' as opposed to doppleganger. 54 this.className = 'tile real'; 55 this.appendChild(contents); 56 contents.tile = this; 57 58 this.addEventListener('dragstart', this.onDragStart_); 59 this.addEventListener('drag', this.onDragMove_); 60 this.addEventListener('dragend', this.onDragEnd_); 61 62 this.firstChild.addEventListener( 63 'webkitAnimationEnd', this.onContentsAnimationEnd_.bind(this)); 64 65 this.eventTracker = new EventTracker(); 66 }, 67 68 get index() { 69 return Array.prototype.indexOf.call(this.tilePage.tileElements_, this); 70 }, 71 72 get tilePage() { 73 return findAncestorByClass(this, 'tile-page'); 74 }, 75 76 /** 77 * Position the tile at |x, y|, and store this as the grid location, i.e. 78 * where the tile 'belongs' when it's not being dragged. 79 * @param {number} x The x coordinate, in pixels. 80 * @param {number} y The y coordinate, in pixels. 81 */ 82 setGridPosition: function(x, y) { 83 this.gridX = x; 84 this.gridY = y; 85 this.moveTo(x, y); 86 }, 87 88 /** 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. 297 * @param {boolean=} opt_animate Whether the animation should be animated. 298 */ 299 doRemove: function(opt_animate) { 300 if (opt_animate) 301 this.firstChild.classList.add('removing-tile-contents'); 302 else 303 this.tilePage.removeTile(this, false); 304 }, 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 }; 317 318 /** 319 * Gives the proportion of the row width that is devoted to a single icon. 320 * @param {number} rowTileCount The number of tiles in a row. 321 * @param {number} tileSpacingFraction The proportion of the tile width which 322 * will be used as spacing between tiles. 323 * @return {number} The ratio between icon width and row width. 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 355 /** 356 * Creates a new TilePage object. This object contains tiles and controls 357 * their layout. 358 * @param {Object} gridValues Pixel values that define the size and layout 359 * of the tile grid. 360 * @constructor 361 * @extends {HTMLDivElement} 362 */ 363 function TilePage(gridValues) { 364 var el = cr.doc.createElement('div'); 365 el.gridValues_ = gridValues; 366 el.__proto__ = TilePage.prototype; 367 el.initialize(); 368 369 return el; 370 } 371 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 = { 402 __proto__: HTMLDivElement.prototype, 403 404 initialize: function() { 405 this.className = 'tile-page'; 406 407 // Div that acts as a custom scrollbar. The scrollbar has to live 408 // outside the content div so it doesn't flicker when scrolling (due to 409 // repainting after the scroll, then repainting again when moved in the 410 // onScroll handler). |scrollbar_| is only aesthetic, and it only 411 // represents the thumb. Actual events are still handled by the invisible 412 // native scrollbars. This div gives us more flexibility with the visuals. 413 this.scrollbar_ = this.ownerDocument.createElement('div'); 414 this.scrollbar_.className = 'tile-page-scrollbar'; 415 this.scrollbar_.hidden = true; 416 this.appendChild(this.scrollbar_); 417 418 // This contains everything but the scrollbar. 419 this.content_ = this.ownerDocument.createElement('div'); 420 this.content_.className = 'tile-page-content'; 421 this.appendChild(this.content_); 422 423 // Div that sets the vertical position of the tile grid. 424 this.topMargin_ = this.ownerDocument.createElement('div'); 425 this.topMargin_.className = 'top-margin'; 426 this.content_.appendChild(this.topMargin_); 427 428 // Div that holds the tiles. 429 this.tileGrid_ = this.ownerDocument.createElement('div'); 430 this.tileGrid_.className = 'tile-grid'; 431 this.tileGrid_.style.minWidth = this.gridValues_.narrowWidth + 'px'; 432 this.tileGrid_.setAttribute('role', 'menu'); 433 this.tileGrid_.setAttribute('aria-label', 434 loadTimeData.getString( 435 'tile_grid_screenreader_accessible_description')); 436 437 this.content_.appendChild(this.tileGrid_); 438 439 // Ordered list of our tiles. 440 this.tileElements_ = this.tileGrid_.getElementsByClassName('tile real'); 441 // Ordered list of the elements which want to accept keyboard focus. These 442 // elements will not be a part of the normal tab order; the tile grid 443 // initially gets focused and then these elements can be focused via the 444 // arrow keys. 445 this.focusableElements_ = 446 this.tileGrid_.getElementsByClassName('focusable'); 447 448 // These are properties used in updateTopMargin. 449 this.animatedTopMarginPx_ = 0; 450 this.topMarginPx_ = 0; 451 452 this.eventTracker = new EventTracker(); 453 this.eventTracker.add(window, 'resize', this.onResize_.bind(this)); 454 455 this.addEventListener('DOMNodeInsertedIntoDocument', 456 this.onNodeInsertedIntoDocument_); 457 458 this.content_.addEventListener('scroll', this.onScroll_.bind(this)); 459 460 this.dragWrapper_ = new cr.ui.DragWrapper(this.tileGrid_, this); 461 462 this.addEventListener('cardselected', this.handleCardSelection_); 463 this.addEventListener('carddeselected', this.handleCardDeselection_); 464 this.addEventListener('focus', this.handleFocus_); 465 this.addEventListener('keydown', this.handleKeyDown_); 466 this.addEventListener('mousedown', this.handleMouseDown_); 467 468 this.focusElementIndex_ = -1; 469 }, 470 471 get tiles() { 472 return this.tileElements_; 473 }, 474 475 get tileCount() { 476 return this.tileElements_.length; 477 }, 478 479 get selected() { 480 return Array.prototype.indexOf.call(this.parentNode.children, this) == 481 ntp.getCardSlider().currentCard; 482 }, 483 484 /** 485 * The size of the margin (unused space) on the sides of the tile grid, in 486 * pixels. 487 * @type {number} 488 */ 489 get sideMargin() { 490 return this.layoutValues_.leftMargin; 491 }, 492 493 /** 494 * Returns the width of the scrollbar, in pixels, if it is active, or 0 495 * otherwise. 496 * @type {number} 497 */ 498 get scrollbarWidth() { 499 return this.scrollbar_.hidden ? 0 : 13; 500 }, 501 502 /** 503 * Returns any extra padding to insert to the bottom of a tile page. By 504 * default there is none, but subclasses can override. 505 * @type {number} 506 */ 507 get extraBottomPadding() { 508 return 0; 509 }, 510 511 /** 512 * The notification content of this tile (if any, otherwise null). 513 * @type {!HTMLElement} 514 */ 515 get notification() { 516 return this.topMargin_.nextElementSibling.id == 'notification-container' ? 517 this.topMargin_.nextElementSibling : null; 518 }, 519 /** 520 * The notification content of this tile (if any, otherwise null). 521 * @type {!HTMLElement} 522 */ 523 set notification(node) { 524 assert(node instanceof HTMLElement, '|node| isn\'t an HTMLElement!'); 525 // NOTE: Implicitly removes from DOM if |node| is inside it. 526 this.content_.insertBefore(node, this.topMargin_.nextElementSibling); 527 this.positionNotification_(); 528 }, 529 530 /** 531 * Fetches the size, in pixels, of the padding-top of the tile contents. 532 * @type {number} 533 */ 534 get contentPadding() { 535 if (typeof this.contentPadding_ == 'undefined') { 536 this.contentPadding_ = 537 parseInt(getComputedStyle(this.content_).paddingTop, 10); 538 } 539 return this.contentPadding_; 540 }, 541 542 /** 543 * Removes the tilePage from the DOM and cleans up event handlers. 544 */ 545 remove: function() { 546 // This checks arguments.length as most remove functions have a boolean 547 // |opt_animate| argument, but that's not necesarilly applicable to 548 // removing a tilePage. Selecting a different card in an animated way and 549 // deleting the card afterward is probably a better choice. 550 assert(typeof arguments[0] != 'boolean', 551 'This function takes no |opt_animate| argument.'); 552 this.tearDown_(); 553 this.parentNode.removeChild(this); 554 }, 555 556 /** 557 * Cleans up resources that are no longer needed after this TilePage 558 * instance is removed from the DOM. 559 * @private 560 */ 561 tearDown_: function() { 562 this.eventTracker.removeAll(); 563 }, 564 565 /** 566 * Appends a tile to the end of the tile grid. 567 * @param {HTMLElement} tileElement The contents of the tile. 568 * @param {boolean} animate If true, the append will be animated. 569 * @protected 570 */ 571 appendTile: function(tileElement, animate) { 572 this.addTileAt(tileElement, this.tileElements_.length, animate); 573 }, 574 575 /** 576 * Adds the given element to the tile grid. 577 * @param {Node} tileElement The tile object/node to insert. 578 * @param {number} index The location in the tile grid to insert it at. 579 * @param {boolean} animate If true, the tile in question will be 580 * animated (other tiles, if they must reposition, do not animate). 581 * @protected 582 */ 583 addTileAt: function(tileElement, index, animate) { 584 this.classList.remove('animating-tile-page'); 585 if (animate) 586 tileElement.classList.add('new-tile-contents'); 587 588 // Make sure the index is positive and either in the the bounds of 589 // this.tileElements_ or at the end (meaning append). 590 assert(index >= 0 && index <= this.tileElements_.length); 591 592 var wrapperDiv = new Tile(tileElement); 593 // If is out of the bounds of the tile element list, .insertBefore() will 594 // act just like appendChild(). 595 this.tileGrid_.insertBefore(wrapperDiv, this.tileElements_[index]); 596 this.calculateLayoutValues_(); 597 this.heightChanged_(); 598 599 this.repositionTiles_(); 600 601 // If this is the first tile being added, make it focusable after add. 602 if (this.focusableElements_.length == 1) 603 this.updateFocusableElement(); 604 this.fireAddedEvent(wrapperDiv, index, animate); 605 }, 606 607 /** 608 * Notify interested subscribers that a tile has been removed from this 609 * page. 610 * @param {Tile} tile The newly added tile. 611 * @param {number} index The index of the tile that was added. 612 * @param {boolean} wasAnimated Whether the removal was animated. 613 */ 614 fireAddedEvent: function(tile, index, wasAnimated) { 615 var e = document.createEvent('Event'); 616 e.initEvent('tilePage:tile_added', true, true); 617 e.addedIndex = index; 618 e.addedTile = tile; 619 e.wasAnimated = wasAnimated; 620 this.dispatchEvent(e); 621 }, 622 623 /** 624 * Removes the given tile and animates the repositioning of the other tiles. 625 * @param {boolean=} opt_animate Whether the removal should be animated. 626 * @param {boolean=} opt_dontNotify Whether a page should be removed if the 627 * last tile is removed from it. 628 */ 629 removeTile: function(tile, opt_animate, opt_dontNotify) { 630 if (opt_animate) 631 this.classList.add('animating-tile-page'); 632 633 var index = tile.index; 634 tile.parentNode.removeChild(tile); 635 this.calculateLayoutValues_(); 636 this.cleanupDrag(); 637 this.updateFocusableElement(); 638 639 if (!opt_dontNotify) 640 this.fireRemovedEvent(tile, index, !!opt_animate); 641 }, 642 643 /** 644 * Notify interested subscribers that a tile has been removed from this 645 * page. 646 * @param {Tile} tile The tile that was removed. 647 * @param {number} oldIndex Where the tile was positioned before removal. 648 * @param {boolean} wasAnimated Whether the removal was animated. 649 */ 650 fireRemovedEvent: function(tile, oldIndex, wasAnimated) { 651 var e = document.createEvent('Event'); 652 e.initEvent('tilePage:tile_removed', true, true); 653 e.removedIndex = oldIndex; 654 e.removedTile = tile; 655 e.wasAnimated = wasAnimated; 656 this.dispatchEvent(e); 657 }, 658 659 /** 660 * Removes all tiles from the page. 661 */ 662 removeAllTiles: function() { 663 this.tileGrid_.innerHTML = ''; 664 }, 665 666 /** 667 * Called when the page is selected (in the card selector). 668 * @param {Event} e A custom cardselected event. 669 * @private 670 */ 671 handleCardSelection_: function(e) { 672 this.updateFocusableElement(); 673 674 // When we are selected, we re-calculate the layout values. (See comment 675 // in doDrop.) 676 this.calculateLayoutValues_(); 677 }, 678 679 /** 680 * Called when the page loses selection (in the card selector). 681 * @param {Event} e A custom carddeselected event. 682 * @private 683 */ 684 handleCardDeselection_: function(e) { 685 if (this.currentFocusElement_) 686 this.currentFocusElement_.tabIndex = -1; 687 }, 688 689 /** 690 * When we get focus, pass it on to the focus element. 691 * @param {Event} e The focus event. 692 * @private 693 */ 694 handleFocus_: function(e) { 695 if (this.focusableElements_.length == 0) 696 return; 697 698 this.updateFocusElement_(); 699 }, 700 701 /** 702 * Since we are doing custom focus handling, we have to manually 703 * set focusability on click (as well as keyboard nav above). 704 * @param {Event} e The focus event. 705 * @private 706 */ 707 handleMouseDown_: function(e) { 708 var focusable = findAncestorByClass(e.target, 'focusable'); 709 if (focusable) { 710 this.focusElementIndex_ = 711 Array.prototype.indexOf.call(this.focusableElements_, 712 focusable); 713 this.updateFocusElement_(); 714 } else { 715 // This prevents the tile page from getting focus when the user clicks 716 // inside the grid but outside of any tile. 717 e.preventDefault(); 718 } 719 }, 720 721 /** 722 * Handle arrow key focus nav. 723 * @param {Event} e The focus event. 724 * @private 725 */ 726 handleKeyDown_: function(e) { 727 // We only handle up, down, left, right without control keys. 728 if (e.metaKey || e.shiftKey || e.altKey || e.ctrlKey) 729 return; 730 731 // Wrap the given index to |this.focusableElements_|. 732 var wrap = function(idx) { 733 return (idx + this.focusableElements_.length) % 734 this.focusableElements_.length; 735 }.bind(this); 736 737 switch (e.keyIdentifier) { 738 case 'Right': 739 case 'Left': 740 var direction = e.keyIdentifier == 'Right' ? 1 : -1; 741 this.focusElementIndex_ = wrap(this.focusElementIndex_ + direction); 742 break; 743 case 'Up': 744 case 'Down': 745 // Look through all focusable elements. Find the first one that is 746 // in the same column. 747 var direction = e.keyIdentifier == 'Up' ? -1 : 1; 748 var currentIndex = 749 Array.prototype.indexOf.call(this.focusableElements_, 750 this.currentFocusElement_); 751 var newFocusIdx = wrap(currentIndex + direction); 752 var tile = this.currentFocusElement_.parentNode; 753 for (;; newFocusIdx = wrap(newFocusIdx + direction)) { 754 var newTile = this.focusableElements_[newFocusIdx].parentNode; 755 var rowTiles = this.layoutValues_.numRowTiles; 756 if ((newTile.index - tile.index) % rowTiles == 0) 757 break; 758 } 759 760 this.focusElementIndex_ = newFocusIdx; 761 break; 762 763 default: 764 return; 765 } 766 767 this.updateFocusElement_(); 768 769 e.preventDefault(); 770 e.stopPropagation(); 771 }, 772 773 /** 774 * Ensure 0 <= this.focusElementIndex_ < this.focusableElements_.length, 775 * make the focusable element at this.focusElementIndex_ (if any) eligible 776 * for tab focus, and the previously-focused element not eligible. 777 * @protected 778 */ 779 updateFocusableElement: function() { 780 if (this.focusableElements_.length == 0 || !this.selected) { 781 this.focusElementIndex_ = -1; 782 return; 783 } 784 785 this.focusElementIndex_ = Math.min(this.focusableElements_.length - 1, 786 this.focusElementIndex_); 787 this.focusElementIndex_ = Math.max(0, this.focusElementIndex_); 788 789 var newFocusElement = this.focusableElements_[this.focusElementIndex_]; 790 var lastFocusElement = this.currentFocusElement_; 791 if (lastFocusElement && lastFocusElement != newFocusElement) 792 lastFocusElement.tabIndex = -1; 793 794 newFocusElement.tabIndex = 1; 795 }, 796 797 /** 798 * Focuses the element at |this.focusElementIndex_|. Makes the previous 799 * focus element, if any, no longer eligible for tab focus. 800 * @private 801 */ 802 updateFocusElement_: function() { 803 this.updateFocusableElement(); 804 if (this.focusElementIndex_ >= 0) 805 this.focusableElements_[this.focusElementIndex_].focus(); 806 }, 807 808 /** 809 * The current focus element is that element which is eligible for focus. 810 * @type {HTMLElement} The node. 811 * @private 812 */ 813 get currentFocusElement_() { 814 return this.querySelector('.focusable[tabindex="1"]'); 815 }, 816 817 /** 818 * Makes some calculations for tile layout. These change depending on 819 * height, width, and the number of tiles. 820 * TODO(estade): optimize calls to this function. Do nothing if the page is 821 * hidden, but call before being shown. 822 * @private 823 */ 824 calculateLayoutValues_: function() { 825 var grid = this.gridValues_; 826 var availableSpace = this.tileGrid_.clientWidth - 2 * MIN_WIDE_MARGIN; 827 var wide = availableSpace >= grid.minWideWidth; 828 var numRowTiles = wide ? grid.maxColCount : grid.minColCount; 829 830 var effectiveGridWidth = wide ? 831 Math.min(Math.max(availableSpace, grid.minWideWidth), 832 grid.maxWideWidth) : 833 grid.narrowWidth; 834 var realTileValues = tileValuesForGrid(effectiveGridWidth, numRowTiles, 835 grid.tileSpacingFraction); 836 837 // leftMargin centers the grid within the avaiable space. 838 var minMargin = wide ? MIN_WIDE_MARGIN : 0; 839 var leftMargin = 840 Math.max(minMargin, 841 (this.tileGrid_.clientWidth - effectiveGridWidth) / 2); 842 843 var rowHeight = this.heightForWidth(realTileValues.tileWidth) + 844 realTileValues.interTileSpacing; 845 846 this.layoutValues_ = { 847 colWidth: realTileValues.offsetX, 848 gridWidth: effectiveGridWidth, 849 leftMargin: leftMargin, 850 numRowTiles: numRowTiles, 851 rowHeight: rowHeight, 852 tileWidth: realTileValues.tileWidth, 853 wide: wide, 854 }; 855 856 // We need to update the top margin as well. 857 this.updateTopMargin_(); 858 859 this.firePageLayoutEvent_(); 860 }, 861 862 /** 863 * Dispatches the custom pagelayout event. 864 * @private 865 */ 866 firePageLayoutEvent_: function() { 867 cr.dispatchSimpleEvent(this, 'pagelayout', true, true); 868 }, 869 870 /** 871 * @return {number} The amount of margin that should be animated (in pixels) 872 * for the current grid layout. 873 */ 874 getAnimatedLeftMargin_: function() { 875 if (this.layoutValues_.wide) 876 return 0; 877 878 var grid = this.gridValues_; 879 return (grid.minWideWidth - MIN_WIDE_MARGIN - grid.narrowWidth) / 2; 880 }, 881 882 /** 883 * Calculates the x/y coordinates for an element and moves it there. 884 * @param {number} index The index of the element to be positioned. 885 * @param {number} indexOffset If provided, this is added to |index| when 886 * positioning the tile. The effect is that the tile will be positioned 887 * in a non-default location. 888 * @private 889 */ 890 positionTile_: function(index, indexOffset) { 891 var grid = this.gridValues_; 892 var layout = this.layoutValues_; 893 894 indexOffset = typeof indexOffset != 'undefined' ? indexOffset : 0; 895 // Add the offset _after_ the modulus division. We might want to show the 896 // tile off the side of the grid. 897 var col = index % layout.numRowTiles + indexOffset; 898 var row = Math.floor(index / layout.numRowTiles); 899 // Calculate the final on-screen position for the tile. 900 var realX = col * layout.colWidth + layout.leftMargin; 901 var realY = row * layout.rowHeight; 902 903 // Calculate the portion of the tile's position that should be animated. 904 var animatedTileValues = layout.wide ? 905 grid.wideTileValues : grid.narrowTileValues; 906 // Animate the difference between three-wide and six-wide. 907 var animatedLeftMargin = this.getAnimatedLeftMargin_(); 908 var animatedX = col * animatedTileValues.offsetX + animatedLeftMargin; 909 var animatedY = row * (this.heightForWidth(animatedTileValues.tileWidth) + 910 animatedTileValues.interTileSpacing); 911 912 var tile = this.tileElements_[index]; 913 tile.setGridPosition(animatedX, animatedY); 914 tile.firstChild.setBounds(layout.tileWidth, 915 realX - animatedX, 916 realY - animatedY); 917 918 // This code calculates whether the tile needs to show a clone of itself 919 // wrapped around the other side of the tile grid. 920 var offTheRight = col == layout.numRowTiles || 921 (col == layout.numRowTiles - 1 && tile.hasDoppleganger()); 922 var offTheLeft = col == -1 || (col == 0 && tile.hasDoppleganger()); 923 if (this.isCurrentDragTarget && (offTheRight || offTheLeft)) { 924 var sign = offTheRight ? 1 : -1; 925 tile.showDoppleganger(-layout.numRowTiles * layout.colWidth * sign, 926 layout.rowHeight * sign); 927 } else { 928 tile.clearDoppleganger(); 929 } 930 931 if (index == this.tileElements_.length - 1) { 932 this.tileGrid_.style.height = (realY + layout.rowHeight) + 'px'; 933 this.queueUpdateScrollbars_(); 934 } 935 }, 936 937 /** 938 * Gets the index of the tile that should occupy coordinate (x, y). Note 939 * that this function doesn't care where the tiles actually are, and will 940 * return an index even for the space between two tiles. This function is 941 * effectively the inverse of |positionTile_|. 942 * @param {number} x The x coordinate, in pixels, relative to the left of 943 * |this|. 944 * @param {number} y The y coordinate, in pixels, relative to the top of 945 * |this|. 946 * @private 947 */ 948 getWouldBeIndexForPoint_: function(x, y) { 949 var grid = this.gridValues_; 950 var layout = this.layoutValues_; 951 952 var gridClientRect = this.tileGrid_.getBoundingClientRect(); 953 var col = Math.floor((x - gridClientRect.left - layout.leftMargin) / 954 layout.colWidth); 955 if (col < 0 || col >= layout.numRowTiles) 956 return -1; 957 958 if (isRTL()) 959 col = layout.numRowTiles - 1 - col; 960 961 var row = Math.floor((y - gridClientRect.top) / layout.rowHeight); 962 return row * layout.numRowTiles + col; 963 }, 964 965 /** 966 * Window resize event handler. Window resizes may trigger re-layouts. 967 * @param {Object} e The resize event. 968 */ 969 onResize_: function(e) { 970 if (this.lastWidth_ == this.clientWidth && 971 this.lastHeight_ == this.clientHeight) { 972 return; 973 } 974 975 this.calculateLayoutValues_(); 976 977 this.lastWidth_ = this.clientWidth; 978 this.lastHeight_ = this.clientHeight; 979 this.classList.add('animating-tile-page'); 980 this.heightChanged_(); 981 982 this.positionNotification_(); 983 this.repositionTiles_(); 984 }, 985 986 /** 987 * The tile grid has an image mask which fades at the edges. We only show 988 * the mask when there is an active drag; it obscures doppleganger tiles 989 * as they enter or exit the grid. 990 * @private 991 */ 992 updateMask_: function() { 993 if (!this.isCurrentDragTarget) { 994 this.tileGrid_.style.WebkitMaskBoxImage = ''; 995 return; 996 } 997 998 var leftMargin = this.layoutValues_.leftMargin; 999 // The fade distance is the space between tiles. 1000 var fadeDistance = (this.gridValues_.tileSpacingFraction * 1001 this.layoutValues_.tileWidth); 1002 fadeDistance = Math.min(leftMargin, fadeDistance); 1003 // On Skia we don't use any fade because it works very poorly. See 1004 // http://crbug.com/99373 1005 if (!cr.isMac) 1006 fadeDistance = 1; 1007 var gradient = 1008 '-webkit-linear-gradient(left,' + 1009 'transparent, ' + 1010 'transparent ' + (leftMargin - fadeDistance) + 'px, ' + 1011 'black ' + leftMargin + 'px, ' + 1012 'black ' + (this.tileGrid_.clientWidth - leftMargin) + 'px, ' + 1013 'transparent ' + (this.tileGrid_.clientWidth - leftMargin + 1014 fadeDistance) + 'px, ' + 1015 'transparent)'; 1016 this.tileGrid_.style.WebkitMaskBoxImage = gradient; 1017 }, 1018 1019 updateTopMargin_: function() { 1020 var layout = this.layoutValues_; 1021 1022 // The top margin is set so that the vertical midpoint of the grid will 1023 // be 1/3 down the page. 1024 var numTiles = this.tileCount + 1025 (this.isCurrentDragTarget && !this.withinPageDrag_ ? 1 : 0); 1026 var numRows = Math.max(1, Math.ceil(numTiles / layout.numRowTiles)); 1027 var usedHeight = layout.rowHeight * numRows; 1028 var newMargin = document.documentElement.clientHeight / 3 - 1029 usedHeight / 3 - this.contentPadding; 1030 // The 'height' style attribute of topMargin is non-zero to work around 1031 // webkit's collapsing margin behavior, so we have to factor that into 1032 // our calculations here. 1033 newMargin = Math.max(newMargin, 0) - this.topMargin_.offsetHeight; 1034 1035 // |newMargin| is the final margin we actually want to show. However, 1036 // part of that should be animated and part should not (for the same 1037 // reason as with leftMargin). The approach is to consider differences 1038 // when the layout changes from wide to narrow or vice versa as 1039 // 'animatable'. These differences accumulate in animatedTopMarginPx_, 1040 // while topMarginPx_ caches the real (total) margin. Either of these 1041 // calculations may come out to be negative, so we use margins as the 1042 // css property. 1043 1044 if (typeof this.topMarginIsForWide_ == 'undefined') 1045 this.topMarginIsForWide_ = layout.wide; 1046 if (this.topMarginIsForWide_ != layout.wide) { 1047 this.animatedTopMarginPx_ += newMargin - this.topMarginPx_; 1048 this.topMargin_.style.marginBottom = toCssPx(this.animatedTopMarginPx_); 1049 } 1050 1051 this.topMarginIsForWide_ = layout.wide; 1052 this.topMarginPx_ = newMargin; 1053 this.topMargin_.style.marginTop = 1054 toCssPx(this.topMarginPx_ - this.animatedTopMarginPx_); 1055 }, 1056 1057 /** 1058 * Position the notification if there's one showing. 1059 */ 1060 positionNotification_: function() { 1061 var notification = this.notification; 1062 if (!notification || notification.hidden) 1063 return; 1064 1065 // Update the horizontal position. 1066 var animatedLeftMargin = this.getAnimatedLeftMargin_(); 1067 notification.style.WebkitMarginStart = animatedLeftMargin + 'px'; 1068 var leftOffset = (this.layoutValues_.leftMargin - animatedLeftMargin) * 1069 (isRTL() ? -1 : 1); 1070 notification.style.WebkitTransform = 'translateX(' + leftOffset + 'px)'; 1071 1072 // Update the allowable widths of the text. 1073 var buttonWidth = notification.querySelector('button').offsetWidth + 8; 1074 notification.querySelector('span').style.maxWidth = 1075 this.layoutValues_.gridWidth - buttonWidth + 'px'; 1076 1077 // This makes sure the text doesn't condense smaller than the narrow size 1078 // of the grid (e.g. when a user makes the window really small). 1079 notification.style.minWidth = 1080 this.gridValues_.narrowWidth - buttonWidth + 'px'; 1081 1082 // Update the top position. 1083 notification.style.marginTop = -notification.offsetHeight + 'px'; 1084 }, 1085 1086 /** 1087 * Handles final setup that can only happen after |this| is inserted into 1088 * the page. 1089 * @private 1090 */ 1091 onNodeInsertedIntoDocument_: function(e) { 1092 this.calculateLayoutValues_(); 1093 this.heightChanged_(); 1094 }, 1095 1096 /** 1097 * Called when the height of |this| has changed: update the size of 1098 * tileGrid. 1099 * @private 1100 */ 1101 heightChanged_: function() { 1102 // The tile grid will expand to the bottom footer, or enough to hold all 1103 // the tiles, whichever is greater. It would be nicer if tilePage were 1104 // a flex box, and the tile grid could be box-flex: 1, but this exposes a 1105 // bug where repositioning tiles will cause the scroll position to reset. 1106 this.tileGrid_.style.minHeight = (this.clientHeight - 1107 this.tileGrid_.offsetTop - this.content_.offsetTop - 1108 this.extraBottomPadding - 1109 (this.footerNode_ ? this.footerNode_.clientHeight : 0)) + 'px'; 1110 }, 1111 1112 /** 1113 * Places an element at the bottom of the content div. Used in bare-minimum 1114 * mode to hold #footer. 1115 * @param {HTMLElement} footerNode The node to append to content. 1116 */ 1117 appendFooter: function(footerNode) { 1118 this.footerNode_ = footerNode; 1119 this.content_.appendChild(footerNode); 1120 }, 1121 1122 /** 1123 * Scrolls the page in response to an mousewheel event, although the event 1124 * may have been triggered on a different element. Return true if the 1125 * event triggered scrolling, and false otherwise. 1126 * This is called explicitly, which allows a consistent experience whether 1127 * the user scrolls on the page or on the page switcher, because this 1128 * function provides a common conversion factor between wheel delta and 1129 * scroll delta. 1130 * @param {Event} e The mousewheel event. 1131 */ 1132 handleMouseWheel: function(e) { 1133 if (e.wheelDeltaY == 0) 1134 return false; 1135 1136 this.content_.scrollTop -= e.wheelDeltaY / 3; 1137 return true; 1138 }, 1139 1140 /** 1141 * Handler for the 'scroll' event on |content_|. 1142 * @param {Event} e The scroll event. 1143 * @private 1144 */ 1145 onScroll_: function(e) { 1146 this.queueUpdateScrollbars_(); 1147 }, 1148 1149 /** 1150 * ID of scrollbar update timer. If 0, there's no scrollbar re-calc queued. 1151 * @private 1152 */ 1153 scrollbarUpdate_: 0, 1154 1155 /** 1156 * Queues an update on the custom scrollbar. Used for two reasons: first, 1157 * coalescing of multiple updates, and second, because action like 1158 * repositioning a tile can require a delay before they affect values 1159 * like clientHeight. 1160 * @private 1161 */ 1162 queueUpdateScrollbars_: function() { 1163 if (this.scrollbarUpdate_) 1164 return; 1165 1166 this.scrollbarUpdate_ = window.setTimeout( 1167 this.doUpdateScrollbars_.bind(this), 0); 1168 }, 1169 1170 /** 1171 * Does the work of calculating the visibility, height and position of the 1172 * scrollbar thumb (there is no track or buttons). 1173 * @private 1174 */ 1175 doUpdateScrollbars_: function() { 1176 this.scrollbarUpdate_ = 0; 1177 1178 var content = this.content_; 1179 1180 // Adjust scroll-height to account for possible header-bar. 1181 var adjustedScrollHeight = content.scrollHeight - content.offsetTop; 1182 1183 if (adjustedScrollHeight <= content.clientHeight) { 1184 this.scrollbar_.hidden = true; 1185 return; 1186 } else { 1187 this.scrollbar_.hidden = false; 1188 } 1189 1190 var thumbTop = content.offsetTop + 1191 content.scrollTop / adjustedScrollHeight * content.clientHeight; 1192 var thumbHeight = content.clientHeight / adjustedScrollHeight * 1193 this.clientHeight; 1194 1195 this.scrollbar_.style.top = thumbTop + 'px'; 1196 this.scrollbar_.style.height = thumbHeight + 'px'; 1197 this.firePageLayoutEvent_(); 1198 }, 1199 1200 /** 1201 * Get the height for a tile of a certain width. Override this function to 1202 * get non-square tiles. 1203 * @param {number} width The pixel width of a tile. 1204 * @return {number} The height for |width|. 1205 */ 1206 heightForWidth: function(width) { 1207 return width; 1208 }, 1209 1210 /** Dragging **/ 1211 1212 get isCurrentDragTarget() { 1213 return this.dragWrapper_.isCurrentDragTarget; 1214 }, 1215 1216 /** 1217 * Thunk for dragleave events fired on |tileGrid_|. 1218 * @param {Event} e A MouseEvent for the drag. 1219 */ 1220 doDragLeave: function(e) { 1221 this.cleanupDrag(); 1222 }, 1223 1224 /** 1225 * Performs all actions necessary when a drag enters the tile page. 1226 * @param {Event} e A mouseover event for the drag enter. 1227 */ 1228 doDragEnter: function(e) { 1229 // Applies the mask so doppleganger tiles disappear into the fog. 1230 this.updateMask_(); 1231 1232 this.classList.add('animating-tile-page'); 1233 this.withinPageDrag_ = this.contains(currentlyDraggingTile); 1234 this.dragItemIndex_ = this.withinPageDrag_ ? 1235 currentlyDraggingTile.index : this.tileElements_.length; 1236 this.currentDropIndex_ = this.dragItemIndex_; 1237 1238 // The new tile may change the number of rows, hence the top margin 1239 // will change. 1240 if (!this.withinPageDrag_) 1241 this.updateTopMargin_(); 1242 1243 this.doDragOver(e); 1244 }, 1245 1246 /** 1247 * Performs all actions necessary when the user moves the cursor during 1248 * a drag over the tile page. 1249 * @param {Event} e A mouseover event for the drag over. 1250 */ 1251 doDragOver: function(e) { 1252 e.preventDefault(); 1253 1254 this.setDropEffect(e.dataTransfer); 1255 var newDragIndex = this.getWouldBeIndexForPoint_(e.pageX, e.pageY); 1256 if (newDragIndex < 0 || newDragIndex >= this.tileElements_.length) 1257 newDragIndex = this.dragItemIndex_; 1258 this.updateDropIndicator_(newDragIndex); 1259 }, 1260 1261 /** 1262 * Performs all actions necessary when the user completes a drop. 1263 * @param {Event} e A mouseover event for the drag drop. 1264 */ 1265 doDrop: function(e) { 1266 e.stopPropagation(); 1267 e.preventDefault(); 1268 1269 var index = this.currentDropIndex_; 1270 // Only change data if this was not a 'null drag'. 1271 if (!((index == this.dragItemIndex_) && this.withinPageDrag_)) { 1272 var adjustedIndex = this.currentDropIndex_ + 1273 (index > this.dragItemIndex_ ? 1 : 0); 1274 if (this.withinPageDrag_) { 1275 this.tileGrid_.insertBefore( 1276 currentlyDraggingTile, 1277 this.tileElements_[adjustedIndex]); 1278 this.tileMoved(currentlyDraggingTile, this.dragItemIndex_); 1279 } else { 1280 var originalPage = currentlyDraggingTile ? 1281 currentlyDraggingTile.tilePage : null; 1282 this.addDragData(e.dataTransfer, adjustedIndex); 1283 if (originalPage) 1284 originalPage.cleanupDrag(); 1285 } 1286 1287 // Dropping the icon may cause topMargin to change, but changing it 1288 // now would cause everything to move (annoying), so we leave it 1289 // alone. The top margin will be re-calculated next time the window is 1290 // resized or the page is selected. 1291 } 1292 1293 this.classList.remove('animating-tile-page'); 1294 this.cleanupDrag(); 1295 }, 1296 1297 /** 1298 * Appends the currently dragged tile to the end of the page. Called 1299 * from outside the page, e.g. when dropping on a nav dot. 1300 */ 1301 appendDraggingTile: function() { 1302 var originalPage = currentlyDraggingTile.tilePage; 1303 if (originalPage == this) 1304 return; 1305 1306 this.addDragData(null, this.tileElements_.length); 1307 if (originalPage) 1308 originalPage.cleanupDrag(); 1309 }, 1310 1311 /** 1312 * Makes sure all the tiles are in the right place after a drag is over. 1313 */ 1314 cleanupDrag: function() { 1315 this.repositionTiles_(currentlyDraggingTile); 1316 // Remove the drag mask. 1317 this.updateMask_(); 1318 }, 1319 1320 /** 1321 * Reposition all the tiles (possibly ignoring one). 1322 * @param {?Node} ignoreNode An optional node to ignore. 1323 * @private 1324 */ 1325 repositionTiles_: function(ignoreNode) { 1326 for (var i = 0; i < this.tileElements_.length; i++) { 1327 if (!ignoreNode || ignoreNode !== this.tileElements_[i]) 1328 this.positionTile_(i); 1329 } 1330 }, 1331 1332 /** 1333 * Updates the visual indicator for the drop location for the active drag. 1334 * @param {Event} e A MouseEvent for the drag. 1335 * @private 1336 */ 1337 updateDropIndicator_: function(newDragIndex) { 1338 var oldDragIndex = this.currentDropIndex_; 1339 if (newDragIndex == oldDragIndex) 1340 return; 1341 1342 var repositionStart = Math.min(newDragIndex, oldDragIndex); 1343 var repositionEnd = Math.max(newDragIndex, oldDragIndex); 1344 1345 for (var i = repositionStart; i <= repositionEnd; i++) { 1346 if (i == this.dragItemIndex_) 1347 continue; 1348 else if (i > this.dragItemIndex_) 1349 var adjustment = i <= newDragIndex ? -1 : 0; 1350 else 1351 var adjustment = i >= newDragIndex ? 1 : 0; 1352 1353 this.positionTile_(i, adjustment); 1354 } 1355 this.currentDropIndex_ = newDragIndex; 1356 }, 1357 1358 /** 1359 * Checks if a page can accept a drag with the given data. 1360 * @param {Event} e The drag event if the drag object. Implementations will 1361 * likely want to check |e.dataTransfer|. 1362 * @return {boolean} True if this page can handle the drag. 1363 */ 1364 shouldAcceptDrag: function(e) { 1365 return false; 1366 }, 1367 1368 /** 1369 * Called to accept a drag drop. Will not be called for in-page drops. 1370 * @param {Object} dataTransfer The data transfer object that holds the drop 1371 * data. This should only be used if currentlyDraggingTile is null. 1372 * @param {number} index The tile index at which the drop occurred. 1373 */ 1374 addDragData: function(dataTransfer, index) { 1375 assertNotReached(); 1376 }, 1377 1378 /** 1379 * Called when a tile has been moved (via dragging). Override this to make 1380 * backend updates. 1381 * @param {Node} draggedTile The tile that was dropped. 1382 * @param {number} prevIndex The previous index of the tile. 1383 */ 1384 tileMoved: function(draggedTile, prevIndex) { 1385 }, 1386 1387 /** 1388 * Sets the drop effect on |dataTransfer| to the desired value (e.g. 1389 * 'copy'). 1390 * @param {Object} dataTransfer The drag event dataTransfer object. 1391 */ 1392 setDropEffect: function(dataTransfer) { 1393 assertNotReached(); 1394 }, 1395 }; 1396 1397 return { 1398 getCurrentlyDraggingTile: getCurrentlyDraggingTile, 1399 setCurrentDropEffect: setCurrentDropEffect, 1400 TilePage: TilePage, 1401 }; 1402}); 1403